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 }); const files = []; for (const e of entries) { const p = path.join(dir, e.name); 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); });