T800 wrote:
It looks like we are in need of functions/ahk features with which one can edit file's metadata/tags.
I also noticed that it seems like Office documents are most easily manipulated, but every other material I searched for (codes, sources, general info) is doomed to c++ or bust.
If this "♣DocumentSummaryInformation" data stream (it smells like unicode to me) could be edited, we would have basics for some real hard-core file manipulations.
I (somewhat successfully) created the following code to operate on this info through brute force (binary stream parsing). Take note of the hyperlinks referenced for data structure and functions used. Using this I can successfully read/write the "category" shown in the Windows Explorer file properties summary tab.
But after getting this far, I've decided to abandon it. Reasons: (1) it only works on NTFS (only tested on XP), (2) doesn't work for "recognized" file types, such as MSOffice documents, (3) my understanding is that Vista is fundamentally different in handling streams, (4) parsing a binary stream in AHK (using text strings) was really difficult compared to the C/C++ programming I'm used to.
Anyway, here it is. Maybe someone else can take this and run with it.
Code:
; read in NTFS alternate stream data
file = % "y:\ahk\test.txt:" Chr(05) "DocumentSummaryInformation" ; includes category
file2 = % "y:\ahk\test.txt:" Chr(05) "SummaryInformation" ; includes title, author, keywords, comments, etc.
res := BinRead(file,data,0,0)
text := Hex2Txt(data)
; parsing SummaryInformation
; credit: http://sedna-soft.de/summary-information-stream/
; get section length
start := 48*2+1
StringMid,out,data,start,8
sectionlen := DWord2Dec(out)
; get date/time stamp length
start := 96*2+1
StringMid,out,data,start,8
datelen := DWord2Dec(out)
; get category text length
start := 96*2+datelen*2-13
StringMid,out,data,start,8
categorylen := DWord2Dec(out)
; extract category text, prompt user to change
StringMid,out,data,96*2+datelen*2-5,categorylen*2+2
category := Hex2Txt(out)
InputBox, newcat, "Category", , , , , , , , ,%category%
; assemble new binary stream
StringLeft, dataout1, data, 48*2
StringMid, dataout2, data, 48*2+9, datelen*2+74
cattext := Txt2Hex(newcat)
catlen := StrLen(newcat)+1
catlentext := Dec2DWord(catlen)
sectionlen := 67+catlen
sectionlentext := Dec2DWord(sectionlen)
dataout = %dataout1%%sectionlentext%%dataout2%%catlentext%%cattext%00000000
; write new binary stream
MsgBox %data%`n`n%dataout%
res := BinWrite(file,dataout,0,0)
;MsgBox ErrorLevel = %ErrorLevel%`nBytes Written = %res%`n`n%dataout%
ExitApp
; BinWrite/BinRead code credit: http://www.autohotkey.com/forum/viewtopic.php?t=4546
/* ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; BinWrite ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
| - Open binary file
| - (Over)Write n bytes (n = 0: all)
| - From offset (offset < 0: counted from end)
| - Close file
| data -> file[offset + 0..n-1], rest of file unchanged
| Return #bytes actually written
*/ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
BinWrite(file, data, n=0, offset=0)
{
; Open file for WRITE (0x40..), OPEN_ALWAYS (4): creates only if it does not exists
h := DllCall("CreateFile","str",file,"Uint",0x40000000,"Uint",0,"UInt",0,"UInt",4,"Uint",0,"UInt",0)
IfEqual h,-1, SetEnv, ErrorLevel, -1
IfNotEqual ErrorLevel,0,Return,0 ; couldn't create the file
m = 0 ; seek to offset
IfLess offset,0, SetEnv,m,2
r := DllCall("SetFilePointerEx","Uint",h,"Int64",offset,"UInt *",p,"Int",m)
IfEqual r,0, SetEnv, ErrorLevel, -3
IfNotEqual ErrorLevel,0, {
t = %ErrorLevel% ; save ErrorLevel to be returned
DllCall("CloseHandle", "Uint", h)
ErrorLevel = %t% ; return seek error
Return 0
}
TotalWritten = 0
m := Ceil(StrLen(data)/2)
If (n <= 0 or n > m)
n := m
Loop %n%
{
StringLeft c, data, 2 ; extract next byte
StringTrimLeft data, data, 2 ; remove used byte
c = 0x%c% ; make it number
result := DllCall("WriteFile","UInt",h,"UChar *",c,"UInt",1,"UInt *",Written,"UInt",0)
TotalWritten += Written ; count written
if (!result or Written < 1 or ErrorLevel)
break
}
IfNotEqual ErrorLevel,0, SetEnv,t,%ErrorLevel%
h := DllCall("CloseHandle", "Uint", h)
IfEqual h,-1, SetEnv, ErrorLevel, -2
IfNotEqual t,,SetEnv, ErrorLevel, %t%
Return TotalWritten
}
/* ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; BinRead ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
| - Open binary file
| - Read n bytes (n = 0: all)
| - From offset (offset < 0: counted from end)
| - Close file
| data (replaced) <- file[offset + 0..n-1]
| Return #bytes actually read
*/ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
BinRead(file, ByRef data, n=0, offset=0)
{
h := DllCall("CreateFile","Str",file,"Uint",0x80000000,"Uint",3,"UInt",0,"UInt",3,"Uint",0,"UInt",0)
IfEqual h,-1, SetEnv, ErrorLevel, -1
IfNotEqual ErrorLevel,0,Return,0 ; couldn't open the file
m = 0 ; seek to offset
IfLess offset,0, SetEnv,m,2
r := DllCall("SetFilePointerEx","Uint",h,"Int64",offset,"UInt *",p,"Int",m)
IfEqual r,0, SetEnv, ErrorLevel, -3
IfNotEqual ErrorLevel,0, {
t = %ErrorLevel% ; save ErrorLevel to be returned
DllCall("CloseHandle", "Uint", h)
ErrorLevel = %t% ; return seek error
Return 0
}
TotalRead = 0
data =
IfEqual n,0, SetEnv n,0xffffffff ; almost infinite
format = %A_FormatInteger% ; save original integer format
SetFormat Integer, Hex ; for converting bytes to hex
Loop %n%
{
result := DllCall("ReadFile","UInt",h,"UChar *",c,"UInt",1,"UInt *",Read,"UInt",0)
if (!result or Read < 1 or ErrorLevel)
break
TotalRead += Read ; count read
c += 0 ; convert to hex
StringTrimLeft c, c, 2 ; remove 0x
c = 0%c% ; pad left with 0
StringRight c, c, 2 ; always 2 digits
data = %data%%c% ; append 2 hex digits
}
IfNotEqual ErrorLevel,0, SetEnv,t,%ErrorLevel%
h := DllCall("CloseHandle", "Uint", h)
IfEqual h,-1, SetEnv, ErrorLevel, -2
IfNotEqual t,,SetEnv, ErrorLevel, %t%
SetFormat Integer, %format% ; restore original format
Totalread += 0 ; convert to original format
Return TotalRead
}
; Txt2Hex/Hex2Txt code credit: http://www.autohotkey.com/forum/viewtopic.php?t=17928
Txt2Hex(Txt) {
format := A_FormatInteger
StringSplit, TxtArray, Txt
loop, %TxtArray0%
{
CurrentTxt := TxtArray%A_Index%
blah := Asc(CurrentTxt)
SetFormat, Integer, Hex
Hex := Asc(CurrentTxt)
SetFormat, Integer, Decimal
StringReplace, Hex, Hex, 0x,,All
If StrLen(Hex) = 1
{
Hex := Hex . "0"
}
Hex%A_Index% := Hex
}
Hex=
Loop, %TxtArray0%
{
Hex := Hex . Hex%A_Index%
}
SetFormat, Integer, %Format%
return %Hex%
}
Hex2Txt(Hex) {
format := A_FormatInteger
go=1
Txt=
HexLen := StrLen(Hex)
Loop, %HexLen%
{
If go=1
{
go=0
HexSet := "0x" . SubStr(Hex, A_Index, 2)
SetFormat, Integer, Decimal
HexSet += 0
Txt := Txt . Chr(HexSet)
}
else
{
go=1
}
}
SetFormat, Integer, %Format%
return %Txt%
}
DWord2Dec(Txt) {
;convert to big endian
byte0 := SubStr(Txt,1,2)
byte1 := SubStr(Txt,3,2)
byte2 := SubStr(Txt,5,2)
byte3 := SubStr(Txt,7,2)
hex := byte3 . byte2 . byte1 . byte0
;convert to decimal
result := "0x" . hex
result += 0
; return resulting decimal value
return result
}
Dec2DWord(Val) {
;convert decimal to hex
SetFormat, integer, hex
Val += 0
SetFormat, integer, d
;extract text
Txt := SubStr(Val,3)
len := StrLen(Txt)
; convert to byte-wise little endian format
if (len=1)
DWord = 0%Txt%000000
else if (len=2)
DWord = %Txt%000000
else if (len=3)
DWord := SubStr(Txt,2,2) . "0" . SubStr(Txt,3,1) . "0000"
else if (len=4)
DWord := SubStr(Txt,3,2) . SubStr(Txt,1,2) . "0000"
else if (len=5)
DWord := SubStr(Txt,4,2) . SubStr(Txt,2,2) . "0" . SubStr(Txt,1,1) . "00"
else if (len=6)
DWord := SubStr(Txt,5,2) . SubStr(Txt,3,2) . SubStr(Txt,1,2) . "00"
else if (len=7)
DWord := SubStr(Txt,6,2) . SubStr(Txt,4,2) . SubStr(Txt,2,2) . "0" . SubStr(Txt,1,1)
else if (len=8)
DWord := SubStr(Txt,7,2) . SubStr(Txt,5,2) . SubStr(Txt,3,2) . SubStr(Txt,1,2)
else
DWord = 00000000
; return resulting string
return %DWord%
}