# Part of the HUBIQ design system (hiq-design, branch v2, illustration/riso). Edit it here;
# illustration/README.md says how the set is made. Moved here 26 October 2026.
# Three plates: forest under-print (copy of the white art), house-green accent objects, white key on top.
import sys, numpy as np
from PIL import Image, ImageFilter
S=1600; rng=np.random.default_rng(11)
def norm(a): return (a-a.min())/(a.max()-a.min()+1e-6)
def blur(a,r): return np.asarray(Image.fromarray((np.clip(a,0,1)*255).astype(np.uint8)).filter(ImageFilter.GaussianBlur(r))).astype(float)/255
def noise(r): return norm(blur(rng.random((S,S)),r))
def streaks():
    n=rng.random((S//40,S)); im=Image.fromarray((n*255).astype(np.uint8)).resize((S,S),Image.BICUBIC).filter(ImageFilter.GaussianBlur(6))
    return norm(np.asarray(im).astype(float))
def move(m,dx,dy): return np.asarray(Image.fromarray((np.clip(m,0,1)*255).astype(np.uint8)).rotate(0,translate=(dx,dy))).astype(float)/255
def plate(m,lo):
    m=blur(m,1.6); m=np.clip((m+(noise(1.0)-.5)*.55-.5)*4+.5,0,1)
    cov=lo+(1-lo)*(.55*noise(14)+.45*streaks())
    grain=.80+.20*blur((rng.random((S,S))<.5).astype(float),.7)
    holes=blur((norm(blur(rng.random((S,S)),2.2))<.93).astype(float),.6)
    return m*cov*grain*holes
def run(src,dst,dx=16,dy=-12,hole=.07,cap=.98):
    a=np.asarray(Image.open(src).convert('RGB').resize((S,S),Image.LANCZOS)).astype(float); R,G,B=a[...,0],a[...,1],a[...,2]
    g=np.clip((G-B-55)/70,0,1)
    w=np.clip((np.minimum(np.minimum(R,G),B)-80)/90,0,1)*(1-g)
    ov=np.clip((G-B-15)/40,0,1)*np.clip((B-110)/60,0,1); g=np.maximum(g,ov); w=np.maximum(w,ov)
    gi=Image.fromarray((g*255).astype(np.uint8))
    closedI=gi.filter(ImageFilter.MaxFilter(21)).filter(ImageFilter.MinFilter(21))   # fill dark linework inside green shapes
    # fill every hole fully enclosed by a green shape (letters, counters, filament), however thick its strokes
    from PIL import ImageDraw
    bi=closedI.point(lambda v:255 if v>127 else 0).convert('L'); pad=Image.new('L',(S+2,S+2),0); pad.paste(bi,(1,1))
    ImageDraw.floodfill(pad,(0,0),128)
    outside=np.asarray(pad).astype(int)[1:-1,1:-1]==128
    closed=np.maximum(np.asarray(closedI).astype(float)/255,(~outside).astype(float))
    closedI=Image.fromarray((closed*255).astype(np.uint8))
    keep=np.asarray(closedI.filter(ImageFilter.MinFilter(31))).astype(float)/255      # seeds: only big solid shapes survive
    for _ in range(60):                                                               # regrow seeds inside the shape: points and corners come back, thin rims do not
        keep=np.minimum(np.asarray(Image.fromarray((keep*255).astype(np.uint8)).filter(ImageFilter.MaxFilter(5))).astype(float)/255,closed)
    # dark details drawn inside accent shapes (letters, filament): knocked out of every plate later
    dark=(np.maximum(np.maximum(R,G),B)<70).astype(float)
    knock=dark*(keep>.5)
    g=g*np.clip(keep*3,0,1)
    f=np.clip(w-g,0,1)                                   # forest: under every white shape not already accent green
    f=move(plate(f,.8),dx,dy); g=move(plate(g,.74),dx,dy); w=plate(w,.66)
    kn=blur(knock,.8); kn_m=move(kn,dx,dy)
    f*=1-kn_m; g*=1-kn_m; w*=1-kn
    dots=blur((rng.random((S,S))<hole).astype(float),.6); mott=.55*noise(5)+.45*noise(22)
    op=np.clip(w*1.5,0,1)*cap*(1-dots*.85)*(.78+.22*mott)
    FO=np.array([27,51,11.]); GR=np.array([169,242,12.]); WH=np.array([236,241,248.])
    # paint bottom to top: forest, green, white
    col=np.zeros((S,S,3)); al=np.zeros((S,S))
    for m,c in ((f,FO),(g,GR),(op,WH)):
        col=c*m[...,None]+col*(1-m[...,None]); al=m+al*(1-m)
    col=col/np.maximum(al,1e-4)[...,None]   # un-premultiply
    # col above is premultiplied-style accumulation; recompute properly
    out=Image.fromarray(np.dstack([np.clip(col,0,255),al*255]).astype(np.uint8),'RGBA').resize((1000,1000),Image.LANCZOS)
    out.save(dst,optimize=True)
    c=Image.new('RGBA',out.size,(0,6,20,255)); c.alpha_composite(out); c.convert('RGB').save(dst.replace('.png','-check.jpg'),quality=88)
run(sys.argv[1],sys.argv[2])
