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
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}`);
|
|
})();
|