Files
pit3000-print-reverse-engin…/scripts/summarize_quickreport_receipts.py
thug0bin bde81cdc7e 초기 정리: PIT3000 분석 자료 업로드
- 좌표/SQL/API 분석 문서화
- 재현 스크립트와 예시 응답 추가
- 개인정보/원본 실행파일 제외
2026-06-29 00:27:06 +00:00

111 lines
5.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import csv, json, re, subprocess
from collections import Counter
from pathlib import Path
ROOT=Path('/root/work/pharmit-ghidra')
CAND=ROOT/'out/a4_receipt_template_candidates'
OUT=CAND/'quickreport_receipt_deep'
OUT.mkdir(parents=True, exist_ok=True)
PARSER=ROOT/'scripts/parse_fastreport_bin.py'
QR_CLASSES={'TQuickRep','TQRBand','TQRChildBand','TQRLabel','TQRDBText','TQRShape','TQRImage','TQRDBImage','TQRRichText','TQRExpr','TQRCompositeReport'}
KEYWORDS=['약제비 계산서','약제비','영수증','진료비','납입확인서','납입 확인서','소득공제','요양기관 종류','환 자','환자성명','약 제 비 총 액','총 수 납 금 액']
def text(o):
pr=o.get('props',{})
for k in ['Caption','Text','DataField','FieldName','Memo.UTF8W']:
v=pr.get(k)
if isinstance(v,str) and v.strip(): return v.strip()
if isinstance(v,list):
s='\n'.join(str(x) for x in v if str(x).strip()).strip()
if s: return s
return ''
def parse_file(p:Path):
jpath=OUT/(p.name+'.objects.json')
if not jpath.exists():
subprocess.run(['python3',str(PARSER),str(p)], stdout=jpath.open('w'), stderr=subprocess.DEVNULL, check=False)
try: return json.loads(jpath.read_text(encoding='utf-8'))
except Exception: return {'objects':[]}
def bounds(objs):
xs=[]; ys=[]
for o in objs:
pr=o.get('props',{})
try:
if 'Left' in pr and 'Width' in pr:
xs += [float(pr['Left']), float(pr['Left'])+float(pr['Width'])]
if 'Top' in pr and 'Height' in pr:
ys += [float(pr['Top']), float(pr['Top'])+float(pr['Height'])]
except Exception: pass
return ([round(min(xs),3),round(max(xs),3)] if xs else None, [round(min(ys),3),round(max(ys),3)] if ys else None)
def group_reports(objs):
q=[o for o in objs if o.get('class') in QR_CLASSES]
starts=[i for i,o in enumerate(q) if o.get('class') in {'TQuickRep','TQRCompositeReport'}]
if not starts:
return [('qr_blob', q)] if q else []
groups=[]
for n,si in enumerate(starts):
ei=starts[n+1] if n+1<len(starts) else len(q)
name=q[si].get('name') or f'report_{n}'
groups.append((name,q[si:ei]))
return groups
def score_group(g):
txt=' '.join(text(o) for o in g)
return sum(10 for k in KEYWORDS if k in txt) + sum(1 for o in g if o.get('class')=='TQRLabel')
def main():
all_rows=[]; summaries=[]
for p in sorted(CAND.glob('*.bin')):
if not any(s in p.name for s in ['진료비','소득공제','요양기관','현금영수증']):
continue
j=parse_file(p); objs=j.get('objects',[])
groups=group_reports(objs)
for gi,(gname,gobjs) in enumerate(groups):
captions=[text(o) for o in gobjs if text(o)]
joined=' | '.join(captions)
if not any(k in joined for k in KEYWORDS):
continue
bx,by=bounds(gobjs)
classes=Counter(o.get('class') for o in gobjs)
summ={'source_bin':p.name,'group_index':gi,'group_name':gname,'objects':len(gobjs),'classes':dict(classes),'bounds_x':bx,'bounds_y':by,'score':score_group(gobjs),'key_captions':[c for c in captions if any(k in c for k in KEYWORDS)][:80]}
summaries.append(summ)
for idx,o in enumerate(gobjs):
pr=o.get('props',{})
if o.get('class') not in QR_CLASSES: continue
all_rows.append({
'source_bin':p.name,'group_index':gi,'group_name':gname,'idx':idx,'class':o.get('class'),'name':o.get('name'),'offset':o.get('offset'),
'left':pr.get('Left',''),'top':pr.get('Top',''),'width':pr.get('Width',''),'height':pr.get('Height',''),
'caption':pr.get('Caption',''),'text':text(o),'datafield':pr.get('DataField') or pr.get('FieldName') or '', 'dataset':pr.get('DataSet') or pr.get('DataSetName') or '',
'font_height':pr.get('Font.Height',''),'alignment':pr.get('Alignment') or pr.get('HAlign') or '',
'props':json.dumps(pr,ensure_ascii=False,sort_keys=True)
})
summaries=sorted(summaries,key=lambda x:(-x['score'],x['source_bin'],x['group_index']))
(OUT/'quickreport_receipt_summary.json').write_text(json.dumps(summaries,ensure_ascii=False,indent=2),encoding='utf-8')
with (OUT/'quickreport_receipt_objects_all.csv').open('w',newline='',encoding='utf-8-sig') as f:
fields=list(all_rows[0].keys()) if all_rows else ['source_bin']
w=csv.DictWriter(f,fieldnames=fields); w.writeheader(); w.writerows(all_rows)
md=['# QuickReport receipt templates deep summary','']
for s in summaries:
md += [f"## score={s['score']} {s['source_bin']} group={s['group_index']} `{s['group_name']}`",
f"- objects: {s['objects']}",f"- bounds_x: {s['bounds_x']}",f"- bounds_y: {s['bounds_y']}",f"- classes: `{s['classes']}`",'- key captions:']
md += [f" - {c}" for c in s['key_captions'][:40]]
md += ['']
(OUT/'quickreport_receipt_summary.md').write_text('\n'.join(md),encoding='utf-8')
# Write top group's canonical CSV for immediate renderer work.
if summaries:
top=summaries[0]
top_rows=[r for r in all_rows if r['source_bin']==top['source_bin'] and int(r['group_index'])==int(top['group_index'])]
with (OUT/'quickreport_receipt_canonical_top.csv').open('w',newline='',encoding='utf-8-sig') as f:
w=csv.DictWriter(f,fieldnames=list(top_rows[0].keys())); w.writeheader(); w.writerows(top_rows)
(OUT/'quickreport_receipt_canonical_top.json').write_text(json.dumps(top,ensure_ascii=False,indent=2),encoding='utf-8')
print('TOP',top)
print(OUT/'quickreport_receipt_summary.md')
print(OUT/'quickreport_receipt_objects_all.csv')
print(OUT/'quickreport_receipt_canonical_top.csv')
if __name__=='__main__': main()