You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

64 lines
2.8 KiB
JavaScript

// Image pipeline: img/projects sources -> dist/assets/img/projects/<slug>/
// Variants per photo: 800w + 1600w WebP, 1200w JPEG fallback. Never upscales.
// Writes src/data/images.json manifest with final dimensions per variant.
// Usage: npm run images (re-run only when photos change; build.js just reads the manifest)
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const ROOT = path.join(__dirname, '..');
const OUT = path.join(ROOT, 'dist', 'assets', 'img');
const projects = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'projects.json'), 'utf8'));
const VARIANTS = [
{ width: 800, format: 'webp', options: { quality: 78 } },
{ width: 1600, format: 'webp', options: { quality: 74 } },
{ width: 1200, format: 'jpeg', options: { quality: 72, mozjpeg: true } },
];
async function processOne(slug, srcRel, index) {
const srcAbs = path.join(ROOT, srcRel);
if (!fs.existsSync(srcAbs)) return { srcRel, error: 'missing source' };
const dir = path.join(OUT, 'projects', slug);
fs.mkdirSync(dir, { recursive: true });
const base = sharp(srcAbs).rotate(); // respect EXIF orientation
const meta = await base.metadata();
const srcW = meta.autoOrient ? meta.autoOrient.width : meta.width;
const srcH = meta.autoOrient ? meta.autoOrient.height : meta.height;
const name = String(index + 1).padStart(2, '0');
const entry = { source: srcRel, width: srcW, height: srcH, variants: [] };
for (const v of VARIANTS) {
const w = Math.min(v.width, srcW);
const file = `${name}-${v.width}.${v.format === 'jpeg' ? 'jpg' : v.format}`;
const outPath = path.join(dir, file);
if (!fs.existsSync(outPath)) {
await base.clone().resize({ width: w, withoutEnlargement: true })[v.format](v.options).toFile(outPath);
}
const h = Math.round(srcH * (w / srcW));
entry.variants.push({ file: `assets/img/projects/${slug}/${file}`, width: w, height: h, format: v.format });
}
return entry;
}
(async () => {
const manifest = {};
let done = 0, failed = 0;
const total = projects.reduce((n, p) => n + p.images.length, 0);
for (const p of projects) {
manifest[p.slug] = [];
// limited parallelism: sharp saturates cores on its own thread pool
const queue = p.images.map((src, i) => () => processOne(p.slug, src, i));
while (queue.length) {
const batch = queue.splice(0, 4).map(fn => fn());
for (const r of await Promise.all(batch)) {
if (r.error) { failed++; console.error('MISSING:', r.source || r.srcRel); }
manifest[p.slug].push(r);
done++;
}
}
console.log(`${p.slug}: ${manifest[p.slug].length} images (${done}/${total})`);
}
fs.writeFileSync(path.join(ROOT, 'src', 'data', 'images.json'), JSON.stringify(manifest, null, 1));
console.log(`DONE. processed=${done} failed=${failed}`);
})();