feat: script nén ảnh panorama + LQIP
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// 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<string, string> = {\n" +
|
||||
entries +
|
||||
"\n};\n";
|
||||
await fs.writeFile(LQIP_OUT, content);
|
||||
console.log("Wrote " + path.relative(ROOT, LQIP_OUT) + " (" + Object.keys(lqip).length + " entries)");
|
||||
}
|
||||
|
||||
const lqip = await optimizePanoramas();
|
||||
await optimizeModals();
|
||||
await writeLqip(lqip);
|
||||
console.log("Done.");
|
||||
Reference in New Issue
Block a user