/**
* Rebuild ~/Desktop/ftp-images-new from original ftp-images sources.
* Fixes blur + loading issues: single encode, baseline JPEG, no blur masks, higher resolution.
*/
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 OUT_W = 2400;
const OUT_H = 1800;
const PLATE_CX = OUT_W / 2;
const PLATE_CY = OUT_H / 2 + 24;
const PLATE_R = 660;
const JPEG = {
quality: 96,
mozjpeg: false,
progressive: false,
chromaSubsampling: '4:4:4' as const,
};
const HARD_CROP_SWEETS = new Set([
'bilder-bp/sweets/patisa/bild/IMG_1400.jpg',
'bilder-bp/sweets/namak-paray/bild/IMG_0209.jpg',
]);
function platePng(seed: number): Buffer {
const specks: string[] = [];
let s = seed + 17;
const rand = () => {
s = (s * 16807) % 2147483647;
return s / 2147483647;
};
const colors = ['#4A7EBB', '#E8843C', '#C4A882', '#6B9E78', '#D4A574'];
for (let i = 0; i < 64; i++) {
const a = rand() * Math.PI * 2;
const d = rand() * (PLATE_R - 80);
const x = PLATE_CX + Math.cos(a) * d;
const y = PLATE_CY + Math.sin(a) * d;
specks.push(
``
);
}
const svg = ``;
return Buffer.from(svg);
}
function collectImages(dir: string): string[] {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const full = path.join(dir, e.name);
if (e.isDirectory()) return collectImages(full);
if (/\.(jpe?g|png|webp)$/i.test(e.name) && !e.name.includes('.polish.')) return [full];
return [];
});
}
function findSource(rel: string): string | null {
const dir = path.dirname(path.join(SRC_ROOT, rel));
const base = path.basename(rel, path.extname(rel));
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 preparePhoto(src: string): Promise {
const rotated = sharp(src, { failOn: 'none', unlimited: true }).rotate();
const { data, info } = await rotated.toColorspace('srgb').removeAlpha().toBuffer({ resolveWithObject: true });
const w = info.width;
const h = info.height;
const aspect = OUT_W / OUT_H;
const sa = w / h;
let cropW = w;
let cropH = h;
let left = 0;
let top = 0;
if (sa > aspect) {
cropW = Math.round(h * aspect);
left = Math.round((w - cropW) / 2);
} else if (sa < 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 })
.modulate({ brightness: 1.02, saturation: 1.1 });
if (minDim < 1200) {
pipe = pipe.sharpen({ sigma: 1, m1: 0.6, m2: 0.3 });
} else {
pipe = pipe.sharpen({ sigma: 0.6, m1: 0.4, m2: 0.2 });
}
return pipe
.resize(OUT_W, OUT_H, { fit: 'fill', kernel: sharp.kernel.lanczos3 })
.png()
.toBuffer();
}
async function writeJpeg(buf: Buffer, dest: string): Promise {
fs.mkdirSync(path.dirname(dest), { recursive: true });
await sharp(buf).jpeg(JPEG).toFile(dest);
}
async function rebuildDish(src: string, dest: string): Promise {
const photo = await preparePhoto(src);
await writeJpeg(photo, dest);
}
async function cutoutFromPng(pngPath: string): Promise {
const blob = await removeBackground(pngPath, { model: 'small' });
return Buffer.from(await blob.arrayBuffer());
}
async function cutoutOk(buf: Buffer): Promise {
const { data, info } = await sharp(buf).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
const w = info.width ?? 1;
const h = info.height ?? 1;
let o = 0;
let t = 0;
for (let y = Math.floor(h * 0.15); y < Math.floor(h * 0.85); y++) {
for (let x = Math.floor(w * 0.15); x < Math.floor(w * 0.85); x++) {
if (data[(y * w + x) * 4 + 3] > 50) o++;
t++;
}
}
return o / t > 0.06;
}
async function placeOnPlate(foodPng: Buffer, index: number): Promise {
const meta = await sharp(foodPng).metadata();
const cw = meta.width ?? 1;
const ch = meta.height ?? 1;
const maxW = PLATE_R * 1.05;
const maxH = PLATE_R * 0.88;
const scale = Math.min(maxW / cw, maxH / ch, 1);
const food = await sharp(foodPng)
.resize(Math.round(cw * scale), Math.round(ch * scale), { kernel: sharp.kernel.lanczos3 })
.png()
.toBuffer();
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 + 4);
const shadow = Buffer.from(``);
return sharp(platePng(index))
.composite([
{ input: await sharp(shadow).png().toBuffer(), top: 0, left: 0 },
{ input: food, top, left },
])
.png()
.toBuffer();
}
async function hardCropFood(photo: Buffer): Promise {
const meta = await sharp(photo).metadata();
const w = meta.width ?? OUT_W;
const h = meta.height ?? OUT_H;
const cw = Math.round(w * 0.62);
const ch = Math.round(h * 0.62);
return sharp(photo)
.extract({ left: Math.round((w - cw) / 2), top: Math.round((h - ch) / 2), width: cw, height: ch })
.png()
.toBuffer();
}
async function rebuildSweet(src: string, dest: string, rel: string, index: number): Promise {
const photo = await preparePhoto(src);
const tmp = path.join(OUT_ROOT, `.tmp-${index}.png`);
fs.writeFileSync(tmp, photo);
try {
let food: Buffer;
if (HARD_CROP_SWEETS.has(rel)) {
food = await hardCropFood(photo);
} else {
const cut = await cutoutFromPng(tmp);
food = (await cutoutOk(cut)) ? cut : await hardCropFood(photo);
}
const composed = await placeOnPlate(food, index);
await writeJpeg(composed, dest);
} finally {
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
}
}
async function verifyJpeg(file: string): Promise {
try {
const meta = await sharp(file).metadata();
return (meta.width ?? 0) > 0 && (meta.height ?? 0) > 0;
} catch {
return false;
}
}
async function main() {
const images = collectImages(OUT_ROOT).sort();
let ok = 0;
let fail = 0;
console.log(`Rebuilding ${images.length} images → ${OUT_W}×${OUT_H} baseline JPEG\n`);
for (let i = 0; i < images.length; i++) {
const dest = images[i];
const rel = path.relative(OUT_ROOT, dest);
const src = findSource(rel);
if (!src) {
console.error(` ✗ ${rel}: no source`);
fail++;
continue;
}
try {
if (rel.includes(SWEETS_MARKER)) {
await rebuildSweet(src, dest, rel, i);
} else {
await rebuildDish(src, dest);
}
if (!(await verifyJpeg(dest))) throw new Error('invalid output JPEG');
const stat = fs.statSync(dest);
if (stat.size < 8000) throw new Error('file too small');
ok++;
console.log(` ✓ ${rel} (${Math.round(stat.size / 1024)}KB)`);
} catch (err) {
fail++;
console.error(` ✗ ${rel}: ${err instanceof Error ? err.message : err}`);
}
}
console.log(`\nDone: ${ok} rebuilt, ${fail} failed`);
if (fail) process.exit(1);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});