SPB Git

Repository Public

build-images.mts 2.8 KB 40 lines typescript

1/**2 * Photo pipeline. Reads the owner's photographs from assets/ (never published, never committed),3 * auto-orients them, STRIPS ALL METADATA (the originals carry GPS coordinates), and writes:4 *   public/images/infrastructure/<id>.jpg            master used by next/image (AVIF/WebP served on demand)5 *   public/images/infrastructure/<id>-<w>.{avif,webp} pre-built variants6 *   public/og.jpg                                     1200x630 Open Graph preview cut from a real photo7 *   data/photos.json                                  dimensions + blur placeholder + captions8 * No colour grading, retouching or generative processing is applied.9 */10import sharp from "sharp";11import { mkdir, readFile, writeFile } from "node:fs/promises";1213const OUT = "public/images/infrastructure";14const WIDTHS = [640, 1080, 1600];15await mkdir(OUT, { recursive: true });16const sources = JSON.parse(await readFile("data/photos.source.json", "utf8")) as Array<Record<string, unknown> & { id: string; source: string }>;17const photos = [];18for (const { source, ...meta } of sources) {19  const base = sharp(`assets/${source}`).rotate(); // apply EXIF orientation, then metadata is dropped on output20  const master = await base.clone().resize({ width: 2400, withoutEnlargement: true }).jpeg({ quality: 86, mozjpeg: true }).toBuffer({ resolveWithObject: true });21  await writeFile(`${OUT}/${meta.id}.jpg`, master.data);22  for (const w of WIDTHS) {23    await base.clone().resize({ width: w }).avif({ quality: 55 }).toFile(`${OUT}/${meta.id}-${w}.avif`);24    await base.clone().resize({ width: w }).webp({ quality: 74 }).toFile(`${OUT}/${meta.id}-${w}.webp`);25  }26  const blur = await base.clone().resize({ width: 16 }).jpeg({ quality: 40 }).toBuffer();27  photos.push({ ...meta, file: `/images/infrastructure/${meta.id}.jpg`, width: master.info.width, height: master.info.height, blur: `data:image/jpeg;base64,${blur.toString("base64")}` });28  const check = await sharp(`${OUT}/${meta.id}.jpg`).metadata();29  if (check.exif || check.xmp || check.iptc) throw new Error(`metadata leaked into ${meta.id}.jpg`);30}31// Open Graph image: horizontal band of the first photograph (real hardware, no overlay).32const first = sources[0];33const m = await sharp(`assets/${first.source}`).rotate().metadata();34const W = m.autoOrient?.width ?? m.width!;35const H = m.autoOrient?.height ?? m.height!;36const bandH = Math.round((W * 630) / 1200);37await sharp(`assets/${first.source}`).rotate().extract({ left: 0, top: Math.round(H * 0.22), width: W, height: bandH }).resize(1200, 630).jpeg({ quality: 84, mozjpeg: true }).toFile("public/og.jpg");38await writeFile("data/photos.json", JSON.stringify(photos, null, 2) + "\n");39console.log(`images: ${photos.length} photographs, metadata stripped`, photos.map((p) => `${p.id} ${p.width}x${p.height}`).join(", "));40
Files main
Menu

Theme