from pathlib import Path from io import BytesIO from xml.etree import ElementTree as ET import json,html,base64,re,hashlib from PIL import Image,ImageDraw from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader from compose import BOOK,ROOT,font,wrap,SCALE # Each newly generated panel is an illustration asset. Typeset and assemble the # assets into the comic page, rather than placing letters over its characters. RECTS=json.loads((ROOT/'tmp/source-rects.json').read_text()) LAYOUT=[] OVERLAY={ (1,3):(450,896,537,200), (2,0):(40,24,944,120),(2,1):(450,530,530,200), (4,0):(40,32,580,150),(4,1):(40,566,450,100), (5,0):(36,28,520,74),(5,1):(614,32,376,170),(5,2):(400,555,580,150), (6,0):(40,36,620,240),(6,1):(40,650,944,120), (7,0):(40,36,590,250),(7,1):(36,616,314,200),(7,2):(410,620,570,200), (8,3):(40,1120,650,210) } def compose(page): n=page['number'];sourcepath=ROOT/'art'/f'page-{n:02d}.png' source=Image.open(sourcepath).convert('RGB') rects=RECTS[f'page-{n:02d}'];assert len(rects)==len(page['panels']) rows=[] for k,rect in enumerate(rects): if not rows or abs(rows[-1][0][1][1]-rect[1])>10:rows.append([]) rows[-1].append((k,rect)) scratch=ImageDraw.Draw(Image.new('RGB',(10,10))) rowdata=[] for row in rows: entries=[] for k,rect in row: x0,y0,x1,y1=rect;w=x1-x0;pad=24 if w>500 else 20 overlay=OVERLAY.get((n,k)) size=30 if w>800 or overlay else 28 if overlay: tx,ty,tw,th=overlay for size in [30,29,28,27,26,25,24]: step=round(size*1.2) groups=[(speaker,wrap(text,size,tw,scratch)) for speaker,text in page['panels'][k]['dialogue']] used=sum(len(lines)*step for _,lines in groups)+max(0,len(groups)-1)*12 if used<=th:break else:raise ValueError(f'Overlay {n}/{k+1} does not fit') else: step=round(size*1.2) groups=[(speaker,wrap(text,size,w-pad*2,scratch)) for speaker,text in page['panels'][k]['dialogue']] h=sum(len(lines)*step for _,lines in groups)+max(0,len(groups)-1)*12 textheight=h+36 if groups and not overlay else 0 entries.append((k,rect,pad,size,step,groups,textheight,overlay)) rowdata.append((entries,max(e[-2] for e in entries),max(e[1][3]-e[1][1] for e in entries))) gap=18;margin=12 H=margin*2+sum(band+arth for _,band,arth in rowdata)+gap*(len(rowdata)-1) pageimg=Image.new('RGB',(1024*SCALE,H*SCALE),'white');d=ImageDraw.Draw(pageimg) data=base64.b64encode(sourcepath.read_bytes()).decode() svg=[f''] y=margin for entries,band,arth in rowdata: for k,rect,pad,size,step,groups,textheight,overlay in entries: x0,y0,x1,y1=rect;w=x1-x0;h=y1-y0 # Pure page composition: the illustration pixels remain untouched. art=source.crop(tuple(rect)).resize((w*SCALE,h*SCALE),Image.Resampling.LANCZOS) desty=y+band pageimg.paste(art,(x0*SCALE,desty*SCALE)) clipid=f'p{n}f{k}' svg.append(f'') if overlay: tx,oy,tw,th=overlay;ty=desty+oy-y0 else:tx=x0+pad;ty=y+14 for speaker,lines in groups: color=BOOK['characters'][speaker]['color'] for line in lines: d.text((tx*SCALE,ty*SCALE),line,font=font(size),fill=color,anchor='lt') svg.append(f'{html.escape(line)}') ty+=step ty+=12 if groups and not overlay:assert ty-12<=desty LAYOUT.append({'page':n,'panel':k+1,'source_rect':rect,'art_rect':[x0,desty,w,h],'text_size':size if groups else None,'lines':sum(len(ls) for _,ls in groups),'page_height':H,'lettering':'inside' if overlay else 'above'}) y+=band+arth+gap svg.append('') path=ROOT/'pages'/f'page-{n:02d}.png';pageimg.save(path) svgpath=ROOT/'editable'/f'page-{n:02d}.svg';svgpath.write_text('\n'.join(svg)) actual=''.join(e.text or '' for e in ET.parse(svgpath).findall('{http://www.w3.org/2000/svg}text')) expected=''.join(text for p in page['panels'] for _,text in p['dialogue']) assert re.sub(r'\s+','',actual)==re.sub(r'\s+','',expected) return path if __name__=='__main__': import sys nums=[int(x) for x in sys.argv[1:]] pages=[p for p in BOOK['pages'] if not nums or p['number'] in nums] paths=[compose(p) for p in pages] if not nums: out=ROOT/'output/pdf';out.mkdir(parents=True,exist_ok=True) pdf=out/'Argon讨论-第二组.pdf' c=canvas.Canvas(str(pdf));c.setTitle(BOOK['title']);c.setAuthor('Codex') for path in paths: im=Image.open(path).convert('RGB');w,h=im.size;b=BytesIO();im.save(b,format='JPEG',quality=96,subsampling=0);b.seek(0) c.setPageSize((512,512*h/w));c.drawImage(ImageReader(b),0,0,width=512,height=512*h/w);c.showPage() c.save() sheet=Image.new('RGB',(1840,2100),'#e9e6ed') for i,path in enumerate(paths): im=Image.open(path);im.thumbnail((440,1010));sheet.paste(im,((i%4)*465,(i//4)*1050)) sheet.save(ROOT/'overview.jpg',quality=95) reader='''漫画
''' reader+=''.join(f'' for i in range(1,9))+'
' (ROOT/'index.html').write_text(reader) (ROOT/'tmp/final-layout.json').write_text(json.dumps(LAYOUT,ensure_ascii=False,indent=2)) print(json.dumps({'rendered':[p['number'] for p in pages],'page_heights':sorted(set(x['page_height'] for x in LAYOUT)),'font_sizes':sorted(set(x['text_size'] for x in LAYOUT if x['text_size']))},ensure_ascii=False))