Import project sources
This commit is contained in:
+90
@@ -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))
|
||||
Reference in New Issue
Block a user