101 lines
10 KiB
Python
101 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Requested provisional David layout only; no production printing or finish masks."""
|
|
from pathlib import Path
|
|
import hashlib,json,os,subprocess
|
|
from xml.sax.saxutils import escape
|
|
import numpy as np
|
|
from PIL import Image,ImageDraw
|
|
P=Path(__file__).resolve().parent
|
|
R=next(p for p in P.parents if (p/'docs/card-workspace.md').is_file())
|
|
ART=P.parents[1]/'source/art-master.png'
|
|
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
|
|
original=sha(ART)
|
|
fonts=json.loads((R/'fonts/manifest.json').read_text())
|
|
for name in ['P052-Bold.otf','SanctificationP052-Medium.otf']:
|
|
assert sha(R/'fonts'/name)==fonts['files'][name]['sha256']
|
|
(P/'fonts.conf').write_text(f'<fontconfig><dir>{R/"fonts"}</dir><cachedir>{P/"font-cache"}</cachedir></fontconfig>')
|
|
env=dict(os.environ,FONTCONFIG_FILE=str(P/'fonts.conf'))
|
|
def ink(*args):return subprocess.check_output(['inkscape',*map(str,args)],env=env,text=True,stderr=subprocess.PIPE)
|
|
def render(src,dst):ink(src,'--export-type=png',f'--export-filename={dst}','--export-width=2000','--export-height=2800')
|
|
# Hand-authored silhouette of the FRONT hanging fold, in native 1060 x 1484 coordinates.
|
|
# Keep whole cloth pixels and internal lead seams, with no color-based alpha or hole repair.
|
|
head='<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">'
|
|
path="M -5 1484 C 27 1398 52 1338 93 1302 C 134 1277 194 1254 239 1237 C 275 1220 299 1227 302 1284 C 291 1269 279 1257 262 1260 C 265 1304 272 1358 282 1402 L 292 1484 Z"
|
|
scale=2000/1060
|
|
clip=f'<defs><clipPath id="front-fold"><path transform="scale({scale})" d="{path}"/></clipPath></defs>'
|
|
fragment_svg=P/'mantle-overlap-preview.svg'
|
|
fragment_svg.write_text(head+clip+'<image xlink:href="../../source/art-master.png" width="2000" height="2800" clip-path="url(#front-fold)"/></svg>')
|
|
render(fragment_svg,P/'mantle-overlap-preview.png')
|
|
alpha=np.asarray(Image.open(P/'mantle-overlap-preview.png').convert('RGBA').getchannel('A'))
|
|
(P/'mantle-contour.json').write_text(json.dumps({'method':'Hand-authored continuous SVG silhouette; source cloth RGB unchanged','nativeCanvas':[1060,1484],'path':path,'uniformScale':scale,'component':'Front hanging fold only; upper-left backing sweep reveals the existing source-art junction to vertical drapery'},indent=2)+'\n')
|
|
rows=[('title','DAVID',1490,255,138,'P052',700)]
|
|
verses=['“I have found David','the son of Jesse, a man','after My own heart,','who will do all My will.”']
|
|
for i,t in enumerate(verses):rows.append((f'verse-{i}',t,1320,2306+i*85,80,'Sanctification P052',500))
|
|
rows.append(('reference','Acts 13:22 • NKJV',1320,2660,57,'Sanctification P052',500))
|
|
for ident,text,x,y,size,family,weight in rows:
|
|
font=R/'fonts'/('P052-Bold.otf' if weight==700 else 'SanctificationP052-Medium.otf')
|
|
charset=subprocess.check_output(['fc-query','--format','%{charset}',str(font)],text=True)
|
|
coverage=set()
|
|
for span in charset.split():
|
|
bounds=[int(v,16) for v in span.split('-')];coverage.update(range(bounds[0],bounds[-1]+1))
|
|
assert all(ord(c) in coverage for c in text),ident
|
|
texts=''.join(f'<text id="{ident}" x="{x}" y="{y}" font-family="{family}" font-weight="{weight}" font-size="{size}" text-anchor="middle" fill="#263f50">{escape(text)}</text>' for ident,text,x,y,size,family,weight in rows)
|
|
head='<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">'
|
|
(P/'text-preview.svg').write_text(head+texts+'</svg>');render(P/'text-preview.svg',P/'text-preview.png')
|
|
# Simple SVG geometry is intentionally undecorated until layout review.
|
|
base='<image xlink:href="../../source/art-master.png" width="2000" height="2800"/>'
|
|
frame='<rect x="0" y="0" width="2000" height="2800" fill="none" stroke="#aa853f" stroke-width="36"/><rect x="30" y="30" width="1940" height="2740" fill="none" stroke="#dfbd70" stroke-width="4"/>'
|
|
lower_path='M 790 2200 H 1920 Q 1940 2200 1940 2220 V 2720 Q 1940 2740 1920 2740 H 380 Q 360 2740 360 2720 V 2500 Q 360 2470 386 2450 C 520 2350 660 2265 770 2207 Q 782 2200 790 2200 Z'
|
|
lower_rule='M 793 2214 H 1920 Q 1926 2214 1926 2220 V 2720 Q 1926 2726 1920 2726 H 380 Q 374 2726 374 2720 V 2500 Q 374 2478 394 2461 C 528 2362 667 2277 777 2219 Q 787 2214 793 2214 Z'
|
|
panels='<rect x="1080" y="90" width="820" height="260" rx="18" fill="#f4ead4"/><rect x="1094" y="104" width="792" height="232" rx="10" fill="none" stroke="#806438" stroke-width="4"/><path d="'+lower_path+'" fill="#f4ead4"/><path d="'+lower_rule+'" fill="none" stroke="#806438" stroke-width="4"/>'
|
|
(P/'backing-review.svg').write_text(head+panels+'</svg>');render(P/'backing-review.svg',P/'backing-review.png')
|
|
backing_alpha=np.asarray(Image.open(P/'backing-review.png').convert('RGBA').getchannel('A'))
|
|
overlap='<image xlink:href="mantle-overlap-preview.png" width="2000" height="2800"/>'
|
|
for name,fg in [('composed-preview',overlap),('without-overlap', '')]:
|
|
src=P/(name+'.svg');src.write_text(head+base+frame+panels+fg+texts+'</svg>');render(src,P/(name+'-2000.png'))
|
|
image=Image.open(P/(name+'-2000.png')).convert('RGB');image.resize((1000,1400),Image.Resampling.LANCZOS).save(P/(name+'-1000.png'))
|
|
# Measure each independently rendered line so no clipping can hide an overlap.
|
|
checks=[]
|
|
for ident,text,x,y,size,family,weight in rows:
|
|
node=next(v for v in texts.split('</text>') if f'id="{ident}"' in v)+'</text>'
|
|
src=P/(ident+'-ink.svg');src.write_text(head+node+'</svg>');dst=P/(ident+'-ink.png');render(src,dst);bounds=Image.open(dst).convert('RGBA').getchannel('A').getbbox();assert bounds
|
|
box=(1098,108,1882,332) if ident=='title' else (410,2240,1910,2700)
|
|
assert bounds[0]>=box[0] and bounds[1]>=box[1] and bounds[2]<=box[2] and bounds[3]<=box[3],(ident,bounds,box)
|
|
inkalpha=np.asarray(Image.open(dst).convert('RGBA').getchannel('A'));assert not np.any((inkalpha>0)&(alpha>0)),ident
|
|
assert np.all(backing_alpha[inkalpha>0]==255),ident
|
|
checks.append({'id':ident,'text':text,'font':family,'weight':weight,'size':size,'inkBounds':list(bounds),'safeBox':list(box),'mantleIntersectionPixels':0,'glyphPixelsOutsideOpaqueBacking':0,'glyphCoverageVerified':True})
|
|
board=Image.new('RGB',(1000,750),'#11161c');d=ImageDraw.Draw(board)
|
|
for i,(label,path) in enumerate([('Simple backing, no overlap',P/'without-overlap-1000.png'),('Provisional mantle overlap',P/'composed-preview-1000.png')]):
|
|
d.text((i*500+12,12),label,fill='#f4ead4');im=Image.open(path);im.thumbnail((480,672));board.paste(im,(i*500+10,45))
|
|
board.save(P/'overlap-comparison.png')
|
|
assert sha(ART)==original
|
|
report={'status':'passed','scope':'Provisional composed layout preview only','artSHA256':original,'sourceArtworkUnchanged':True,'canvas':[2000,2800],'titleBacking':[1080,90,820,260],'verseBacking':{'bounds':[360,2200,1580,540],'path':lower_path,'insetRulePath':lower_rule},'palette':{'backing':'#f4ead4','text':'#263f50','rule':'#806438'},'fontManifestSHA256':sha(R/'fonts/manifest.json'),'textChecks':checks,'overlap':{'method':'Hand-authored continuous vector contour of front hanging mantle fold; same-canvas source fragment.','contour':'mantle-contour.json','note':'Review alpha edges before production; not a locked foreground layer.','letteringIntersectionPixels':0},'notPerformed':['Decorative asset generation','Final printing exports','Finish or glyph-protection masks','Harness fixture installation','GPU/moving-light validation']}
|
|
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps({'status':'passed','preview':str((P/'composed-preview-1000.png').relative_to(R)),'inkChecks':checks},indent=2))
|
|
|
|
# Register the source, fragment and lettering independently of visual review.
|
|
art_array=np.asarray(Image.open(ART).convert('RGB'))
|
|
composed=np.asarray(Image.open(P/'composed-preview-2000.png').convert('RGB'))
|
|
fragment=np.asarray(Image.open(P/'mantle-overlap-preview.png').convert('RGBA'))
|
|
assert np.array_equal(fragment[:,:,:3][alpha==255],art_array[alpha==255])
|
|
assert (P/'text-preview.png').read_bytes()==(P.parent/'layout-preview-v2/text-preview.png').read_bytes()
|
|
# A continuous narrow corridor through the existing cloth junction must remain source-visible.
|
|
points=[(300,1160),(290,1190),(278,1215),(265,1237),(240,1254),(220,1268),(195,1280),(175,1300)]
|
|
corridor=Image.new('L',(2000,2800));ImageDraw.Draw(corridor).line([(round(x*scale),round(y*scale)) for x,y in points],fill=255,width=12)
|
|
selected=np.asarray(corridor)>0
|
|
corridor_changes=int(np.any(composed!=art_array,axis=2)[selected].sum())
|
|
assert corridor_changes==0,corridor_changes
|
|
old=np.asarray(Image.open(P.parent/'layout-preview-v2/composed-preview-2000.png').convert('RGB'))
|
|
old_occluded=int(np.any(old!=art_array,axis=2)[selected].sum())
|
|
assert old_occluded>0
|
|
report.update({'sourceFragmentOpaquePixelsMatch':True,'letteringPNGMatchesV2':True,'glyphCoverageVerified':True,'glyphPixelsOutsideOpaqueBacking':0,'continuityCorridor':{'nativePolyline':points,'masterWidth':12,'sampledPixels':int(selected.sum()),'sourceChangedPixels':corridor_changes,'previousPreviewChangedPixels':old_occluded,'note':'Authored interior corridor through visible existing mantle junction; supporting evidence, not a general anatomical continuity test.'},'staticReview':{'status':'provisional-improvement','assessment':'Swept upper-left backing exposes the red drape joining the front fold. The fold now visibly descends from the existing mantle; no illustration editing was needed. The diagonal backing shoulder is visible and remains a bespoke layout decision for user review.','limitation':'Small foreground tip and diagonal panel shoulder still need user aesthetic review; no production foreground layer approved.'}})
|
|
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
board=Image.new('RGB',(1000,750),'#11161c');d=ImageDraw.Draw(board)
|
|
for i,(label,f) in enumerate([('Previous: junction concealed',P.parent/'layout-preview-v2/composed-preview-1000.png'),('Astra: original mantle junction exposed',P/'composed-preview-1000.png')]):
|
|
d.text((i*500+12,12),label,fill='#f4ead4');im=Image.open(f);im.thumbnail((480,672));board.paste(im,(i*500+10,45))
|
|
board.save(P/'before-after.png')
|
|
board=Image.new('RGB',(1000,710),'#11161c');d=ImageDraw.Draw(board)
|
|
for i,(label,f) in enumerate([('Previous',P.parent/'layout-preview-v2/composed-preview-2000.png'),('Astra: continuous cloth',P/'composed-preview-2000.png')]):
|
|
d.text((i*500+12,12),label,fill='#f4ead4');im=Image.open(f).crop((120,1920,950,2800));im.thumbnail((480,645));board.paste(im,(i*500+10,45))
|
|
board.save(P/'junction-comparison.png')
|
|
print(json.dumps({'continuityCorridor':report['continuityCorridor'],'sourceFragmentOpaquePixelsMatch':True,'letteringPNGMatchesV2':True,'glyphPixelsOutsideOpaqueBacking':0},indent=2))
|