170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
import json, math, struct, sys
|
|
from pathlib import Path
|
|
|
|
CLASSES=[
|
|
b'TfrxMemoView', b'TfrxShapeView', b'TfrxLineView', b'TfrxPictureView',
|
|
b'TfrxReport', b'TfrxReportPage', b'TfrxMasterData', b'TfrxDetailData',
|
|
b'TfrxHeader', b'TfrxFooter', b'TfrxPageHeader', b'TfrxPageFooter',
|
|
b'TfrxDBDataset', b'TfrxUserDataSet', b'TfrxDataPage', b'TfrxDialogPage',
|
|
# QuickReport / QRDesign objects used by older PIT A4 forms
|
|
b'TQuickRep', b'TQRBand', b'TQRChildBand', b'TQRLabel', b'TQRDBText', b'TQRShape',
|
|
b'TQRRichText', b'TQRExpr', b'TQRImage', b'TQRDBImage', b'TQRCompositeReport',
|
|
]
|
|
# Keep essentially all useful report-layout/data-binding properties. Large binary payloads
|
|
# are still summarized by read_value() as {'binary_len': N}.
|
|
PROP_KEEP={
|
|
'Left','Top','Width','Height','Font.Height','Font.Name','Font.Style','Font.Charset','Font.Color',
|
|
'HAlign','VAlign','Memo.UTF8W','Memo.Text','Text','Caption','Frame.Typ','Frame.Width','Frame.Color',
|
|
'Color','DataSet','DataSetName','DataField','DataSource','FieldName','Master','Detail','Filter',
|
|
'Format','FormatStr','DisplayFormat','ExpressionDelimiters','Name','Tag','Visible','Printable',
|
|
'StretchMode','WordWrap','AutoWidth','CharSpacing','LineSpacing','GapX','GapY','Rotation','Align',
|
|
'Band','ParentFont','Transparent','BrushStyle','Frame.Style','Frame.ShadowWidth','Frame.ShadowColor',
|
|
'Restrictions','Page','PaperWidth','PaperHeight','PaperSize','Orientation','Margins.Left','Margins.Top',
|
|
'Margins.Right','Margins.Bottom','Columns','ColumnWidth','ColumnPositions','RowCount','Version',
|
|
}
|
|
|
|
# Delphi DFM binary value kinds (subset)
|
|
VK={0:'Null',1:'List',2:'Int8',3:'Int16',4:'Int32',5:'Extended',6:'String',7:'Ident',8:'False',9:'True',10:'Binary',11:'Set',12:'LString',13:'Nil',14:'Collection',15:'Single',16:'Currency',17:'Date',18:'WString',19:'Int64',20:'UTF8String'}
|
|
|
|
def ext80(b: bytes) -> float:
|
|
if len(b)!=10: return math.nan
|
|
lo=int.from_bytes(b[:8],'little')
|
|
se=int.from_bytes(b[8:10],'little')
|
|
sign=-1 if (se & 0x8000) else 1
|
|
exp=se & 0x7fff
|
|
if exp==0 and lo==0: return 0.0
|
|
if exp==0x7fff: return math.inf*sign
|
|
# 80-bit extended has explicit integer bit, value = lo/2^63 * 2^(exp-bias)
|
|
return sign * (lo/(1<<63)) * (2**(exp-16383))
|
|
|
|
def read_lp(data,i):
|
|
if i>=len(data): raise EOFError
|
|
n=data[i]; i+=1
|
|
if i+n>len(data): raise EOFError
|
|
try: s=data[i:i+n].decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
try: s=data[i:i+n].decode('cp949')
|
|
except Exception: s=data[i:i+n].decode('latin1','replace')
|
|
return s,i+n
|
|
|
|
def read_value(data,i):
|
|
if i>=len(data): raise EOFError
|
|
tag=data[i]; i+=1
|
|
if tag==0: return None,i
|
|
if tag==1:
|
|
vals=[]
|
|
# Delphi DFM vaList: nested values terminated by vaNull (0).
|
|
while i < len(data):
|
|
if data[i] == 0:
|
|
i += 1
|
|
break
|
|
v,i = read_value(data,i)
|
|
vals.append(v)
|
|
return vals,i
|
|
if tag==2:
|
|
v=struct.unpack_from('<b',data,i)[0]; return v,i+1
|
|
if tag==3:
|
|
v=struct.unpack_from('<h',data,i)[0]; return v,i+2
|
|
if tag==4:
|
|
v=struct.unpack_from('<i',data,i)[0]; return v,i+4
|
|
if tag==5:
|
|
return ext80(data[i:i+10]), i+10
|
|
if tag==6 or tag==7:
|
|
return read_lp(data,i)
|
|
if tag==8: return False,i
|
|
if tag==9: return True,i
|
|
if tag==10:
|
|
# binary blob: int32 length + bytes
|
|
if i+4>len(data): raise EOFError
|
|
n=struct.unpack_from('<I',data,i)[0]; i+=4
|
|
return {'binary_len':n}, min(len(data),i+n)
|
|
if tag==11:
|
|
vals=[]
|
|
while i < len(data) and data[i]!=0:
|
|
s,i=read_lp(data,i); vals.append(s)
|
|
if i<len(data) and data[i]==0: i+=1
|
|
return vals,i
|
|
if tag==12:
|
|
if i+4>len(data): raise EOFError
|
|
n=struct.unpack_from('<I',data,i)[0]; i+=4
|
|
raw=data[i:i+n]; i+=n
|
|
return raw.decode('cp949','replace'),i
|
|
if tag==15:
|
|
return struct.unpack_from('<f',data,i)[0],i+4
|
|
if tag==18:
|
|
if i+4>len(data): raise EOFError
|
|
n=struct.unpack_from('<I',data,i)[0]; i+=4
|
|
raw=data[i:i+n*2]; i+=n*2
|
|
return raw.decode('utf-16le','replace'),i
|
|
if tag==19:
|
|
return struct.unpack_from('<q',data,i)[0],i+8
|
|
if tag==20:
|
|
if i+4>len(data): raise EOFError
|
|
n=struct.unpack_from('<I',data,i)[0]; i+=4
|
|
raw=data[i:i+n]; i+=n
|
|
return raw.decode('utf-8','replace'),i
|
|
# unknown; fail to let caller resync
|
|
raise ValueError(f'unknown tag {tag} at {i-1:x}')
|
|
|
|
def find_objects(data):
|
|
starts=[]
|
|
for cls in CLASSES:
|
|
pos=0
|
|
while True:
|
|
j=data.find(cls,pos)
|
|
if j<0: break
|
|
if j>0 and data[j-1]==len(cls):
|
|
starts.append(j-1)
|
|
pos=j+1
|
|
return sorted(set(starts))
|
|
|
|
def parse_object_at(data,start,next_start=None):
|
|
i=start
|
|
cls,i=read_lp(data,i)
|
|
name,i=read_lp(data,i)
|
|
props={}
|
|
limit=next_start if next_start else min(len(data), start+5000)
|
|
# Parse until next object start; binary blobs can skip far, so limit may be before i after Picture.Data.
|
|
while i < len(data) and i < limit:
|
|
if data[i]==0:
|
|
i+=1; continue
|
|
# Avoid eating next object header as property if at a known start.
|
|
if i in OBJECT_STARTS and i!=start:
|
|
break
|
|
try:
|
|
prop,i2=read_lp(data,i)
|
|
except Exception:
|
|
break
|
|
if not prop or len(prop)>80 or any(ord(ch)<32 for ch in prop):
|
|
break
|
|
try:
|
|
val,i3=read_value(data,i2)
|
|
except Exception:
|
|
# Some extracted chunks are not aligned; advance one byte and try to keep scanning.
|
|
i=i+1
|
|
continue
|
|
if prop in PROP_KEEP or prop in {'Tag','Name'}:
|
|
props[prop]=val
|
|
i=i3
|
|
return {'class':cls,'name':name,'offset':start,'props':props}
|
|
|
|
def main(path):
|
|
global OBJECT_STARTS
|
|
data=Path(path).read_bytes()
|
|
OBJECT_STARTS=find_objects(data)
|
|
objs=[]
|
|
for idx,s in enumerate(OBJECT_STARTS):
|
|
ns=OBJECT_STARTS[idx+1] if idx+1<len(OBJECT_STARTS) else None
|
|
try:
|
|
o=parse_object_at(data,s,ns)
|
|
if o['class'] in [c.decode() for c in CLASSES]:
|
|
objs.append(o)
|
|
except Exception:
|
|
pass
|
|
print(json.dumps({'file':Path(path).name,'objects':objs}, ensure_ascii=False, indent=2))
|
|
|
|
if __name__=='__main__':
|
|
main(sys.argv[1])
|