94 lines
2.2 KiB
JavaScript
94 lines
2.2 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import sharp from "sharp";
|
|
|
|
const SRC_DIR = path.resolve("docs/public"); // change if needed
|
|
const OUT_DIR = path.resolve("docs/public/images"); // change if needed
|
|
|
|
const MAX_W = 800;
|
|
const MAX_H = 600;
|
|
|
|
const FORMAT = "webp";
|
|
const WEBP_QUALITY = 75; // tweak 60-85 if you want smaller/larger
|
|
|
|
async function walk(dir) {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
console.log(dir, entries.filter((e) => !e.isDirectory()).length);
|
|
|
|
const files = [];
|
|
for (const e of entries) {
|
|
const p = path.join(dir, e.name);
|
|
if (p === OUT_DIR) continue;
|
|
if (e.isDirectory()) files.push(...(await walk(p)));
|
|
else files.push(p);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function isImage(file) {
|
|
return /\.(png|jpe?g|webp|avif)$/i.test(file);
|
|
}
|
|
|
|
async function ensureDir(p) {
|
|
await fs.mkdir(p, { recursive: true });
|
|
}
|
|
|
|
async function processOne(inPath, outPathBase) {
|
|
await ensureDir(path.dirname(outPathBase));
|
|
|
|
await sharp(inPath)
|
|
.rotate() // respects EXIF orientation
|
|
.resize({
|
|
width: MAX_W,
|
|
height: MAX_H,
|
|
fit: "inside", // preserve aspect, fit within box
|
|
withoutEnlargement: true, // don't upscale small images
|
|
})
|
|
.toFormat(FORMAT, {
|
|
quality: WEBP_QUALITY,
|
|
effort: 4,
|
|
})
|
|
.toFile(`${outPathBase}.${FORMAT}`);
|
|
}
|
|
|
|
async function main() {
|
|
const files = (await walk(SRC_DIR)).filter(isImage);
|
|
await ensureDir(OUT_DIR);
|
|
|
|
let done = 0;
|
|
|
|
for (const inPath of files) {
|
|
const rel = path.relative(SRC_DIR, inPath);
|
|
|
|
// strip original extension and write same relative path
|
|
const outBase = path
|
|
.join(OUT_DIR, rel)
|
|
.replace(/\.(png|jpe?g|webp|avif)$/i, "");
|
|
|
|
const outPath = `${outBase}.${FORMAT}`;
|
|
|
|
// simple cache: skip if output exists and is newer than input
|
|
try {
|
|
const [stIn, stOut] = await Promise.all([
|
|
fs.stat(inPath),
|
|
fs.stat(outPath),
|
|
]);
|
|
if (stOut.mtimeMs >= stIn.mtimeMs) continue;
|
|
} catch {
|
|
// output missing -> process
|
|
}
|
|
|
|
await processOne(inPath, outBase);
|
|
done++;
|
|
}
|
|
|
|
console.log(
|
|
`Optimized ${done} image(s) to max ${MAX_W}x${MAX_H} (${FORMAT}).`,
|
|
);
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|