Files
Sanctification/artifacts/layers/spacing_v3.py

54 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Create the approved 20px central-flourish revision without changing v2."""
from pathlib import Path
import json,re,shutil,subprocess,sys,xml.etree.ElementTree as ET
import numpy as np
from PIL import Image
ROOT=Path(__file__).resolve().parent;sys.path.insert(0,str(ROOT))
from library import load_tier,digest
from build import rebuild
NS='{http://www.w3.org/2000/svg}';ET.register_namespace('','http://www.w3.org/2000/svg');ET.register_namespace('xlink','http://www.w3.org/1999/xlink')
def save(p,v):p.write_text(json.dumps(v,indent=2)+'\n')
def query(p):
rows=subprocess.check_output(['inkscape',str(p),'--query-all'],text=True,stderr=subprocess.PIPE)
return {a[0]:list(map(float,a[1:])) for row in rows.splitlines() if len(a:=row.split(','))==5}
def transform(src,dst):
tree=ET.parse(src);page=tree.getroot();ornaments=[]
for node in page.iter(NS+'g'):
ident=node.get('id','');t=node.get('transform','')
if ident.startswith('ornament-') or re.match(r'translate\(1000,',t):
y=float(ident.split('-')[1]) if ident.startswith('ornament-') else float(re.search(r'translate\(1000,([\d.]+)',t).group(1));ornaments.append((y,node))
ornaments.sort(key=lambda item:item[0]);assert len(ornaments)==3
for (_,node),ident in zip(ornaments,['header','footer-top','footer-bottom']):node.set('id',ident)
tree.write(dst,encoding='unicode');old=query(dst)
rects=[n for n in page.iter(NS+'rect') if n.get('fill')=='none' and n.get('stroke')]
header=max((n for n in rects if float(n.get('y'))<400),key=lambda n:float(n.get('y')));footer=max((n for n in rects if float(n.get('y'))>2000),key=lambda n:float(n.get('y')))
def edge(n,top):return float(n.get('y'))+(float(n.get('stroke-width'))/2 if top else float(n.get('height'))-float(n.get('stroke-width'))/2)
edges={'header':edge(header,False),'footer-top':edge(footer,True),'footer-bottom':edge(footer,False)};changes=[]
for (_,node),ident in zip(ornaments,['header','footer-top','footer-bottom']):
x,y,w,h=old[ident];before=y if ident=='footer-top' else y+h;target=edges[ident]+20 if ident=='footer-top' else edges[ident]-20;delta=target-before
node.set('transform',f'translate(0,{delta:.8f}) '+node.get('transform',''));changes.append({'id':ident,'innerRuleInkEdge':edges[ident],'verticalShift':delta})
tree.write(dst,encoding='unicode');new=query(dst)
for item in changes:
ident=item['id'];x,y,w,h=new[ident];g=y-edges[ident] if ident=='footer-top' else edges[ident]-(y+h);assert abs(g-20)<.02;item['gap']=g;item['flourishVisualBounds']=new[ident]
return {'sourceSHA256':digest(src),'flourishes':changes,'headerInnerTop':edge(header,True)}
def main():
index=json.loads((ROOT/'manifest.json').read_text())
for tier in ['uncommon','rare','extraordinary']:
old=load_tier(tier,'v2');dest=ROOT/tier/'v3';assert not dest.exists(),f'Already created: {dest}'
dest.mkdir();(dest/'exports').mkdir();shutil.copytree(old['directory']/'source',dest/'source')
checks={kind:transform(old['directory']/'source'/f'backing-{kind}.svg',dest/'source'/f'backing-{kind}.svg') for kind in ['normal','borderless']}
layout=dict(old['layout']);layout.update({'version':'v3','reviewStatus':'approved-spacing-static-printing-review-pending','spacing':{'flourishToInnerRule':20,'referenceLineBoxToLowerFlourish':20,'referenceMetricsPolicy':'ceil hhea ascent; ceil max(hhea descent, global outline descent); no line gap','verseCentering':'combined visible ink between upper flourish bottom and fixed reference line-box top','titleClearance':20,'geometry':checks}})
layout['textRegions']=dict(layout['textRegions']);layout['textRegions']['reference']=[160,2580,1680,100]
save(dest/'layout.json',layout);save(dest/'provenance.json',{'baseTier':tier,'baseVersion':'v2','baseManifestSHA256':old['manifestSHA256'],'approval':{'date':'2026-09-14','wording':'I think this looks really good.','scope':'Central flourish spacing and fixed reference baseline/verse centering static comparison.'},'changes':checks,'preserved':'Frame, panel boundaries, palette, textures, inset rules and side/corner details','builderSHA256':digest(Path(__file__))})
result=rebuild(tier,'v3',True)
for name in ['frame-normal','frame-textless']:
for width in [2000,1000,500]:assert np.array_equal(np.asarray(Image.open(dest/'exports'/f'{name}-{width}.png').convert('RGBA')),np.asarray(Image.open(old['directory']/'exports'/f'{name}-{width}.png').convert('RGBA'))),(tier,name,width)
# Borderless master must reproduce the accepted trial's decoded pixels.
trial=ROOT.parents[1]/'spikes/flourish-spacing-20px-proof/output-retyped'/tier/'backing-borderless-20.png'
assert np.array_equal(np.asarray(Image.open(trial).convert('RGBA')),np.asarray(Image.open(dest/'exports/backing-borderless-2000.png').convert('RGBA'))),tier
index['tiers'][tier]['versions'].append('v3');index['tiers'][tier]['v3ReviewStatus']=layout['reviewStatus']
save(ROOT/'manifest.json',index)
print('v3 registered for explicit card pinning; existing selectedVersion defaults unchanged.')
if __name__=='__main__':main()