Files

145 lines
4.0 KiB
TypeScript

/**
* 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);
});