Moved some files around and made cleanup better

This commit is contained in:
2026-09-14 19:01:08 -07:00
parent 50b234104a
commit 97ed7f05c8
866 changed files with 404 additions and 1612 deletions

View File

@@ -0,0 +1,56 @@
#!/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))