145 lines
4.0 KiB
TypeScript
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);
|
|
}); |