/** * Enhance all images under ftp-images/ and write to ~/Desktop/ftp-images-new * preserving directory structure. Non-image files are copied as-is. */ import fs from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; const ROOT = path.resolve(__dirname, '..'); const SRC_ROOT = path.join(ROOT, 'ftp-images'); const OUT_ROOT = path.join(process.env.HOME ?? '', 'Desktop', 'ftp-images-new'); const IMAGE_RE = /\.(jpe?g|png|webp)$/i; const TARGET_W = 1600; const TARGET_H = 1200; const JPEG_QUALITY = 92; function walk(dir: string): string[] { const entries = fs.readdirSync(dir, { withFileTypes: true }); return entries.flatMap((entry) => { const full = path.join(dir, entry.name); if (entry.isDirectory()) return walk(full); return [full]; }); } function isPosterPath(rel: string): boolean { return rel.includes('/others/') || /poster/i.test(rel); } async function enhanceFoodPhoto(input: string, output: string) { const rotated = sharp(input, { failOn: 'none' }).rotate(); const { data, info } = await rotated .toColorspace('srgb') .removeAlpha() .toBuffer({ resolveWithObject: true }); const width = info.width; const height = info.height; const targetAspect = TARGET_W / TARGET_H; const sourceAspect = width / height; let cropW = width; let cropH = height; let left = 0; let top = 0; if (sourceAspect > targetAspect) { cropW = Math.round(height * targetAspect); left = Math.round((width - cropW) / 2); } else if (sourceAspect < targetAspect) { cropH = Math.round(width / targetAspect); top = Math.round((height - cropH) / 2); } cropW = Math.min(cropW, width - left); cropH = Math.min(cropH, height - top); let pipeline = sharp(data) .extract({ left, top, width: cropW, height: cropH }) .normalize() .modulate({ brightness: 1.04, saturation: 1.18 }) .gamma(1.05); const minDim = Math.min(cropW, cropH); if (minDim < 900) { pipeline = pipeline.sharpen({ sigma: 1.2, m1: 0.8, m2: 0.4 }); } else { pipeline = pipeline.sharpen({ sigma: 0.9, m1: 0.6, m2: 0.3 }); } await pipeline .resize(TARGET_W, TARGET_H, { fit: 'fill', kernel: sharp.kernel.lanczos3 }) .jpeg({ quality: JPEG_QUALITY, mozjpeg: true, chromaSubsampling: '4:4:4' }) .toFile(output); } async function enhancePoster(input: string, output: string) { await sharp(input, { failOn: 'none' }) .rotate() .toColorspace('srgb') .normalize() .modulate({ brightness: 1.02, saturation: 1.08 }) .sharpen({ sigma: 0.6, m1: 0.5, m2: 0.25 }) .resize({ width: 2400, withoutEnlargement: false, kernel: sharp.kernel.lanczos3 }) .jpeg({ quality: 94, mozjpeg: true }) .toFile(output); } async function processImage(src: string, rel: string) { const outRel = rel.replace(IMAGE_RE, '.jpg'); const dest = path.join(OUT_ROOT, outRel); fs.mkdirSync(path.dirname(dest), { recursive: true }); if (isPosterPath(rel)) { await enhancePoster(src, dest); } else { await enhanceFoodPhoto(src, dest); } return dest; } async function main() { if (!fs.existsSync(SRC_ROOT)) { console.error(`Source not found: ${SRC_ROOT}`); process.exit(1); } fs.mkdirSync(OUT_ROOT, { recursive: true }); const files = walk(SRC_ROOT); let images = 0; let copied = 0; for (const src of files) { const rel = path.relative(SRC_ROOT, src); if (IMAGE_RE.test(src)) { try { const dest = await processImage(src, rel); images++; console.log(` ✓ ${rel} → ${path.basename(dest)}`); } catch (err) { console.error(` ✗ ${rel}: ${err instanceof Error ? err.message : err}`); } continue; } const dest = path.join(OUT_ROOT, rel); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); copied++; } console.log(`\nDone: ${images} images enhanced → ${OUT_ROOT}`); console.log(` ${copied} non-image files copied`); } main().catch((err) => { console.error(err); process.exit(1); });