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