Import project sources

This commit is contained in:
2026-11-05 01:04:53 +08:00
commit 76de825c03
41 changed files with 2498 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
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))