Files

66 lines
6.3 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,ImageFilter
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')
# Provisional overlap selection: trailing red cloth in the lower-left, never faces/hands.
art=Image.open(ART).convert('RGBA');a=np.asarray(art);rgb=a[:,:,:3].astype(float)
region=np.zeros((2800,2000),bool);region[2100:,:950]=True
red=(rgb[:,:,0]>40)&(rgb[:,:,0]>1.8*rgb[:,:,1])&(rgb[:,:,0]>1.3*rgb[:,:,2])&region
mask=Image.fromarray((red*255).astype('uint8')).filter(ImageFilter.MaxFilter(15)).filter(ImageFilter.MinFilter(15))
# Keep the connected trailing cloth; discard isolated warm flecks in adjacent blue panes.
probe=np.asarray(mask);yy,xx=np.where(probe[2300:2500,:600]>0);assert len(xx)
k=len(xx)//2;seed=(int(xx[k]),int(yy[k])+2300)
component=mask.copy();ImageDraw.floodfill(component,seed,128)
mask=Image.fromarray(np.where(np.asarray(component)==128,255,0).astype('uint8'))
# Fill enclosed cloth seams while keeping the exterior outside the selection.
exterior=mask.copy();ImageDraw.floodfill(exterior,(1999,0),128)
b=np.asarray(exterior);filled=Image.fromarray(np.where(b==128,0,255).astype('uint8'))
alpha=np.asarray(filled.filter(ImageFilter.MaxFilter(3))).copy();alpha[~region]=0
fragment=art.copy();fragment.putalpha(Image.fromarray(alpha));fragment.save(P/'mantle-overlap-preview.png')
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))
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"/>'
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"/><rect x="360" y="2200" width="1580" height="540" rx="20" fill="#f4ead4"/><rect x="374" y="2214" width="1552" height="512" rx="12" fill="none" stroke="#806438" stroke-width="4"/>'
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
checks.append({'id':ident,'text':text,'font':family,'weight':weight,'size':size,'inkBounds':list(bounds),'safeBox':list(box),'mantleIntersectionPixels':0})
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':[360,2200,1580,540],'palette':{'backing':'#f4ead4','text':'#263f50','rule':'#806438'},'fontManifestSHA256':sha(R/'fonts/manifest.json'),'textChecks':checks,'overlap':{'method':'Provisional bounded connected red-cloth selection, seam closure and enclosed-hole fill; same-canvas source fragment.','region':[0,2100,950,700],'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))