/** Precise layout authorized by the user; all photographic/puppet/paper content comes from original photos or imagegen. */ import {createRequire} from 'node:module'; import {writeFile} from 'node:fs/promises'; import {resolve,join} from 'node:path'; const require=createRequire(new URL('../apps/waterfall-wiki/package.json',import.meta.url)),sharp=require('sharp'); const root=resolve(process.argv[2]||'../output/shadow-puppet-covers'),W=1536,H=2048,HALF=1024; const specs=[ {key:'lotus',draft:'lotus-shadow-puppet.png',seam:724}, {key:'pavilion',draft:'pavilion-shadow-puppet.png',seam:727}, {key:'crane',draft:'crane-draft.png',seam:648,cutoutHeight:880}, {key:'lion',draft:'lion-draft.png',seam:724,cutoutHeight:880,generatedPhoto:true}, {key:'peach',draft:'peach-draft.png',seam:645,cutoutHeight:900} ]; const checks=[]; for(const spec of specs){ const path=join(root,spec.draft),meta=await sharp(path).metadata();let lower; if(spec.cutoutHeight){ const cutout=join(root,spec.key+'-cutout.png'),alpha=await sharp(cutout).stats(); if(!alpha.channels[3]||alpha.channels[3].min!==0)throw Error('Cutout lacks real transparent background: '+spec.key); const resizedSubject=await sharp(cutout).trim({background:'#00000000',threshold:1}).resize({width:1050,height:spec.cutoutHeight,fit:'inside'}).png().toBuffer(); const sized=await sharp(resizedSubject).metadata(); // Control rods continue beyond the frame rather than terminating on the paper. const subject=await sharp(resizedSubject).extract({left:0,top:0,width:sized.width,height:sized.height-36}).png().toBuffer(); const subjectInfo=await sharp(subject).metadata(); const paper=await sharp(join(root,'paper-stage.png')).resize(W,HALF,{fit:'cover'}).png().toBuffer(); lower=await sharp(paper).composite([{input:subject,left:Math.round((W-subjectInfo.width)/2),top:HALF-subjectInfo.height}]).png().toBuffer(); }else{ lower=await sharp(path).extract({left:0,top:spec.seam,width:meta.width,height:meta.height-spec.seam}).resize(W,HALF,{fit:'cover'}).png().toBuffer(); } const upper=spec.generatedPhoto?await sharp(path).extract({left:0,top:0,width:meta.width,height:spec.seam}).resize(W,HALF,{fit:'cover'}).png().toBuffer():await sharp(join(root,'sources',spec.key+'.jpg')).resize(W,HALF,{fit:'cover',position:'centre'}).png().toBuffer(); const output=join(root,spec.key+'-cover.png'); const composed=await sharp({create:{width:W,height:H,channels:3,background:'#f3d39c'}}).composite([{input:upper,left:0,top:0},{input:lower,left:0,top:HALF}]).png().toBuffer(); await sharp(composed).flatten({background:'#f3d39c'}).removeAlpha().png().toFile(output); const final=await sharp(output).metadata();if(final.width*4!==final.height*3||final.height%2||final.hasAlpha)throw Error('Invalid cover geometry: '+spec.key); checks.push({key:spec.key,path:output,width:final.width,height:final.height,photographRows:[0,HALF-1],puppetRows:[HALF,H-1],sourcePanelSeam:spec.seam,sourcePhoto:spec.generatedPhoto?'imagegen photographic outpaint of original':'original photograph',cutout:!!spec.cutoutHeight}); } await writeFile(join(root,'layout-checks.json'),JSON.stringify(checks,null,2)); console.log(JSON.stringify({covers:checks.length,width:W,height:H,seam:HALF,checks:'exact 3:4, equal halves, opaque'}));