#!/usr/bin/env python3 from __future__ import annotations import csv, re, json, 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 ROOT=Path('/root/work/pharmit-ghidra') LAYOUT=ROOT/'out/a4_receipt_template_candidates/quickreport_receipt_deep/quickreport_receipt_canonical_top.csv' 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' # QuickReport labels found in PIT template -> actual SQL field names from A4 amount query. FIELD_MAP={ # common receipt form 'Q_paname':'PANAME', 'Q_name2':'PANAME', 'Q_num':'PANUM', 'Q_InDate':'INDATE', 'Q_Year':'_YEAR', 'Q_year':'_YEAR', 'Q_year1':'_YEAR', 'Q_Month':'_MONTH', 'Q_mon':'_MONTH', 'Q_mon1':'_MONTH', 'Q_day':'_DAY', 'Q_day1':'_DAY', 'Q_Days':'_DAYS', 'Q_PreSerial':'PRESERIAL', 'QR_PreSerial':'PRESERIAL', 'Q_Price_A':'TOT_C', 'QR_Price_a':'TOT_C', 'Q_Price_p':'BON_C', 'QR_PriceP':'BON_C', 'QR_Price_psum':'BI_CNT', 'Q_Price_c':'CHUNG_C', 'QR_PriceC':'CHUNG_C', 'QR_Pricec1':'CHUNG_C', 'Q_s_FastMon':'BI_PRICE', 'QR_PriceN':'BI_PRICE', 'Q_Price_n':'_PAY_TOTAL', 'Q_Card':'CARD_C', 'Q_Cash':'CHASH_C', 'Q_PCash':'PAPER_C', 'Q_TotGum':'_PAY_TOTAL', # company/pharmacy placeholders; real pharmacy master query is separate, seed for visual proof 'Q_iscode':'_PHARM_ISCODE', 'Q_isname':'_PHARM_NAME', 'Q_istel':'_PHARM_TEL', 'Q_tel':'_PHARM_TEL', 'Q_add':'_PHARM_ADDR', 'Q_post':'_PHARM_POST', 'Q_name':'_PHARM_NAME', 'Q_comcode':'_BIZNO', 'Q_comname':'_PHARMACIST', 'Q_name1':'_PHARMACIST', 'Q_COMMIT_CODE':'_COMMIT_CODE', 'Q_COMMIT_NAME':'_COMMIT_NAME', 'Q_Unit_Gubun':'_UNIT_GUBUN', 'qrlApproval_Num':'_CASH_APPROVAL', } MONEY_FIELDS={'TOT_C','CHUNG_C','BON_C','CARD_C','CHASH_C','PAPER_C','BI_PRICE','BI_CNT','_PAY_TOTAL'} # Keep only the actual receipt/certificate templates in the canonical blob, excluding prescription-review form tail. KEEP_KEYWORDS=['약제비','영수증','진료비','납입 확인서','소득공제','환자성명','조제일자','총 수 납','요양기관','사업자','상 호','대 표 자','전 화 번 호','현금','카드'] KEEP_NAMES=set(FIELD_MAP)|{'Q_tag1','Q_tag2'} def load_layout(form='certificate'): with LAYOUT.open(encoding='utf-8-sig') as f: rows=list(csv.DictReader(f)) # The selected binary is a concatenated QR blob. Band indices reveal independent forms: # 0..77 = 진료비(약제비) 납입 확인서 summary/table # 78..169 = 약제비 계산서·영수증 # 171..394/396..626 = 청구/기관확인서 variants ranges={ 'certificate': (0,77), 'receipt': (78,169), 'claim1': (171,394), 'claim2': (396,626), } lo,hi=ranges.get(form,ranges['certificate']) out=[] for r in rows: idx=int(r['idx']) if not (lo <= idx <= hi): continue cls=r.get('class','') cap=(r.get('caption') or r.get('text') or '') name=r.get('name','') if cls in {'TQRShape','TQRBand'}: out.append(r) elif cls in {'TQRLabel','TQRDBText'}: # In an isolated form keep most labels; mapped dynamic placeholders are replaced. if cap or name in KEEP_NAMES or name.startswith(('QR_Price','QR_PreSerial')): out.append(r) return out def parse_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()) <= {'-'}] reader=csv.DictReader(data, delimiter='|') rows=[] for r in reader: if r.get('CUSCODE') and not r['CUSCODE'].startswith('-'): rows.append({k:(v or '').strip() for k,v in r.items()}) return rows def money(v): try: return f"{int(float(str(v).replace(',','') or '0')):,}" except Exception: return str(v or '') def ymd_parts(s): s=re.sub(r'\D','',s or '') if len(s)>=8: return s[:4],s[4:6],s[6:8],f'{s[:4]}-{s[4:6]}-{s[6:8]}' return '','','',s def enrich(r): r=dict(r) y,m,d,fmt=ymd_parts(r.get('INDATE')) r['_YEAR']=y; r['_MONTH']=m; r['_DAY']=d; r['_DATE_FMT']=fmt # Actual SQL sample had payment method columns zero in these rows; PIT sometimes updates CD_SELL_MASTE later. 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']='' # raw names are codepage-garbled in saved text; mask/use generic visual proof, keep ids real. r['PANAME']='홍길동' if '�' in r.get('PANAME','') else r.get('PANAME') or '홍길동' panum=r.get('PANUM') or '' r['PANUM']=(panum[:6]+'-'+panum[6:7]+'******') if len(re.sub(r'\D','',panum))>=7 else '******-*******' r['_PHARM_ISCODE']='12345678' r['_PHARM_NAME']='샘플약국' r['_PHARM_TEL']='02-1234-5678' r['_PHARM_ADDR']='서울특별시 샘플구 약국로 1' r['_PHARM_POST']='01234' r['_BIZNO']='123-45-67890' r['_PHARMACIST']='김약사' r['_COMMIT_CODE']='' r['_COMMIT_NAME']='' r['_UNIT_GUBUN']='약국' r['_CASH_APPROVAL']='' return r def obj_value(r, o): name=o['name']; cap=o.get('caption') or o.get('text') or '' fld=FIELD_MAP.get(name) if fld: v=r.get(fld,'') return money(v) if fld in MONEY_FIELDS else str(v) # TQRDBText from actual sqlMain in certificate table df=o.get('datafield') or '' if df and df in r: return money(r[df]) if df in MONEY_FIELDS else r[df] # drop design placeholders that are dynamic but not mapped if name.startswith(('Q_','QR_Price','QR_PreSerial','qrlApproval')) and cap==name: return '' return cap def draw_text(c, r, row, page_h): try: x=float(row['left'] or 0)*SCALE; top=float(row['top'] or 0)*SCALE w=float(row['width'] or 0)*SCALE; h=float(row['height'] or 0)*SCALE except Exception: return txt=obj_value(r,row) if not txt: return y=page_h-top-h fh=abs(float(row.get('font_height') or -10))*0.72 fh=max(5.5,min(fh,17)) c.setFont(FONT,fh) c.setFillColor(colors.black) # right-align amount/name-ish labels by PIT naming convention if row['name'] in FIELD_MAP and FIELD_MAP[row['name']] in MONEY_FIELDS: c.drawRightString(x+w-1,y+max(1,(h-fh)/2)+1,txt) else: # simplistic wrap for long notes if len(txt)>70 and w>100: words=list(txt) line=''; yy=y+h-fh maxchars=max(10,int(w/(fh*0.55))) for ch in words: line+=ch if len(line)>=maxchars or ch=='\n': c.drawString(x+1,yy,line.strip()); yy-=fh*1.15; line='' if yy=y: c.drawString(x+1,yy,line) else: c.drawString(x+1,y+max(1,(h-fh)/2)+1,txt) def draw_shape(c,row,page_h): try: x=float(row['left'] or 0)*SCALE; top=float(row['top'] or 0)*SCALE w=float(row['width'] or 0)*SCALE; h=float(row['height'] or 0)*SCALE except Exception: return if w<=0 or h<=0: return y=page_h-top-h c.setStrokeColor(colors.HexColor('#444444')) c.setLineWidth(0.35) if w<=2*SCALE or h<=2*SCALE: c.line(x,y,x+w,y+h) else: c.rect(x,y,w,h,stroke=1,fill=0) def render_one(data_row, idx, layout, form): r=enrich(data_row) out=OUT/f'a4_quickreport_{form}_{idx}_{r["PRESERIAL"]}.pdf' c=canvas.Canvas(str(out), pagesize=A4) W,H=A4 # QuickReport object order: shapes first, text on top. for o in layout: if o['class']=='TQRShape': draw_shape(c,o,H) for o in layout: if o['class'] in {'TQRLabel','TQRDBText'}: draw_text(c,r,o,H) c.setFont(FONT,6); c.setFillColor(colors.HexColor('#777777')) c.drawString(4,4,f'PIT QuickReport mapped render / CUSCODE={r.get("CUSCODE")} PRESERIAL={r.get("PRESERIAL")} / source SQL fields: TOT_C CHUNG_C BON_C CARD_C CHASH_C PAPER_C BI_PRICE') c.save() return out def main(): rows=parse_rows() outs=[] for form in ['certificate','receipt']: layout=load_layout(form) for i,r in enumerate(rows[:3],1): outs.append(render_one(r,i,layout,form)) z=ROOT/'out/a4_quickreport_receipt_sql_mapped_20260628.zip' with zipfile.ZipFile(z,'w',zipfile.ZIP_DEFLATED) as zz: for p in outs+[Path(__file__),LAYOUT,RAW]: zz.write(p,p.relative_to(ROOT)) print('\n'.join(str(p) for p in outs)) print(z) print(z.stat().st_size) if __name__=='__main__': main()