91 lines
5.3 KiB
Python
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))
|