Moved some files around and made cleanup better
This commit is contained in:
168
spikes/rejected-compositions/tier-layer-library-proof/build.py
Normal file
168
spikes/rejected-compositions/tier-layer-library-proof/build.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Four specific retained-art proofs for review of the shared tier library."""
|
||||
from pathlib import Path
|
||||
import argparse, hashlib, json, os, subprocess, sys
|
||||
from xml.sax.saxutils import escape
|
||||
import xml.etree.ElementTree as ET
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
REPO=next(p for p in ROOT.parents if (p/'docs/card-layer-pipeline.md').is_file())
|
||||
sys.path.insert(0,str(REPO/'artifacts/layers'))
|
||||
sys.path.insert(0,str(REPO/'tools/card-production'))
|
||||
from card_workspace import resolve_revision
|
||||
from library import load_tier, digest
|
||||
from finish_masks import prepare_illustration, compile_finish, export_finish, RECIPE_PATH, RECIPE_SHA256
|
||||
OUT=ROOT/'output'
|
||||
for folder in ['cards','text','masks','editable','review','runtime']:(OUT/folder).mkdir(parents=True,exist_ok=True)
|
||||
FONT_DIR=REPO/'fonts'
|
||||
FM=json.loads((FONT_DIR/'manifest.json').read_text())
|
||||
for role in ['title','verse','reference']:
|
||||
file=FM['productionTypography'][role]['file']; assert digest(FONT_DIR/file)==FM['files'][file]['sha256']
|
||||
config=OUT/'fonts.conf'; config.write_text(f'<fontconfig><dir>{FONT_DIR}</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)],text=True,env=ENV).strip()
|
||||
def save_json(path,data):path.write_text(json.dumps(data,indent=2)+'\n')
|
||||
def svg(body):return f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800">{body}</svg>'
|
||||
def luminance(rgb):
|
||||
c=np.asarray(rgb,dtype=float)/255; c=np.where(c<=.04045,c/12.92,((c+.055)/1.055)**2.4)
|
||||
return float(c@np.array([.2126,.7152,.0722]))
|
||||
def contrast(a,b):
|
||||
lo,hi=sorted([luminance(a),luminance(b)]);return (hi+.05)/(lo+.05)
|
||||
report={'status':'pending','scope':'Four retained-art review proofs; historical outputs and harness fixtures unchanged.','sharedLayerManifestSHA256':digest(REPO/'artifacts/layers/manifest.json'),'sharedFontManifestSHA256':digest(FONT_DIR/'manifest.json'),'sharedLayerLoaderSHA256':digest(REPO/'artifacts/layers/library.py'),'builderSHA256':digest(Path(__file__)),'cardDataSHA256':digest(ROOT/'cards.json'),'finishRecipe':{'id':'raw-ridges-v1','sha256':RECIPE_SHA256,'moduleSHA256':digest(REPO/'tools/card-production/finish_masks.py')},'tools':{'inkscape':ink('--version'),'pillow':Image.__version__,'numpy':np.__version__},'cards':[],'visualReview':{'static':'pending','userTemplateApproval':'pending','movingLight':'not performed'}}
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--check-paths',action='store_true',help='Verify canonical inputs and compatibility exports without rendering.')
|
||||
parser.add_argument('--tier',choices=['common','uncommon','rare','extraordinary'],help='Rebuild one tier and refresh comparison sheets, preserving verified unchanged exports for other tiers.')
|
||||
args=parser.parse_args()
|
||||
previous=json.loads((OUT/'validation.json').read_text()) if args.tier else None
|
||||
report['rebuildScope']=args.tier or 'all'
|
||||
card_references=json.loads((ROOT/'cards.json').read_text())
|
||||
records=[json.loads((resolve_revision(ref['cardId'],ref['revision'])/'card.json').read_text()) for ref in card_references]
|
||||
parser_revision_paths=[resolve_revision(ref['cardId'],ref['revision']) for ref in card_references]
|
||||
if not args.check_paths and any(json.loads((path/'manifest.json').read_text())['stage']=='approved' for path,record in zip(parser_revision_paths,records) if not args.tier or record['tier']==args.tier):
|
||||
raise SystemExit('Approved revisions are frozen; select a working revision before rebuilding.')
|
||||
if args.check_paths:
|
||||
for reference,record,directory in zip(card_references,records,parser_revision_paths):
|
||||
assert digest(REPO/record['art'])==record['artSHA256']
|
||||
for label,width in [('low',500),('med',1000),('high',2000)]:
|
||||
for printing in ['normal','boundless','borderless','textless']:
|
||||
for old,filename in [(OUT/'cards'/f'{record['exportPrefix']}-{printing}-{width}.png','card.png'),(OUT/'masks'/f'{record['exportPrefix']}-{printing}-{width}-mask.png','finish-mask.png'),(OUT/'masks'/f'{record['exportPrefix']}-{printing}-{width}-text-mask.png','text-mask.png')]:
|
||||
assert old.resolve()==directory/label/printing/filename,(old,directory)
|
||||
assert (OUT/'editable'/f'{record['exportPrefix']}-{printing}.svg').resolve()==directory/'high'/printing/'card.svg'
|
||||
print('Four canonical card inputs and all compatibility export paths verified; no images rendered.')
|
||||
raise SystemExit(0)
|
||||
for card in records:
|
||||
tier=card['tier']; selected=load_tier(tier,card['layerVersion']); layout=selected['layout']; assets=selected['assets']; id=card['exportPrefix']
|
||||
if args.tier and tier!=args.tier:
|
||||
retained=next(entry for entry in previous['cards'] if entry['tier']==tier)
|
||||
assert retained['sharedLayers']['manifestSHA256']==selected['manifestSHA256'],f'Unselected tier changed: {tier}'
|
||||
for printing,record in retained['printings'].items():
|
||||
for export in record['exports']:
|
||||
width=export['width']
|
||||
for path,key in [(OUT/'cards'/f'{id}-{printing}-{width}.png','faceSHA256'),(OUT/'masks'/f'{id}-{printing}-{width}-mask.png','finishSHA256'),(OUT/'masks'/f'{id}-{printing}-{width}-text-mask.png','textMaskSHA256')]:
|
||||
assert digest(path)==export[key],f'Unselected export changed: {path}'
|
||||
retained['reusedUnchangedExports']=True
|
||||
report['cards'].append(retained)
|
||||
continue
|
||||
art_path=REPO/card['art']; art=Image.open(art_path).convert('RGBA');assert art.size==(2000,2800) and art.getchannel('A').getextrema()==(255,255)
|
||||
assert digest(art_path)==card['artSHA256'],'Retained art changed'
|
||||
assert card['referenceDisplay']==f'{card["reference"]} • {card["translation"]}'
|
||||
assert ' '.join(card['verseLines'])==card['excerpt']
|
||||
sizes=layout['fontSizes']; regions=layout['textRegions']; families={'title':'P052','verse':'Sanctification P052','reference':'Sanctification P052'}
|
||||
body='<g fill="#263f50" text-anchor="middle">'
|
||||
text_rows=[('title','title',card['name'],layout['baselines']['title'])]+[(f'verse-{i}','verse',line,y) for i,(line,y) in enumerate(zip(card['verseLines'],card['verseBaselines']))]+[('reference','reference',card['referenceDisplay'],layout['baselines']['reference'])]
|
||||
assert len(card['verseLines'])<=layout['maximumVerseLines']
|
||||
assert card['verseBaselines']==layout['baselines']['verseByLineCount'][str(len(card['verseLines']))]
|
||||
font_checks=[]
|
||||
for role in ['title','verse','reference']:
|
||||
face=FM['productionTypography'][role]; file=FONT_DIR/face['file']; style='Bold' if role=='title' else 'Medium'
|
||||
matched=Path(subprocess.check_output(['fc-match','--format=%{file}',f'{families[role]}:style={style}'],env=ENV,text=True)).resolve()
|
||||
assert matched==file.resolve(),(role,matched,file)
|
||||
ranges=[(int(t.split('-')[0],16),int(t.split('-')[-1],16)) for t in subprocess.check_output(['fc-query','--format=%{charset}',str(file)],text=True).split()]
|
||||
content=''.join(row[2] for row in text_rows if row[1]==role)
|
||||
assert all(any(lo<=ord(c)<=hi for lo,hi in ranges) for c in content)
|
||||
font_checks.append({'role':role,'file':str(file.relative_to(REPO)),'sha256':digest(file),'weight':face['weight'],'fallbackUsed':False,'missingGlyphs':[]})
|
||||
for key,role,content,y in text_rows:
|
||||
assert sizes[role]>=layout['fontMinimums'][role]
|
||||
body+=f'<text id="{key}" x="1000" y="{y}" font-family="{families[role]}" font-weight="{FM["productionTypography"][role]["weight"]}" font-size="{sizes[role]}">{escape(content)}</text>'
|
||||
body+='</g>'
|
||||
source=OUT/'text'/f'{id}-text.svg'; source.write_text(svg(body));text_path=OUT/'text'/f'{id}-text-2000.png'
|
||||
ink(source,'--export-type=png',f'--export-filename={text_path}','--export-width=2000','--export-height=2800')
|
||||
text=Image.open(text_path).convert('RGBA');ta=np.asarray(text)[:,:,3]
|
||||
bounds={}
|
||||
for row in ink(source,'--query-all').splitlines():
|
||||
parts=row.split(',')
|
||||
if len(parts)==5:bounds[parts[0]]=list(map(float,parts[1:]))
|
||||
glyphs=[]
|
||||
for key,role,content,y in text_rows:
|
||||
rx,ry,rw,rh=regions[role];x,yy,w,h=bounds[key]
|
||||
assert rx<=x and ry<=yy and x+w<=rx+rw and yy+h<=ry+rh,(id,key,bounds[key],regions[role])
|
||||
node=next(n for n in ET.fromstring(body) if n.get('id')==key)
|
||||
isolated=OUT/'text'/f'{id}-{key}-unclipped.svg';isolated.write_text(svg('<g text-anchor="middle" fill="#263f50">'+ET.tostring(node,encoding='unicode')+'</g>'))
|
||||
png=isolated.with_suffix('.png');ink(isolated,'--export-type=png',f'--export-filename={png}')
|
||||
bb=Image.open(png).getchannel('A').getbbox();assert bb and rx<=bb[0] and ry<=bb[1] and bb[2]<=rx+rw and bb[3]<=ry+rh
|
||||
glyphs.append({'id':key,'objectBounds':bounds[key],'unclippedInkBounds':bb,'region':regions[role],'pass':True})
|
||||
protected=text.getchannel('A').filter(ImageFilter.MaxFilter(25)).filter(ImageFilter.GaussianBlur(8))
|
||||
analysis=prepare_illustration(art)
|
||||
entry={'id':id,'tier':tier,'sharedLayers':{'version':selected['version'],'manifestSHA256':selected['manifestSHA256'],'assets':{k:{'path':str(v.relative_to(REPO)),'sha256':digest(v)} for k,v in assets.items()}},'art':{'path':card['art'],'sha256':digest(art_path),'pixelsUnchanged':True},'typography':{'fonts':font_checks,'glyphs':glyphs},'printings':{}}
|
||||
for printing,overlay_key,has_text in [('normal','overlay-normal',True),('textless','frame-textless',False),('borderless','backing-borderless',True),('boundless',None,False)]:
|
||||
overlay=Image.open(assets[overlay_key]).convert('RGBA') if overlay_key else None
|
||||
face=art.copy()
|
||||
if overlay:face=Image.alpha_composite(face,overlay)
|
||||
if has_text:
|
||||
assert np.all(np.asarray(overlay)[:,:,3][ta>0]==255),'Text outside opaque backing'
|
||||
face=Image.alpha_composite(face,text)
|
||||
assert face.getchannel('A').getextrema()==(255,255)
|
||||
if printing=='boundless':assert np.array_equal(np.asarray(face),np.asarray(art))
|
||||
finishes=export_finish(compile_finish(analysis,printing,overlay,text if has_text else None),OUT/'masks',id,printing)
|
||||
files=[]
|
||||
for width in [2000,1000,500]:
|
||||
target=OUT/'cards'/f'{id}-{printing}-{width}.png';im=face if width==2000 else face.resize((width,width*7//5),Image.Resampling.LANCZOS);im.save(target)
|
||||
mask_path=OUT/'masks'/f'{id}-{printing}-{width}-text-mask.png'
|
||||
m=protected if has_text else Image.new('L',(2000,2800),0)
|
||||
if width!=2000:m=m.resize(im.size,Image.Resampling.BILINEAR)
|
||||
rgba=m.convert('RGBA');rgba.putalpha(255);rgba.save(mask_path)
|
||||
fm_path=OUT/'masks'/f'{id}-{printing}-{width}-mask.png'; fm_image=Image.open(fm_path).convert('RGBA');arr=np.asarray(fm_image)
|
||||
assert fm_image.size==im.size and np.all(arr[:,:,3]==255) and np.array_equal(arr[:,:,0],arr[:,:,1]) and np.array_equal(arr[:,:,1],arr[:,:,2])
|
||||
if width==1000:
|
||||
for path in [target,fm_path]+([mask_path] if has_text else []):(OUT/'runtime'/path.name).write_bytes(path.read_bytes())
|
||||
files.append({'width':width,'faceSHA256':digest(target),'finishSHA256':digest(fm_path),'textMaskSHA256':digest(mask_path),'opaque':True})
|
||||
# Linked shared layers remain editable centrally; lettering remains real text objects.
|
||||
editable=(OUT/'editable'/f'{id}-{printing}.svg').resolve()
|
||||
def link(p):return f'<image width="2000" height="2800" xlink:href="{escape(os.path.relpath(p,editable.parent))}"/>'
|
||||
editable.write_text(svg(link(art_path)+(link(assets[overlay_key]) if overlay_key else '')+(body if has_text else '')))
|
||||
entry['printings'][printing]={'exports':files,'finishProvenance':finishes,'overlay':overlay_key,'text':has_text}
|
||||
if tier!='common':
|
||||
contrasts=[]
|
||||
for width in [2000,1000,500]:
|
||||
scale=width/2000
|
||||
b=np.asarray(Image.open(selected['directory']/'exports'/f'backing-borderless-{width}.png').convert('RGBA'))
|
||||
xx=int((layout['borderlessBacking'][0][0]+layout['primaryRuleInset'])*scale); yy=int(180*scale)
|
||||
sample=b[yy,max(0,xx-1):xx+2,:3]; bg=b[yy,int(500*scale),:3]
|
||||
ratio=max(contrast(c,bg) for c in sample)
|
||||
assert ratio>=3,(id,width,ratio)
|
||||
contrasts.append({'width':width,'primaryRuleToCream':round(ratio,2),'minimum':3})
|
||||
entry['backingContrast']=contrasts
|
||||
entry['textContrast']=round(contrast([38,63,80],[244,234,212]),2);assert entry['textContrast']>=7
|
||||
report['cards'].append(entry);print(f'{tier}: all printings and text checks passed',flush=True)
|
||||
report['status']='passed';save_json(OUT/'validation.json',report)
|
||||
# Comparison sheets use identical scale and neutral backdrops, with labels outside assets.
|
||||
FONT=ImageFont.truetype(str(FONT_DIR/'P052-Bold.otf'),24)
|
||||
def sheet(rows,path):
|
||||
cellw,cellh=340,510; im=Image.new('RGB',(4*cellw,len(rows)*cellh),(14,18,22));draw=ImageDraw.Draw(im)
|
||||
for row,(label,paths,checker) in enumerate(rows):
|
||||
for col,(tier,p) in enumerate(zip([c['tier'] for c in records],paths)):
|
||||
x=col*cellw+20;y=row*cellh+50;draw.text((x,row*cellh+12),f'{tier.title()} · {label}',font=FONT,fill='#eee6d5')
|
||||
bg=Image.new('RGBA',(300,420),(40,46,52,255))
|
||||
if checker:
|
||||
d=ImageDraw.Draw(bg)
|
||||
for yy in range(0,420,20):
|
||||
for xx in range(0,300,20):
|
||||
if (xx//20+yy//20)%2:d.rectangle([xx,yy,xx+19,yy+19],fill=(61,66,72,255))
|
||||
layer=Image.open(p).convert('RGBA').resize((300,420),Image.Resampling.LANCZOS);bg=Image.alpha_composite(bg,layer);im.paste(bg.convert('RGB'),(x,y))
|
||||
im.save(path)
|
||||
new=[load_tier(c['tier'],c['layerVersion']) for c in records];old=[load_tier(c['tier'],'v1') for c in records]
|
||||
sheet([(label,[s['assets'][key] for s in new],True) for label,key in [('Normal','overlay-normal'),('Borderless','backing-borderless'),('Textless','frame-textless')]],OUT/'review/tier-layers.png')
|
||||
sheet([('Before',[REPO/c['previousNormal'] for c in records],False),('After',[OUT/'cards'/f'{c["exportPrefix"]}-normal-1000.png' for c in records],False)],OUT/'review/cards-before-after.png')
|
||||
sheet([('Before',[s['assets']['overlay-normal'] for s in old],True),('After',[s['assets']['overlay-normal'] for s in new],True)],OUT/'review/layers-before-after.png')
|
||||
print(OUT/'review')
|
||||
Reference in New Issue
Block a user