/** * Polish every image in ~/Desktop/ftp-images-new for eye-catching menu quality. * - Sweets: premium white-bg plate compositing (rebuilt from source) * - Dishes: vibrance, contrast, and sharpness boost in-place */ import { removeBackground } from '@imgly/background-removal-node'; import fs from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; const OUT_ROOT = path.join(process.env.HOME ?? '', 'Desktop', 'ftp-images-new'); const SRC_ROOT = path.join(__dirname, '..', 'ftp-images'); const SWEETS_MARKER = `${path.sep}sweets${path.sep}`; const CANVAS_W = 1600; const CANVAS_H = 1200; const PLATE_CX = CANVAS_W / 2; const PLATE_CY = CANVAS_H / 2 + 20; const PLATE_R = 440; const VIGNETTE_FALLBACK = new Set([ 'bilder-bp/sweets/namak-paray/bild/IMG_0209.jpg', ]); const CENTER_CROP_FALLBACK = new Set([ 'bilder-bp/sweets/patisa/bild/IMG_1400.jpg', ]); function plateSvg(seed: number): string { const specks: string[] = []; let s = seed + 11; const rand = () => { s = (s * 16807) % 2147483647; return s / 2147483647; }; const colors = ['#4A7EBB', '#E8843C', '#C4A882', '#6B9E78', '#D4A574', '#9B7CB8']; for (let i = 0; i < 88; i++) { const angle = rand() * Math.PI * 2; const dist = rand() * (PLATE_R - 55); const x = PLATE_CX + Math.cos(angle) * dist; const y = PLATE_CY + Math.sin(angle) * dist; const r = 2.5 + rand() * 10; specks.push( `` ); } return ` ${specks.join('')} `; } function collectImages(dir: string): string[] { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { const full = path.join(dir, entry.name); if (entry.isDirectory()) return collectImages(full); if (/\.(jpe?g|png|webp)$/i.test(entry.name)) return [full]; return []; }); } function findSource(destPath: string): string | null { const rel = path.relative(OUT_ROOT, destPath); const dir = path.dirname(path.join(SRC_ROOT, rel)); const base = path.basename(destPath, path.extname(destPath)); if (!fs.existsSync(dir)) return null; for (const name of fs.readdirSync(dir)) { if (name.startsWith(base) && /\.(jpe?g|png|webp)$/i.test(name)) { return path.join(dir, name); } } return null; } async function enhanceFromSource(src: string): Promise { const rotated = sharp(src, { failOn: 'none' }).rotate(); const { data, info } = await rotated.toColorspace('srgb').removeAlpha().toBuffer({ resolveWithObject: true }); const w = info.width; const h = info.height; const aspect = 4 / 3; const sourceAspect = w / h; let cropW = w; let cropH = h; let left = 0; let top = 0; if (sourceAspect > aspect) { cropW = Math.round(h * aspect); left = Math.round((w - cropW) / 2); } else if (sourceAspect < aspect) { cropH = Math.round(w / aspect); top = Math.round((h - cropH) / 2); } cropW = Math.min(cropW, w - left); cropH = Math.min(cropH, h - top); const minDim = Math.min(cropW, cropH); let pipe = sharp(data) .extract({ left, top, width: cropW, height: cropH }) .normalize() .modulate({ brightness: 1.05, saturation: 1.22 }) .gamma(1.04); pipe = minDim < 900 ? pipe.sharpen({ sigma: 1.1, m1: 0.7, m2: 0.35 }) : pipe.sharpen({ sigma: 0.85, m1: 0.55, m2: 0.28 }); return pipe .resize(CANVAS_W, CANVAS_H, { fit: 'fill', kernel: sharp.kernel.lanczos3 }) .jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toBuffer(); } async function cutout(imagePath: string, model: 'small' | 'medium'): Promise { const blob = await removeBackground(imagePath, { model }); return Buffer.from(await blob.arrayBuffer()); } async function cutoutValid(cutout: Buffer): Promise { const { data, info } = await sharp(cutout).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); const w = info.width ?? 1; const h = info.height ?? 1; const x0 = Math.floor(w * 0.2); const y0 = Math.floor(h * 0.2); const x1 = Math.floor(w * 0.8); const y1 = Math.floor(h * 0.8); let opaque = 0; let total = 0; for (let y = y0; y < y1; y++) { for (let x = x0; x < x1; x++) { if (data[(y * w + x) * 4 + 3] > 45) opaque++; total++; } } return opaque / total > 0.07; } async function beautifyFood(cutout: Buffer, maxW: number, maxH: number): Promise { const meta = await sharp(cutout).metadata(); const cw = meta.width ?? 1; const ch = meta.height ?? 1; const scale = Math.min(maxW / cw, maxH / ch, 1); return sharp(cutout) .resize(Math.round(cw * scale), Math.round(ch * scale), { kernel: sharp.kernel.lanczos3 }) .modulate({ brightness: 1.04, saturation: 1.18 }) .sharpen({ sigma: 0.7, m1: 0.5, m2: 0.25 }) .png() .toBuffer(); } async function stylizeWithCenterCrop(photo: Buffer, index: number): Promise { const meta = await sharp(photo).metadata(); const w = meta.width ?? CANVAS_W; const h = meta.height ?? CANVAS_H; const cropW = Math.round(w * 0.58); const cropH = Math.round(h * 0.58); const left = Math.round((w - cropW) / 2); const top = Math.round((h - cropH) / 2); const cropped = await sharp(photo) .extract({ left, top, width: cropW, height: cropH }) .modulate({ brightness: 1.04, saturation: 1.2 }) .sharpen({ sigma: 0.9 }) .png() .toBuffer(); const maskSvg = ` `; const masked = await sharp(cropped) .composite([{ input: await sharp(Buffer.from(maskSvg)).blur(12).png().toBuffer(), blend: 'dest-in' }]) .png() .toBuffer(); const fm = await sharp(masked).metadata(); const fw = fm.width ?? cropW; const fh = fm.height ?? cropH; const scale = Math.min((PLATE_R * 1.25) / fw, (PLATE_R * 1.05) / fh); const food = await sharp(masked) .resize(Math.round(fw * scale), Math.round(fh * scale), { kernel: sharp.kernel.lanczos3 }) .png() .toBuffer(); const f2 = await sharp(food).metadata(); const posL = Math.round(PLATE_CX - (f2.width ?? 0) / 2); const posT = Math.round(PLATE_CY - (f2.height ?? 0) / 2 + 6); const shadow = await sharp( Buffer.from(` `) ) .png() .toBuffer(); return sharp(Buffer.from(plateSvg(index))) .composite([ { input: shadow, top: 0, left: 0 }, { input: food, top: posT, left: posL }, ]) .jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toBuffer(); } async function stylizeWithVignette(photo: Buffer, index: number): Promise { const plateBuf = await sharp(Buffer.from(plateSvg(index))).png().toBuffer(); const maskSvg = ` `; const masked = await sharp(photo) .resize(CANVAS_W, CANVAS_H, { fit: 'cover', position: 'centre' }) .composite([{ input: await sharp(Buffer.from(maskSvg)).blur(18).png().toBuffer(), blend: 'dest-in' }]) .modulate({ brightness: 1.03, saturation: 1.15 }) .png() .toBuffer(); const fm = await sharp(masked).metadata(); const fw = fm.width ?? CANVAS_W; const fh = fm.height ?? CANVAS_H; const scale = Math.min((PLATE_R * 1.35) / fw, (PLATE_R * 1.15) / fh, 0.88); const food = await sharp(masked) .resize(Math.round(fw * scale), Math.round(fh * scale), { kernel: sharp.kernel.lanczos3 }) .png() .toBuffer(); const f2 = await sharp(food).metadata(); const left = Math.round(PLATE_CX - (f2.width ?? 0) / 2); const top = Math.round(PLATE_CY - (f2.height ?? 0) / 2 + 8); const shadow = await sharp( Buffer.from(` `) ) .png() .toBuffer(); return sharp(Buffer.from(plateSvg(index))) .composite([ { input: shadow, top: 0, left: 0 }, { input: food, top, left }, ]) .jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toBuffer(); } async function stylizeSweet(destPath: string, rel: string, index: number): Promise { const src = findSource(destPath); if (!src) throw new Error('missing source'); const enhanced = await enhanceFromSource(src); if (CENTER_CROP_FALLBACK.has(rel)) { const out = await stylizeWithCenterCrop(enhanced, index); fs.writeFileSync(destPath, out); return; } const useVignette = VIGNETTE_FALLBACK.has(rel); if (useVignette) { const out = await stylizeWithVignette(enhanced, index); fs.writeFileSync(destPath, out); return; } const tmp = path.join(OUT_ROOT, `.tmp-cut-${index}.jpg`); fs.writeFileSync(tmp, enhanced); let cutoutBuf: Buffer | null = null; try { for (const model of ['medium', 'small'] as const) { const attempt = await cutout(tmp, model); if (await cutoutValid(attempt)) { cutoutBuf = attempt; break; } } } finally { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); } if (!cutoutBuf) { const out = await stylizeWithVignette(enhanced, index); fs.writeFileSync(destPath, out); return; } const plateBuf = await sharp(Buffer.from(plateSvg(index))).png().toBuffer(); const food = await beautifyFood(cutoutBuf, PLATE_R * 1.42, PLATE_R * 1.18); const fm = await sharp(food).metadata(); const fw = fm.width ?? 1; const fh = fm.height ?? 1; const left = Math.round(PLATE_CX - fw / 2); const top = Math.round(PLATE_CY - fh / 2 + 6); const shadow = await sharp( Buffer.from(` `) ) .png() .toBuffer(); const out = await sharp(plateBuf) .composite([ { input: shadow, top: 0, left: 0 }, { input: food, top, left }, ]) .sharpen({ sigma: 0.35, m1: 0.3, m2: 0.15 }) .jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toBuffer(); fs.writeFileSync(destPath, out); } async function polishDish(imagePath: string): Promise { const tmp = `${imagePath}.polish.jpg`; await sharp(imagePath, { failOn: 'none' }) .rotate() .toColorspace('srgb') .normalize() .modulate({ brightness: 1.04, saturation: 1.16 }) .gamma(1.03) .sharpen({ sigma: 0.75, m1: 0.5, m2: 0.25 }) .jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toFile(tmp); fs.renameSync(tmp, imagePath); } async function polishPoster(imagePath: string): Promise { const tmp = `${imagePath}.polish.jpg`; await sharp(imagePath, { failOn: 'none' }) .rotate() .toColorspace('srgb') .normalize() .modulate({ brightness: 1.02, saturation: 1.1 }) .sharpen({ sigma: 0.5 }) .jpeg({ quality: 95, mozjpeg: true }) .toFile(tmp); fs.renameSync(tmp, imagePath); } async function main() { const onlyArg = process.argv.find((a) => a.startsWith('--only=')); const onlyFilter = onlyArg?.slice('--only='.length); let images = collectImages(OUT_ROOT).sort(); if (onlyFilter) { images = images.filter((p) => path.relative(OUT_ROOT, p).includes(onlyFilter)); } let sweets = 0; let dishes = 0; let posters = 0; console.log(`Polishing ${images.length} images in ${OUT_ROOT}\n`); for (let i = 0; i < images.length; i++) { const img = images[i]; const rel = path.relative(OUT_ROOT, img); try { if (rel.includes(SWEETS_MARKER)) { await stylizeSweet(img, rel, i); sweets++; console.log(` ✓ sweet ${rel}`); } else if (rel.includes('/others/')) { await polishPoster(img); posters++; console.log(` ✓ poster ${rel}`); } else { await polishDish(img); dishes++; console.log(` ✓ dish ${rel}`); } } catch (err) { console.error(` ✗ ${rel}: ${err instanceof Error ? err.message : err}`); } } console.log(`\nDone: ${sweets} sweets restyled, ${dishes} dishes polished, ${posters} posters`); } main().catch((err) => { console.error(err); process.exit(1); });