54 lines
4.9 KiB
Python
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()
|