add: script to generate thumbnails
All checks were successful
Deploy on push / deploy (push) Has been skipped

This commit is contained in:
2026-03-22 19:54:53 -07:00
parent c5f525bb03
commit 3439fc834f
7 changed files with 843 additions and 33 deletions

View File

@@ -0,0 +1,93 @@
const fs = require("fs");
const path = require("path");
const sharp = require("sharp");
const { loadMeals, repoRoot } = require("./lib/meals");
const fullsDir = path.join(repoRoot, "images", "fulls");
const thumbsDir = path.join(repoRoot, "images", "thumbs");
const THUMB_WIDTH = 240;
const THUMB_HEIGHT = 320;
const JPEG_QUALITY = 82;
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getThumbnailPaths(meal) {
return {
fullPath: path.join(fullsDir, `${meal.id}.jpg`),
thumbPath: path.join(thumbsDir, `${meal.id}.jpg`),
};
}
function getCropArea(width, height, focus) {
const targetRatio = THUMB_WIDTH / THUMB_HEIGHT;
const sourceRatio = width / height;
let cropWidth = width;
let cropHeight = height;
if (sourceRatio > targetRatio) {
cropWidth = Math.round(height * targetRatio);
} else if (sourceRatio < targetRatio) {
cropHeight = Math.round(width / targetRatio);
}
const centerX = (focus?.x ?? 0.5) * width;
const centerY = (focus?.y ?? 0.5) * height;
const left = Math.round(centerX - cropWidth / 2);
const top = Math.round(centerY - cropHeight / 2);
return {
left: clamp(left, 0, width - cropWidth),
top: clamp(top, 0, height - cropHeight),
width: cropWidth,
height: cropHeight,
};
}
async function generateThumbnail(meal) {
const { fullPath, thumbPath } = getThumbnailPaths(meal);
if (!fs.existsSync(fullPath)) {
throw new Error(`Missing full-size image for meal ${meal.id}: ${fullPath}`);
}
const image = sharp(fullPath);
const metadata = await image.metadata();
if (!metadata.width || !metadata.height) {
throw new Error(`Could not read image dimensions for ${fullPath}`);
}
const cropArea = getCropArea(
metadata.width,
metadata.height,
meal.thumbnail?.focus
);
await image
.extract(cropArea)
.resize(THUMB_WIDTH, THUMB_HEIGHT)
.jpeg({ quality: JPEG_QUALITY, mozjpeg: true })
.toFile(thumbPath);
}
async function main() {
const meals = loadMeals();
await fs.promises.mkdir(thumbsDir, { recursive: true });
for (const meal of meals) {
await generateThumbnail(meal);
}
console.log(`Generated ${meals.length} thumbnails in images/thumbs`);
}
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});