#!/usr/bin/env python3 """Rebuild shared templates, or check their decoded-pixel repeatability.""" from pathlib import Path import argparse, json, subprocess, tempfile import numpy as np from PIL import Image from library import ROOT, digest, load_tier NAMES=['frame-normal','frame-textless','backing-normal','backing-borderless','overlay-normal'] def rebuild(tier,version,refresh=False): directory=ROOT/tier/version layout=json.loads((directory/'layout.json').read_text()) if not refresh: load_tier(tier,version) with tempfile.TemporaryDirectory(prefix='sanctification-tier-') as tmp: temporary=Path(tmp) for name in NAMES[:-1]: target=temporary/f'{name}-2000.png' if version=='v1': Image.open(directory/'exports'/target.name).save(target) else: source=directory/'source'/f'{name}.svg' subprocess.run(['inkscape',str(source),'--export-type=png',f'--export-filename={target}','--export-width=2000','--export-height=2800'],check=True,stdout=subprocess.DEVNULL) if version=='v1': Image.open(directory/'exports/overlay-normal-2000.png').save(temporary/'overlay-normal-2000.png') else: back=Image.open(temporary/'backing-normal-2000.png').convert('RGBA') Image.alpha_composite(back,Image.open(temporary/'frame-normal-2000.png').convert('RGBA')).save(temporary/'overlay-normal-2000.png') for name in NAMES: master=Image.open(temporary/f'{name}-2000.png').convert('RGBA') for width in [1000,500]: master.resize((width,width*7//5),Image.Resampling.LANCZOS).save(temporary/f'{name}-{width}.png') for width in [2000,1000,500]: p=temporary/f'{name}-{width}.png' if refresh: (directory/'exports'/p.name).write_bytes(p.read_bytes()) else: expected=Image.open(directory/'exports'/p.name).convert('RGBA') assert np.array_equal(np.asarray(expected),np.asarray(Image.open(p).convert('RGBA'))),f'Non-repeatable {tier}/{version}/{p.name}' assets={name:f'exports/{name}-2000.png' for name in NAMES} if refresh: files={str(p.relative_to(directory)):{'sha256':digest(p)} for p in sorted(directory.rglob('*')) if p.is_file() and p.name!='manifest.json'} (directory/'manifest.json').write_text(json.dumps({'schemaVersion':1,'tier':tier,'version':version,'canvas':[2000,2800],'reviewStatus':layout['reviewStatus'],'assets':assets,'files':files},indent=2)+'\n') checks=[] for width in [2000,1000,500]: for name in ['frame-normal','frame-textless','overlay-normal']: a=np.asarray(Image.open(directory/'exports'/f'{name}-{width}.png').convert('RGBA'))[:,:,3] assert all(a[y,x]==255 for x,y in [(0,0),(width-1,0),(0,len(a)-1),(width-1,len(a)-1)]) checks.append({'width':width,'opaqueCorners':True}) a=np.asarray(Image.open(directory/'exports/overlay-normal-2000.png').convert('RGBA'))[:,:,3] for x,y,w,h in layout['normalBacking']: assert np.all(a[y+10:y+h-10,x+10:x+w-10]==255),f'Panel holes in {tier}' assert not np.any(a[430:2100,160:1840]),f'Art window obstruction in {tier}' if layout['normalRelationship']=='floating': band=layout['perimeterWidth']; x=layout['normalBacking'][0][0] assert not np.any(a[220:280,band+3:x-3]),f'Lost intentional reveal in {tier}' result={'tier':tier,'version':version,'status':'passed','repeatability':'decoded pixels checked' if not refresh else 'initial exports created','checks':checks,'geometry':{'opaquePanels':True,'clearFocalWindow':True,'relationship':layout['normalRelationship']}} print(json.dumps(result)) return result if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--refresh',action='store_true',help='Write exports/manifests for an explicitly revised template; use a new version directory after approval.') parser.add_argument('--tier',choices=['common','uncommon','rare','extraordinary']) parser.add_argument('--version',default='v2') args=parser.parse_args() results=[rebuild(tier,args.version,args.refresh) for tier in ([args.tier] if args.tier else ['common','uncommon','rare','extraordinary'])] if not args.refresh: (ROOT/'repeatability.json').write_text(json.dumps({'status':'passed','templates':results},indent=2)+'\n') if args.refresh: (ROOT/'validation.json').write_text(json.dumps({'status':'passed','templates':results,'visualReview':'pending-user-review','movingLight':'not performed'},indent=2)+'\n')