57 lines
6.4 KiB
Python
57 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Center each verse's visible ink between the upper flourish and reference ink."""
|
|
from pathlib import Path
|
|
import copy,hashlib,json,os,subprocess
|
|
import xml.etree.ElementTree as ET
|
|
import numpy as np
|
|
from PIL import Image,ImageDraw,ImageFont
|
|
ROOT=Path(__file__).resolve().parent
|
|
REPO=next(p for p in ROOT.parents if (p/'docs/card-layer-pipeline.md').is_file())
|
|
BASE=ROOT/'output-retyped';OUT=ROOT/'output-centered';OUT.mkdir(exist_ok=True)
|
|
NS='{http://www.w3.org/2000/svg}';ET.register_namespace('','http://www.w3.org/2000/svg')
|
|
fonts=REPO/'fonts';fm=json.loads((fonts/'manifest.json').read_text())
|
|
def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
for name in ['P052-Bold.otf','SanctificationP052-Medium.otf']:assert digest(fonts/name)==fm['files'][name]['sha256']
|
|
config=OUT/'fonts.conf';config.write_text(f'<fontconfig><dir>{fonts}</dir><cachedir>{OUT/"font-cache"}</cachedir></fontconfig>');env=dict(os.environ,FONTCONFIG_FILE=str(config))
|
|
def ink(*args):return subprocess.check_output(['inkscape',*map(str,args)],env=env,text=True,stderr=subprocess.PIPE).strip()
|
|
def render(source,target,*options):ink(source,*options,'--export-type=png',f'--export-filename={target}','--export-width=2000','--export-height=2800')
|
|
roots={'uncommon':REPO/'in-progress/cards/BE-042-calling-of-the-first-disciples/revisions/v02','rare':REPO/'artifacts/cards/BE-010-burning-bush','extraordinary':REPO/'artifacts/cards/BE-002-the-fall'}
|
|
report={'status':'pending','scope':'Separate static comparison; verse baselines only change.','centeringRule':'Center the combined unclipped verse ink bounds between the bottom visible edge of the upper flourish and top visible edge of the reference. No prescribed margin participates in placement.','minimumClearance':20,'clearanceRole':'Validation only; never used to offset the center.','tiers':[]}
|
|
for tier,root in roots.items():
|
|
folder=OUT/tier;folder.mkdir(exist_ok=True);card=json.loads((root/'card.json').read_text())
|
|
ornament_png=folder/'upper-flourish-guide.png';render(BASE/tier/'backing-borderless-20.svg',ornament_png,'--export-id=footer-top','--export-id-only','--export-area-page')
|
|
ornament_bounds=Image.open(ornament_png).getchannel('A').getbbox();reference_bounds=Image.open(BASE/tier/'reference-unclipped.png').getchannel('A').getbbox()
|
|
tree=ET.parse(BASE/tier/'text-preview.svg');verses=[node for node in tree.getroot().iter(NS+'text') if node.get('id','').startswith('verse-')]
|
|
assert verses
|
|
old_boxes=[Image.open(BASE/tier/(node.get('id')+'-unclipped.png')).getchannel('A').getbbox() for node in verses]
|
|
old_top=min(box[1] for box in old_boxes);old_bottom=max(box[3] for box in old_boxes)
|
|
upper=ornament_bounds[3];lower=reference_bounds[1];target_center=(upper+lower)/2;delta=target_center-(old_top+old_bottom)/2
|
|
for node in verses:node.set('y',str(float(node.get('y'))+delta))
|
|
source=folder/'text-centered.svg';tree.write(source,encoding='unicode');png=folder/'text-centered.png';render(source,png);text=Image.open(png).convert('RGBA')
|
|
glyphs=[]
|
|
for node in verses:
|
|
page=ET.Element(NS+'svg',{'width':'2000','height':'2800','viewBox':'0 0 2000 2800'});g=ET.SubElement(page,NS+'g',{'fill':'#263f50','text-anchor':'middle'});g.append(copy.deepcopy(node))
|
|
isolated=folder/(node.get('id')+'-unclipped.svg');ET.ElementTree(page).write(isolated,encoding='unicode');im=isolated.with_suffix('.png');render(isolated,im);box=Image.open(im).getchannel('A').getbbox()
|
|
assert box and 160<=box[0] and 2260<=box[1] and box[2]<=1840 and box[3]<=2560,(tier,box)
|
|
glyphs.append({'id':node.get('id'),'baseline':float(node.get('y')),'unclippedInkBounds':list(box)})
|
|
new_top=min(g['unclippedInkBounds'][1] for g in glyphs);new_bottom=max(g['unclippedInkBounds'][3] for g in glyphs)
|
|
above=new_top-upper;below=lower-new_bottom;assert min(above,below)>=20,(tier,above,below);assert abs(above-below)<=1,(tier,above,below)
|
|
oldtext=Image.open(BASE/tier/'text-preview.png').convert('RGBA');old=np.asarray(oldtext);new=np.asarray(text)
|
|
# Header and attribution remain bit-for-bit identical; only verse rows move.
|
|
assert np.array_equal(old[:2260],new[:2260]) and np.array_equal(old[2560:],new[2560:]),tier
|
|
art=Image.open(REPO/card['art']).convert('RGBA');backing=Image.open(BASE/tier/'backing-borderless-20.png').convert('RGBA')
|
|
assert np.all(np.asarray(backing.getchannel('A'))[new[:,:,3]>0]==255)
|
|
face=Image.alpha_composite(Image.alpha_composite(art,backing),text)
|
|
for width in [2000,1000,500]:face.resize((width,width*7//5),Image.Resampling.LANCZOS).save(folder/f'centered-borderless-{width}.png')
|
|
report['tiers'].append({'tier':tier,'cardId':card['cardId'],'upperFlourishInkBounds':list(ornament_bounds),'referenceInkBounds':list(reference_bounds),'availableBand':[upper,lower],'targetCenter':target_center,'verseCenter':(new_top+new_bottom)/2,'verseVerticalShift':delta,'gapAboveVerse':above,'gapBelowVerse':below,'glyphs':glyphs,'backingSHA256':digest(BASE/tier/'backing-borderless-20.png'),'previousTextSHA256':digest(BASE/tier/'text-preview.png'),'newTextSHA256':digest(png)})
|
|
label=ImageFont.truetype(str(fonts/'P052-Bold.otf'),23)
|
|
board=Image.new('RGB',(1740,490),'#11161c');d=ImageDraw.Draw(board)
|
|
lineup=Image.new('RGB',(1560,770),'#11161c');ld=ImageDraw.Draw(lineup)
|
|
for col,tier in enumerate(roots):
|
|
for row,(name,parent) in enumerate([('Previous retyped',BASE),('Verse ink centered',OUT)]):
|
|
path=parent/tier/('spacing-20-borderless-2000.png' if parent==BASE else 'centered-borderless-2000.png');im=Image.open(path).convert('RGB').crop((70,2165,1930,2755));im.thumbnail((560,197));x=col*580+10;y=row*245;d.text((x,y+8),tier.title()+' · '+name,font=label,fill='#f4ead4');board.paste(im,(x,y+42))
|
|
x=col*520+10;ld.text((x,10),tier.title()+' · centered',font=label,fill='#f4ead4');lineup.paste(Image.open(OUT/tier/'centered-borderless-500.png').convert('RGB'),(x,50))
|
|
board.save(OUT/'footers-comparison.png');lineup.save(OUT/'cards-centered.png')
|
|
report.update({'status':'passed-preview','builderSHA256':digest(Path(__file__)),'sharedFontManifestSHA256':digest(fonts/'manifest.json'),'tools':{'inkscape':ink('--version'),'pillow':Image.__version__},'visualReview':'Pending static inspection; no moving-light review.'});(OUT/'validation.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
print(json.dumps([{'tier':e['tier'],'verticalShift':e['verseVerticalShift'],'gapAbove':e['gapAboveVerse'],'gapBelow':e['gapBelowVerse']} for e in report['tiers']],indent=2))
|