// Nén ảnh panorama + modal, backup bản gốc, sinh ảnh mờ (LQIP). // Chạy: node scripts/optimize-images.mjs (chạy lại nhiều lần an toàn — luôn nén từ backup gốc) import sharp from "sharp"; import { promises as fs } from "node:fs"; import path from "node:path"; const ROOT = process.cwd(); const PANO_DIR = path.join(ROOT, "public/images/chua-thai-lac"); const MODAL_DIR = path.join(ROOT, "public/modal"); const BACKUP_DIR = path.join(ROOT, "image-originals"); const LQIP_OUT = path.join(ROOT, "src/app/tours/chua-thai-lac.lqip.ts"); const PANO_WIDTH = 6144; const PANO_QUALITY = 80; const LQIP_WIDTH = 32; const MODAL_FILES = ["modal-1.png", "modal-2.png"]; const fmt = (b) => (b / 1024 / 1024).toFixed(2) + "MB"; const ensureDir = (d) => fs.mkdir(d, { recursive: true }); const exists = (p) => fs.access(p).then(() => true).catch(() => false); // Đảm bảo ảnh gốc đã được backup; luôn ĐỌC từ backup để không nén chồng. async function sourceFrom(destPath, backupSubdir) { const name = path.basename(destPath); const backupPath = path.join(BACKUP_DIR, backupSubdir, name); await ensureDir(path.dirname(backupPath)); if (!(await exists(backupPath))) { await fs.copyFile(destPath, backupPath); } return backupPath; } async function optimizePanoramas() { const files = (await fs.readdir(PANO_DIR)).filter((f) => /\.jpe?g$/i.test(f)).sort(); const lqip = {}; for (const f of files) { const dest = path.join(PANO_DIR, f); const src = await sourceFrom(dest, "chua-thai-lac"); const before = (await fs.stat(dest)).size; const buf = await sharp(src) .rotate() .resize({ width: PANO_WIDTH, withoutEnlargement: true }) .jpeg({ quality: PANO_QUALITY, mozjpeg: true, progressive: true }) .toBuffer(); await fs.writeFile(dest, buf); const lqipBuf = await sharp(src).rotate().resize({ width: LQIP_WIDTH }).jpeg({ quality: 40 }).toBuffer(); lqip["/images/chua-thai-lac/" + f] = "data:image/jpeg;base64," + lqipBuf.toString("base64"); console.log(`${f}: ${fmt(before)} -> ${fmt(buf.length)} (-${Math.round((1 - buf.length / before) * 100)}%)`); } return lqip; } async function optimizeModals() { for (const f of MODAL_FILES) { const dest = path.join(MODAL_DIR, f); if (!(await exists(dest))) continue; const src = await sourceFrom(dest, "modal"); const before = (await fs.stat(dest)).size; const buf = await sharp(src).png({ compressionLevel: 9, palette: true, quality: 90, dither: 1 }).toBuffer(); await fs.writeFile(dest, buf); console.log(`${f}: ${fmt(before)} -> ${fmt(buf.length)}`); } } async function writeLqip(lqip) { const entries = Object.entries(lqip) .map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)},`) .join("\n"); const content = "// TỰ ĐỘNG SINH bởi scripts/optimize-images.mjs — KHÔNG sửa tay.\n" + "// Ảnh mờ tạm (blur-up/LQIP) base64 cho mỗi panorama, khoá theo đường dẫn ảnh.\n" + "export const lqip: Record = {\n" + entries + "\n};\n"; await fs.writeFile(LQIP_OUT, content); console.log("Wrote " + path.relative(ROOT, LQIP_OUT) + " (" + Object.keys(lqip).length + " entries)"); } try { const lqip = await optimizePanoramas(); await optimizeModals(); await writeLqip(lqip); console.log("Done."); } catch (err) { console.error("optimize-images failed:", err); process.exit(1); }