Files
Sanctification/artifacts/cards/BP-001-moses/source/build-art.py

53 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Deterministic narrow post extension; original pixels untouched outside alpha."""
from pathlib import Path
from PIL import Image,ImageDraw,ImageFilter
import numpy as np,json,hashlib,shutil
p=Path(__file__).resolve().parent.parent
base=Image.open(p/'source/base-v04.png').convert('RGB');W,H=base.size
# Master coordinates: begin inside the final existing segment, terminate at socket.
box=(310,994,379,2652);s=4;x0,y0,x1,y1=box
layer=Image.new('RGBA',((x1-x0)*s,(y1-y0)*s));d=ImageDraw.Draw(layer)
def left(y):return 332.5-(y-975)*12.5/(2645-975)
def pt(x,y):return ((x-x0)*s,(y-y0)*s)
def poly(points,fill):d.polygon([pt(x,y) for x,y in points],fill=fill)
# Dark structural silhouette overlays the cloud, with a small shaped ground end.
poly([(left(994),994),(left(994)+42,994),(left(2626)+40,2626),(left(2645)+35,2645),(left(2645)+1,2640)],'#19190f')
# Unequal segment heights mirror the extant near and far posts.
edges=[994,1200,1417,1601,1818,2040,2259,2441,2628]
for i,(a,b) in enumerate(zip(edges,edges[1:])):
top=a+6 if i else a;bottom=b-7
l1,l2=left(top)+9,left(bottom)+9;wid=23
poly([(l1,top),(l1+wid,top+1),(l2+wid,bottom),(l2,bottom-1)],'#c08b28')
poly([(l1+3,top),(l1+wid-3,top+1),(l2+wid-3,bottom),(l2+3,bottom-1)],'#f0ca51')
poly([(l1+4,top+2),(l1+9,top+2),(l2+9,bottom-2),(l2+4,bottom-2)],'#f9e591')
# Inset diagonal pane break in selected longer segments, never across the outline.
if i in [2,5,7]:
mid=(top+bottom)/2;lx=left(mid)+9
d.line([pt(lx,mid-10),pt(lx+23,mid+11)],fill='#8b6726',width=2*s)
# Contained worked-glass variation sampled from the original source, not fresh noise.
arr=np.array(layer);gold=(arr[:,:,3]>0)&(arr[:,:,0]>100)
tex=base.crop((510,600,610,850)).resize(layer.size,Image.Resampling.BICUBIC)
t=np.asarray(tex,dtype=float).mean(axis=2);variation=np.clip((t-t.mean())*.26,-17,17)
rgb=arr[:,:,:3].astype(float);rgb[gold]+=variation[gold,None];arr[:,:,:3]=np.clip(rgb,0,255).astype('uint8')
layer=Image.fromarray(arr).filter(ImageFilter.GaussianBlur(1.6)).resize((x1-x0,y1-y0),Image.Resampling.LANCZOS)
overlay=Image.new('RGBA',(W,H));overlay.paste(layer,(x0,y0));overlay.save(p/'source/post-extension.png')
art=Image.alpha_composite(base.convert('RGBA'),overlay).convert('RGB');art.save(p/'source/art-master.png')
for name,size in [('art-review.png',(1000,1400)),('art-card-size.png',(500,700)),('art-thumbnail.png',(250,350))]:art.resize(size,Image.Resampling.LANCZOS).save(p/'review'/name)
board=Image.new('RGB',(720,1020),'#ece5d5');dd=ImageDraw.Draw(board)
for j,(label,im) in enumerate([('v04 — interrupted',base),('v05 — continued',art)]):
dd.text((j*360+15,10),label,fill='#263f50')
detail=im.crop((265,870,440,2680));detail.thumbnail((340,960));board.paste(detail,(j*360+100,40))
board.save(p/'review/post-before-after.png')
join=Image.new('RGB',(840,660),'#ece5d5');dd=ImageDraw.Draw(join)
for j,(label,im) in enumerate([('v04',base),('v05',art)]):
dd.text((j*420+10,10),label,fill='#263f50')
for k,b in enumerate([(305,940,395,1085),(300,2490,390,2670)]):
crop=im.crop(b);crop.thumbnail((380,300));crop=crop.resize((180,290 if k==0 else 360));join.paste(crop,(j*420+110,35+k*290))
join.save(p/'review/post-join-detail.png')
sha=lambda f:hashlib.sha256(f.read_bytes()).hexdigest()
a=np.asarray(base);b=np.asarray(art);diff=np.any(a!=b,axis=2);ys,xs=np.where(diff)
assert not diff[:y0].any() and not diff[y1:].any() and not diff[:,:x0].any() and not diff[:,x1:].any()
report={'status':'pass','baseSHA256':sha(p/'source/base-v04.png'),'artSHA256':sha(p/'source/art-master.png'),'dimensions':[W,H],'changedPixels':int(diff.sum()),'changedFraction':float(diff.mean()),'differenceBoundsExclusive':[int(xs.min()),int(ys.min()),int(xs.max()+1),int(ys.max()+1)],'authorizedRegion':list(box),'allPixelsOutsidePostRegionIdentical':True,'method':'Deterministic authored overlay, antialiased at 4x; RGB compositing only inside overlay alpha','scope':'Art-only, no card assembly'}
(p/'review/source-validation.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report))