from pathlib import Path import json, base64, html, textwrap from io import BytesIO 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' SANS='/System/Library/Fonts/Hiragino Sans GB.ttc' W,H=1024,1696 TOP=112 SCALE=2 def f(size,hand=True): return ImageFont.truetype(FONT if hand else SANS,size*SCALE) def wrap(t,font,maxw,draw): out=[] for raw in t.split('\n'): line='' for c in raw: if line and draw.textlength(line+c,font=font)>maxw: out.append(line); line=c else: line+=c out.append(line) return out def compose(p): num=p['number'] artpath=ROOT/'art'/f'page-{num:02d}-final.png' art=Image.open(artpath).convert('RGB').resize((1024*SCALE,1536*SCALE),Image.Resampling.LANCZOS) page=Image.new('RGB',(W*SCALE,H*SCALE),'white'); page.paste(art,(0,TOP*SCALE)) d=ImageDraw.Draw(page) svg=[f'', f''] def txt(x,y,t,size=32,fill='#171719',hand=True): d.text((x*SCALE,y*SCALE),t,font=f(size,hand),fill=fill,anchor='lt') family='Hannotate SC' if hand else 'Hiragino Sans GB' svg.append(f'{html.escape(t)}') def line(coords,fill='#171719',width=2): d.line(tuple(int(v*SCALE) for v in coords),fill=fill,width=width*SCALE) svg.append(f'') txt(24,14,BOOK['title'],22,fill='#727078') txt(24,46,p['title'],44) txt(896,54,f'{num:02d} / 07',22,fill='#727078',hand=False) for j,utterances in enumerate(p['panels']): col,row=j%2,j//2 x=col*512+38 compact=(num in (2,6) and j==2) y=TOP+row*768+(16 if compact else 32) textsize=28 if compact else 31 textfont=f(textsize) for k,(speaker,words) in enumerate(utterances): color=BOOK['characters'][speaker]['color'] txt(x,y,speaker,17 if compact else 19,fill=color) y+=22 if compact else 27 lines=wrap(words,textfont,438*SCALE,d) for t in lines: txt(x,y,t,textsize); y+=33 if compact else 39 y+=8 # Speaker names keep freely positioned dialogue unambiguous. if y>TOP+row*768+238: raise ValueError(f'Text overflows reserved art region: page {num}, panel {j+1}, y={y}') footer='根据两帖评论改编 · 虚构合成人物 · 中文对白后期排版' if num==5: footer='额度数字为帖内个别用户自述,不代表所有账号或统一规则。' txt(24,1665,footer,17,fill='#6F6B75',hand=False) svg.append('') out=ROOT/'pages'/f'page-{num:02d}.png'; page.save(out) (ROOT/'editable'/f'page-{num:02d}.svg').write_text('\n'.join(svg)) return out paths=[compose(p) for p in BOOK['pages']] pdfpath=ROOT/'output/pdf/最强模型茶话会.pdf' c=canvas.Canvas(str(pdfpath),pagesize=(512,848)) c.setTitle(BOOK['title']); c.setAuthor('Codex · Reddit讨论改编') for p in paths: encoded=BytesIO() Image.open(p).convert('RGB').save(encoded,format='JPEG',quality=96,subsampling=0) encoded.seek(0) c.drawImage(ImageReader(encoded),0,0,width=512,height=848); c.showPage() c.save() # A contact sheet for visual inspection; page images remain full resolution. sheet=Image.new('RGB',(1024,1696),'#E9E7ED') for i,p in enumerate(paths): im=Image.open(p); im.thumbnail((326,540)) x=(i%3)*342+(326-im.width)//2; y=(i//3)*564 sheet.paste(im,(x,y)) sheet.save(ROOT/'overview.jpg',quality=92) index='''最强模型茶话会

最强模型茶话会

7页 · 6位虚构朋友 · 根据两帖公开讨论重新改编。评论为个人体验,非产品实测。

PDF阅读版 · 改编说明 · 对白源文件

''' index+=''.join(f'第{i}页:{BOOK[' for i in range(1,8))+'
' (ROOT/'index.html').write_text(index) print(json.dumps({'pages':len(paths),'pdf':str(pdfpath),'size':pdfpath.stat().st_size,'text_overflow':False},ensure_ascii=False))