#!/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('len(data): raise EOFError n=struct.unpack_from('len(data): raise EOFError n=struct.unpack_from('len(data): raise EOFError n=struct.unpack_from('len(data): raise EOFError n=struct.unpack_from('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