from pathlib import Path from io import BytesIO from functools import lru_cache from xml.etree import ElementTree as ET import json,base64,html,re,zipfile,hashlib from PIL import Image,ImageDraw,ImageFont from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader ROOT=Path(__file__).resolve().parent BOOK=json.loads((ROOT/'book.json').read_text()) FONT='/System/Library/AssetsV2/com_apple_MobileAsset_Font7/8dc7805506cc9f233dcc19aabf593196842a47ae.asset/AssetData/Hannotate.ttc' SCALE=2 @lru_cache(None) def font(size):return ImageFont.truetype(FONT,size*SCALE) def wrap(text,size,width,draw): tokens=re.findall(r'GPT-6 Astra|Opus 5\.5|Sonnet 5\.5|Gemini 4\.1|Sol 6\.1|3\.1 Pro|3\.8 Flash|\d+(?:\.\d+)?%|第[一二三四五六七八九十百千万0-9]+|[A-Za-z0-9]+(?:[.%-][A-Za-z0-9]+)*|\s+|.',text) closing=',。!?;:、)》”’…';opening='(《“‘' n=len(tokens);maxw=width*SCALE;face=font(size);dp={n:(0,[])} for i in range(n-1,-1,-1): if tokens[i].isspace(): if i+1 in dp:dp[i]=dp[i+1] continue if tokens[i][0] in closing:continue segment='';best=None for j in range(i,n): segment+=tokens[j];line=segment.strip();measured=draw.textlength(line,font=face) if measured>maxw:break if not line or line[-1] in opening:continue nxt=j+1 while nxt'] for k,panel in enumerate(p['panels']): if not panel['dialogue']:continue x,y,w,h=panel['text_box'];fit=None for size in [30,29,28,27,26,25,24]: step=round(size*1.2);gap=10 groups=[(speaker,wrap(text,size,w,draw)) for speaker,text in panel['dialogue']] used=sum(len(lines)*step for _,lines in groups)+(len(groups)-1)*gap if used<=h:fit=(size,step,groups,used);break if fit is None:raise ValueError(f'Page {n} panel {k+1}: text needs more space') size,step,groups,used=fit start=y for speaker,lines in groups: color=BOOK['characters'][speaker]['color'] for line in lines: draw.text((x*SCALE,y*SCALE),line,font=font(size),fill=color,anchor='lt') svg.append(f'{html.escape(line)}') y+=step y+=gap layout.append({'page':n,'panel':k+1,'size':size,'lines':sum(len(ls) for _,ls in groups),'text_bottom':start+used,'box_bottom':start+h}) svg.append('') out=ROOT/'pages'/f'page-{n:02d}.png';im.save(out) svgpath=ROOT/'editable'/f'page-{n:02d}.svg';svgpath.write_text('\n'.join(svg)) texts=[e.text or '' for e in ET.parse(svgpath).findall('{http://www.w3.org/2000/svg}text')] expected=''.join(t for panel in p['panels'] for _,t in panel['dialogue']) assert re.sub(r'\s+','',expected)==re.sub(r'\s+','',''.join(texts)) assert not any(t[0] in ',。!?;:、)》”’…' for t in texts) return out if __name__=='__main__': import sys requested=[int(x) for x in sys.argv[1:]] pages=[p for p in BOOK['pages'] if not requested or p['number'] in requested] paths=[compose(p) for p in pages] (ROOT/'tmp/layout.json').write_text(json.dumps(layout,ensure_ascii=False,indent=2)) if not requested: out=ROOT/'output/pdf';out.mkdir(parents=True,exist_ok=True) pdf=out/'Argon讨论-第二组.pdf' c=canvas.Canvas(str(pdf),pagesize=(512,768));c.setTitle(BOOK['title']);c.setAuthor('Codex') for path in paths: b=BytesIO();Image.open(path).convert('RGB').save(b,format='JPEG',quality=96,subsampling=0);b.seek(0) c.drawImage(ImageReader(b),0,0,width=512,height=768);c.showPage() c.save() sheet=Image.new('RGB',(1840,1410),'#e9e6ed') for i,p in enumerate(paths): im=Image.open(p);im.thumbnail((440,660));sheet.paste(im,((i%4)*465,(i//4)*710)) 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/art-sha256.json').write_text(json.dumps({p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (ROOT/'art').glob('*.png')},indent=2)) print(json.dumps({'rendered':[p['number'] for p in pages],'layout':layout},ensure_ascii=False))