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

129 lines
5.3 KiB
Python
Raw Permalink Blame History

#!/usr/bin/env python3
from __future__ import annotations
import csv, re, zipfile
from pathlib import Path
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
import json
ROOT=Path('/root/work/pharmit-ghidra')
JSONP=ROOT/'out/a4_receipt_template_candidates/quickreport_deep/PM_MAIN.EXE.a4receipt.05.02812c03.현금영수증 문의.bin.qr.objects.json'
RAW=ROOT/'out/a4_receipt_three_raw.txt'
OUT=ROOT/'out/a4_receipt_quickreport_render_samples'
OUT.mkdir(parents=True, exist_ok=True)
SCALE=72/96
FONT='HYGothic-Medium'
try: pdfmetrics.registerFont(UnicodeCIDFont(FONT))
except Exception: FONT='Helvetica'
# Actual SQL fields seen in a4_receipt_three_raw.txt / decomp SQL
FIELD_MAP={
'Q_paname':'PANAME','Q_InDate':'_DATE_FMT','Q_Days':'_DAYS','Q_PreSerial':'PRESERIAL',
'Q_Price_A':'TOT_C','Q_Price_p':'BON_C','Q_Price_c':'CHUNG_C','Q_s_FastMon':'BI_PRICE','Q_Price_n':'_PAY_TOTAL',
'Q_CardPrice':'CARD_C','Q_CashPrint':'PAPER_C','Q_Chah':'CHASH_C','Q_PersonNum':'PANUM',
'Q_Year':'_YEAR','Q_Month':'_MONTH','Q_day':'_DAY',
'Q_name':'_PHARM_NAME','Q_comcode':'_BIZNO','Q_add':'_PHARM_ADDR','Q_comname':'_PHARMACIST',
'qrlApproval_Num':'_CASH_APPROVAL','Q_Bag':'_BAG',
}
MONEY={'TOT_C','BON_C','CHUNG_C','BI_PRICE','_PAY_TOTAL','CARD_C','PAPER_C','CHASH_C'}
def money(v):
try: return f"{int(float(str(v).replace(',','') or '0')):,}"
except Exception: return str(v or '')
def read_sql_rows():
lines=RAW.read_text(encoding='utf-8',errors='replace').splitlines()
data=[ln for ln in lines if '|' in ln and not set(ln.replace('|','').strip()) <= {'-'}]
rows=[]
for r in csv.DictReader(data, delimiter='|'):
if r.get('CUSCODE') and not r['CUSCODE'].startswith('-'):
rows.append({k:(v or '').strip() for k,v in r.items()})
return rows
def enrich(r):
r=dict(r)
s=re.sub(r'\D','',r.get('INDATE',''))
if len(s)>=8:
r['_YEAR'],r['_MONTH'],r['_DAY']=s[:4],s[4:6],s[6:8]
r['_DATE_FMT']=f'{s[:4]}-{s[4:6]}-{s[6:8]}'
else:
r['_YEAR']=r['_MONTH']=r['_DAY']=''; r['_DATE_FMT']=r.get('INDATE','')
pay=sum(int(float(r.get(k) or 0)) for k in ['CARD_C','CHASH_C','PAPER_C'])
if pay==0:
pay=int(float(r.get('BON_C') or 0)); r['CHASH_C']=str(pay)
r['_PAY_TOTAL']=str(pay); r['_DAYS']=''; r['_BAG']=''
if '<EFBFBD>' in r.get('PANAME','') or not r.get('PANAME'): r['PANAME']='홍길동'
panum=re.sub(r'\D','',r.get('PANUM',''))
r['PANUM']=(panum[:6]+'-'+panum[6:7]+'******') if len(panum)>=7 else '050930-3******'
r['_PHARM_NAME']='샘플약국'; r['_BIZNO']='123-45-67890'; r['_PHARM_ADDR']='서울특별시 샘플구 약국로 1'; r['_PHARMACIST']='김약사'; r['_CASH_APPROVAL']=''
return r
def load_objects():
j=json.loads(JSONP.read_text(encoding='utf-8'))
q=[o for o in j['objects'] if o.get('class','').startswith('TQR')]
# First PM_MAIN 약제비 계산서·영수증 form: q indices 19..103 from object inspection.
objs=q[19:104]
return objs
def prop(o,k,default=''):
return o.get('props',{}).get(k,default)
def value(r,o):
name=o.get('name',''); cap=str(prop(o,'Caption','') or '')
fld=FIELD_MAP.get(name)
if fld:
v=r.get(fld,'')
return money(v) if fld in MONEY else str(v)
# hide design placeholders if equal to object name
if name.startswith('Q_') and cap==name: return ''
return cap
def draw_shape(c,o,H):
try:
x=float(prop(o,'Left',0))*SCALE; top=float(prop(o,'Top',0))*SCALE; w=float(prop(o,'Width',0))*SCALE; h=float(prop(o,'Height',0))*SCALE
except Exception: return
if w<=0 or h<=0: return
y=H-top-h
c.setStrokeColor(colors.HexColor('#333333')); c.setLineWidth(0.45)
if w<=1.6 or h<=1.6: c.line(x,y,x+w,y+h)
else: c.rect(x,y,w,h,stroke=1,fill=0)
def draw_text(c,r,o,H):
txt=value(r,o)
if not txt: return
try:
x=float(prop(o,'Left',0))*SCALE; top=float(prop(o,'Top',0))*SCALE; w=float(prop(o,'Width',0))*SCALE; h=float(prop(o,'Height',0))*SCALE
except Exception: return
y=H-top-h
fh=max(5.5,min(abs(float(prop(o,'Font.Height',-13)))*0.72,16))
c.setFont(FONT,fh); c.setFillColor(colors.black)
fld=FIELD_MAP.get(o.get('name',''))
if fld in MONEY:
c.drawRightString(x+w-2,y+max(1,(h-fh)/2)+1,txt)
else:
c.drawString(x+1,y+max(1,(h-fh)/2)+1,txt)
def render(row, idx, objs):
r=enrich(row)
out=OUT/f'a4_pm_main_receipt_sql_{idx}_{r["PRESERIAL"]}.pdf'
c=canvas.Canvas(str(out),pagesize=A4); W,H=A4
for o in objs:
if o.get('class')=='TQRShape': draw_shape(c,o,H)
for o in objs:
if o.get('class') in {'TQRLabel','TQRDBText'}: draw_text(c,r,o,H)
c.setFont(FONT,6); c.setFillColor(colors.HexColor('#777777'))
c.drawString(4,4,f'PM_MAIN QuickReport receipt mapped / CUSCODE={r.get("CUSCODE")} PRESERIAL={r.get("PRESERIAL")}')
c.save(); return out
def main():
rows=read_sql_rows(); objs=load_objects(); outs=[]
for i,r in enumerate(rows[:3],1): outs.append(render(r,i,objs))
z=ROOT/'out/a4_pm_main_receipt_sql_mapped_20260628.zip'
with zipfile.ZipFile(z,'w',zipfile.ZIP_DEFLATED) as zz:
for p in outs+[Path(__file__),JSONP,RAW]: zz.write(p,p.relative_to(ROOT))
print('\n'.join(map(str,outs))); print(z); print(z.stat().st_size)
if __name__=='__main__': main()