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
+108
View File
@@ -0,0 +1,108 @@
from pathlib import Path
from io import BytesIO
import json, base64, html, re, hashlib
from PIL import Image, ImageDraw, ImageFont
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
ROOT=Path(__file__).resolve().parent
OUT=ROOT/'v2'
BOOK=json.loads((ROOT/'book-v2.json').read_text())
FONT='/System/Library/AssetsV2/com_apple_MobileAsset_Font7/8dc7805506cc9f233dcc19aabf593196842a47ae.asset/AssetData/Hannotate.ttc'
SCALE=2
W,H=1024,1536
for folder in ['pages','editable','qa']:(OUT/folder).mkdir(parents=True,exist_ok=True)
def font(size):return ImageFont.truetype(FONT,size*SCALE)
def wrap(text,size,width,draw):
tokens=re.findall(r'GPT-6 Astra|Opus 5\.5|Gemini 4\.1|3\.1 Pro|[A-Za-z0-9]+(?:[.%-][A-Za-z0-9]+)*|\s+|.',text)
closing=',。!?;:、)》”’…'
opening='(《“‘'
n=len(tokens);maxw=width*SCALE;face=font(size)
dp={n:(0,[])}
for i in range(n-1,-1,-1):
if tokens[i].isspace():
if i+1 in dp:dp[i]=dp[i+1]
continue
if tokens[i][0] in closing:continue
segment='';best=None
for j in range(i,n):
segment+=tokens[j];line=segment.strip()
measured=draw.textlength(line,font=face)
if measured>maxw:break
if not line or line[-1] in opening:continue
nxt=j+1
while nxt<n and tokens[nxt].isspace():nxt+=1
if nxt<n and tokens[nxt][0] in closing:continue
if nxt not in dp:continue
cost=1000000+100*((maxw-measured)/maxw)**2+dp[nxt][0]
candidate=(cost,[line]+dp[nxt][1])
if best is None or cost<best[0]:best=candidate
if best is not None:dp[i]=best
if 0 not in dp:raise ValueError('Cannot wrap dialogue')
return dp[0][1]
layout=[]
def compose(page):
n=page['number'];artnum=page['art']
artpath=ROOT/'art'/f'page-{artnum:02d}-final.png'
art=Image.open(artpath).convert('RGB').resize((W*SCALE,H*SCALE),Image.Resampling.LANCZOS)
draw=ImageDraw.Draw(art)
data=base64.b64encode(artpath.read_bytes()).decode()
svg=[f'<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1536" viewBox="0 0 1024 1536"><image width="1024" height="1536" href="data:image/png;base64,{data}"/>']
for k,utterances in enumerate(page['panels']):
col,row=k%2,k//2
x=col*512+34
top=18 if (artnum==2 and k>=2) or (artnum==6 and k==2) else 24
bottom=page['bottoms'][k]
fit=None
for size in [27,26,25,24,23]:
leading=round(size*1.2)
gap=9
groups=[(speaker,wrap(words,size,438,draw)) for speaker,words in utterances]
height=sum(len(lines)*leading for speaker,lines in groups)+(len(groups)-1)*gap
if top+height<=bottom:
fit=(size,leading,gap,groups,height);break
if fit is None:
raise ValueError(f'Page {n}, panel {k+1}: rewrite required, available={bottom-top}px')
size,leading,gap,groups,height=fit
y=row*768+top
for speaker,lines in groups:
color=BOOK['characters'][speaker]['color']
for line in lines:
draw.text((x*SCALE,y*SCALE),line,font=font(size),fill=color,anchor='lt')
svg.append(f'<text x="{x}" y="{y}" dominant-baseline="text-before-edge" font-family="Hannotate SC,sans-serif" font-size="{size}" fill="{color}">{html.escape(line)}</text>')
y+=leading
y+=gap
layout.append({'page':n,'art':artnum,'panel':k+1,'font':size,'lines':sum(len(ls) for _,ls in groups),'bottom':top+height,'limit':bottom})
svg.append('</svg>')
path=OUT/'pages'/f'page-{n:02d}.png'
art.save(path)
(OUT/'editable'/f'page-{n:02d}.svg').write_text('\n'.join(svg))
return path
paths=[compose(p) for p in BOOK['pages']]
pdfpath=OUT/'最强模型茶话会-对白重写版.pdf'
c=canvas.Canvas(str(pdfpath),pagesize=(512,768))
c.setTitle(BOOK['title']+' · 对白重写版');c.setAuthor('Codex')
for path in paths:
buf=BytesIO()
Image.open(path).convert('RGB').save(buf,format='JPEG',quality=96,subsampling=0)
buf.seek(0)
c.drawImage(ImageReader(buf),0,0,width=512,height=768);c.showPage()
c.save()
(OUT/'qa/layout.json').write_text(json.dumps(layout,ensure_ascii=False,indent=2))
# No headings, names, page numbers, explanatory notes, or legends in the reader.
reader='''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>漫画</title><style>html,body{margin:0;background:#e9e6ed}main{max-width:1000px;margin:auto}img{width:100%;height:auto;display:block;margin:0 0 20px}@media print{body{background:white}img{margin:0;break-after:page}}</style><main>'''
reader+=''.join(f'<img src="pages/page-{i:02d}.png" alt="">' for i in range(1,8))+'</main></html>'
(OUT/'index.html').write_text(reader)
sheet=Image.new('RGB',(1400,2110),'#E9E6ED')
for i,p in enumerate(paths):
im=Image.open(p);im.thumbnail((450,675))
sheet.paste(im,((i%3)*475,(i//3)*710))
sheet.save(OUT/'overview.jpg',quality=94)
old=json.loads((OUT/'art-sha256-before.json').read_text())
now={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (ROOT/'art').glob('*-final.png')}
assert old==now,'Original art changed'
print(json.dumps({'pages':len(paths),'art_unchanged':True,'font_min':min(x['font'] for x in layout),'font_max':max(x['font'] for x in layout),'layout':layout,'pdf_mb':round(pdfpath.stat().st_size/1e6,2)},ensure_ascii=False))