import type {Asset} from './types.ts'; /** Repository file listings can be truncated. Only a failed image request, * not absence from that listing, means a thumbnail is unavailable. * Also upgrades catalogues generated by earlier app versions. */ export function previewSources(asset:Pick):string[]{ const prepared=asset.preview?[asset.preview]:[]; const url=new URL(asset.image); if(!url.pathname.endsWith('.png'))return [...new Set([...prepared,asset.image,asset.imageFallback])]; const base=url.pathname.replace(/(?:_(?:transparent|light))?\.png$/,''); const variants=['_transparent.png','_light.png','.png'].map(suffix=>{ const candidate=new URL(url);candidate.pathname=base+suffix;return candidate.href; }); return [...new Set([...prepared,...variants,asset.imageFallback,asset.image])]; } /** Read native alpha bounds without altering colors or transparency. * Opaque previews retain their background; color-keying them can destroy * dark details and leave baked shadows behind. Antialiased edges stay intact. */ export function alphaBounds(data:Uint8ClampedArray,width:number,height:number){ let left=width,right=-1,top=height,bottom=-1; for(let y=0;y0){ left=Math.min(left,x);right=Math.max(right,x);top=Math.min(top,y);bottom=Math.max(bottom,y); } return right2048||canvas.height>2048)return null; const context=canvas.getContext('2d',{willReadFrequently:true});if(!context)return null;context.drawImage(image,0,0); const pixels=context.getImageData(0,0,canvas.width,canvas.height); const bounds=alphaBounds(pixels.data,canvas.width,canvas.height);if(!bounds)return null; const output=document.createElement('canvas');output.width=output.height=320;const out=output.getContext('2d');if(!out)return null; out.imageSmoothingEnabled=true;out.imageSmoothingQuality='high'; const {left,top,width:w,height:h}=bounds,scale=292/Math.max(w,h); out.drawImage(canvas,left,top,w,h,(320-w*scale)/2,(320-h*scale)/2,w*scale,h*scale); return output.toDataURL('image/png'); }catch{return null;} }