Files
argon-comic/compose.py
2026-11-05 01:04:53 +08:00

91 lines
5.3 KiB
Python

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'<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1696" viewBox="0 0 1024 1696"><rect width="1024" height="1696" fill="white"/>',
f'<image x="0" y="{TOP}" width="1024" height="1536" href="data:image/png;base64,{base64.b64encode(artpath.read_bytes()).decode()}"/>']
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'<text x="{x}" y="{y}" dominant-baseline="text-before-edge" font-family="{family},sans-serif" font-size="{size}" fill="{fill}">{html.escape(t)}</text>')
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'<path d="M{coords[0]},{coords[1]} L{coords[2]},{coords[3]}" stroke="{fill}" stroke-width="{width}" fill="none" stroke-linecap="round"/>')
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('</svg>')
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='''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>最强模型茶话会</title><style>body{margin:0;background:#eae7ee;font-family:system-ui;color:#25212b}header{max-width:900px;margin:30px auto;padding:0 16px}h1{font-size:28px}main{max-width:920px;margin:auto}img{display:block;width:100%;height:auto;margin:24px 0;box-shadow:0 5px 30px #211c3220}a{color:inherit}p{line-height:1.7}@media print{header{display:none}img{margin:0;box-shadow:none;break-after:page}}</style><header><h1>最强模型茶话会</h1><p>7页 · 6位虚构朋友 · 根据两帖公开讨论重新改编。评论为个人体验,非产品实测。</p><p><a href="output/pdf/最强模型茶话会.pdf">PDF阅读版</a> · <a href="README.md">改编说明</a> · <a href="book.json">对白源文件</a></p></header><main>'''
index+=''.join(f'<img src="pages/page-{i:02d}.png" alt="第{i}页:{BOOK["pages"][i-1]["title"]}">' for i in range(1,8))+'</main></html>'
(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))