113 lines
6.6 KiB
Python
113 lines
6.6 KiB
Python
from pathlib import Path
|
|
from io import BytesIO
|
|
from xml.etree import ElementTree as ET
|
|
import json,html,base64,re,hashlib
|
|
from PIL import Image,ImageDraw
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib.utils import ImageReader
|
|
from compose import BOOK,ROOT,font,wrap,SCALE
|
|
|
|
# Each newly generated panel is an illustration asset. Typeset and assemble the
|
|
# assets into the comic page, rather than placing letters over its characters.
|
|
RECTS=json.loads((ROOT/'tmp/source-rects.json').read_text())
|
|
LAYOUT=[]
|
|
OVERLAY={
|
|
(1,3):(450,896,537,200),
|
|
(2,0):(40,24,944,120),(2,1):(450,530,530,200),
|
|
(4,0):(40,32,580,150),(4,1):(40,566,450,100),
|
|
(5,0):(36,28,520,74),(5,1):(614,32,376,170),(5,2):(400,555,580,150),
|
|
(6,0):(40,36,620,240),(6,1):(40,650,944,120),
|
|
(7,0):(40,36,590,250),(7,1):(36,616,314,200),(7,2):(410,620,570,200),
|
|
(8,3):(40,1120,650,210)
|
|
}
|
|
|
|
def compose(page):
|
|
n=page['number'];sourcepath=ROOT/'art'/f'page-{n:02d}.png'
|
|
source=Image.open(sourcepath).convert('RGB')
|
|
rects=RECTS[f'page-{n:02d}'];assert len(rects)==len(page['panels'])
|
|
rows=[]
|
|
for k,rect in enumerate(rects):
|
|
if not rows or abs(rows[-1][0][1][1]-rect[1])>10:rows.append([])
|
|
rows[-1].append((k,rect))
|
|
scratch=ImageDraw.Draw(Image.new('RGB',(10,10)))
|
|
rowdata=[]
|
|
for row in rows:
|
|
entries=[]
|
|
for k,rect in row:
|
|
x0,y0,x1,y1=rect;w=x1-x0;pad=24 if w>500 else 20
|
|
overlay=OVERLAY.get((n,k))
|
|
size=30 if w>800 or overlay else 28
|
|
if overlay:
|
|
tx,ty,tw,th=overlay
|
|
for size in [30,29,28,27,26,25,24]:
|
|
step=round(size*1.2)
|
|
groups=[(speaker,wrap(text,size,tw,scratch)) for speaker,text in page['panels'][k]['dialogue']]
|
|
used=sum(len(lines)*step for _,lines in groups)+max(0,len(groups)-1)*12
|
|
if used<=th:break
|
|
else:raise ValueError(f'Overlay {n}/{k+1} does not fit')
|
|
else:
|
|
step=round(size*1.2)
|
|
groups=[(speaker,wrap(text,size,w-pad*2,scratch)) for speaker,text in page['panels'][k]['dialogue']]
|
|
h=sum(len(lines)*step for _,lines in groups)+max(0,len(groups)-1)*12
|
|
textheight=h+36 if groups and not overlay else 0
|
|
entries.append((k,rect,pad,size,step,groups,textheight,overlay))
|
|
rowdata.append((entries,max(e[-2] for e in entries),max(e[1][3]-e[1][1] for e in entries)))
|
|
gap=18;margin=12
|
|
H=margin*2+sum(band+arth for _,band,arth in rowdata)+gap*(len(rowdata)-1)
|
|
pageimg=Image.new('RGB',(1024*SCALE,H*SCALE),'white');d=ImageDraw.Draw(pageimg)
|
|
data=base64.b64encode(sourcepath.read_bytes()).decode()
|
|
svg=[f'<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="{H}" viewBox="0 0 1024 {H}"><defs><image id="source" width="1024" height="1536" href="data:image/png;base64,{data}"/></defs><rect width="1024" height="{H}" fill="white"/>']
|
|
y=margin
|
|
for entries,band,arth in rowdata:
|
|
for k,rect,pad,size,step,groups,textheight,overlay in entries:
|
|
x0,y0,x1,y1=rect;w=x1-x0;h=y1-y0
|
|
# Pure page composition: the illustration pixels remain untouched.
|
|
art=source.crop(tuple(rect)).resize((w*SCALE,h*SCALE),Image.Resampling.LANCZOS)
|
|
desty=y+band
|
|
pageimg.paste(art,(x0*SCALE,desty*SCALE))
|
|
clipid=f'p{n}f{k}'
|
|
svg.append(f'<defs><clipPath id="{clipid}"><rect x="{x0}" y="{desty}" width="{w}" height="{h}"/></clipPath></defs><g clip-path="url(#{clipid})"><use href="#source" transform="translate(0 {desty-y0})"/></g>')
|
|
if overlay:
|
|
tx,oy,tw,th=overlay;ty=desty+oy-y0
|
|
else:tx=x0+pad;ty=y+14
|
|
for speaker,lines in groups:
|
|
color=BOOK['characters'][speaker]['color']
|
|
for line in lines:
|
|
d.text((tx*SCALE,ty*SCALE),line,font=font(size),fill=color,anchor='lt')
|
|
svg.append(f'<text x="{tx}" y="{ty}" dominant-baseline="text-before-edge" font-family="Hannotate SC,sans-serif" font-size="{size}" fill="{color}">{html.escape(line)}</text>')
|
|
ty+=step
|
|
ty+=12
|
|
if groups and not overlay:assert ty-12<=desty
|
|
LAYOUT.append({'page':n,'panel':k+1,'source_rect':rect,'art_rect':[x0,desty,w,h],'text_size':size if groups else None,'lines':sum(len(ls) for _,ls in groups),'page_height':H,'lettering':'inside' if overlay else 'above'})
|
|
y+=band+arth+gap
|
|
svg.append('</svg>')
|
|
path=ROOT/'pages'/f'page-{n:02d}.png';pageimg.save(path)
|
|
svgpath=ROOT/'editable'/f'page-{n:02d}.svg';svgpath.write_text('\n'.join(svg))
|
|
actual=''.join(e.text or '' for e in ET.parse(svgpath).findall('{http://www.w3.org/2000/svg}text'))
|
|
expected=''.join(text for p in page['panels'] for _,text in p['dialogue'])
|
|
assert re.sub(r'\s+','',actual)==re.sub(r'\s+','',expected)
|
|
return path
|
|
|
|
if __name__=='__main__':
|
|
import sys
|
|
nums=[int(x) for x in sys.argv[1:]]
|
|
pages=[p for p in BOOK['pages'] if not nums or p['number'] in nums]
|
|
paths=[compose(p) for p in pages]
|
|
if not nums:
|
|
out=ROOT/'output/pdf';out.mkdir(parents=True,exist_ok=True)
|
|
pdf=out/'Argon讨论-第二组.pdf'
|
|
c=canvas.Canvas(str(pdf));c.setTitle(BOOK['title']);c.setAuthor('Codex')
|
|
for path in paths:
|
|
im=Image.open(path).convert('RGB');w,h=im.size;b=BytesIO();im.save(b,format='JPEG',quality=96,subsampling=0);b.seek(0)
|
|
c.setPageSize((512,512*h/w));c.drawImage(ImageReader(b),0,0,width=512,height=512*h/w);c.showPage()
|
|
c.save()
|
|
sheet=Image.new('RGB',(1840,2100),'#e9e6ed')
|
|
for i,path in enumerate(paths):
|
|
im=Image.open(path);im.thumbnail((440,1010));sheet.paste(im,((i%4)*465,(i//4)*1050))
|
|
sheet.save(ROOT/'overview.jpg',quality=95)
|
|
reader='''<!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:#e9e6ed}main{max-width:1000px;margin:auto}img{width:100%;height:auto;display:block;margin-bottom:24px}@media print{img{margin:0;break-after:page}}</style><main>'''
|
|
reader+=''.join(f'<img src="pages/page-{i:02d}.png" alt="">' for i in range(1,9))+'</main></html>'
|
|
(ROOT/'index.html').write_text(reader)
|
|
(ROOT/'tmp/final-layout.json').write_text(json.dumps(LAYOUT,ensure_ascii=False,indent=2))
|
|
print(json.dumps({'rendered':[p['number'] for p in pages],'page_heights':sorted(set(x['page_height'] for x in LAYOUT)),'font_sizes':sorted(set(x['text_size'] for x in LAYOUT if x['text_size']))},ensure_ascii=False))
|