fix: resize images to 2550 px (300 DPI) at build time to prevent WSL2 OOM crash
4096×4096 source images decoded simultaneously by Chromium consumed ~3.2 GB, exhausting WSL2 RAM. Build now uses sharp to downsize to 2550×2550 (8.5 in × 300 DPI) into output/resized/ before Puppeteer loads them, cutting in-memory footprint to ~1.25 GB. Also adds all story images (01–12) to the repository. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+41
-7
@@ -1,9 +1,10 @@
|
||||
import { readdir, readFile, writeFile, access } from 'fs/promises';
|
||||
import { readdir, readFile, writeFile, access, mkdir } from 'fs/promises';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import matter from 'gray-matter';
|
||||
import { marked } from 'marked';
|
||||
import nunjucks from 'nunjucks';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dir, '..');
|
||||
@@ -11,12 +12,39 @@ const root = resolve(__dir, '..');
|
||||
// Nunjucks: load templates relative to project root
|
||||
const env = nunjucks.configure(join(root, 'templates'), { autoescape: true });
|
||||
|
||||
const PRINT_PX = 2550; // 8.5 in × 300 DPI
|
||||
|
||||
async function fileExists(path) {
|
||||
try { await access(path); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
// Resize image to PRINT_PX on the long side, write to output/resized/ tree.
|
||||
// Returns the resized file path (absolute). Skips resize if already ≤ PRINT_PX.
|
||||
async function resizeForPrint(srcAbs, resizedDir) {
|
||||
const rel = srcAbs.replace(root + '/', ''); // e.g. images/01/scene-1.png
|
||||
const dest = join(resizedDir, rel);
|
||||
const destDir = dirname(dest);
|
||||
|
||||
await mkdir(destDir, { recursive: true });
|
||||
|
||||
const meta = await sharp(srcAbs).metadata();
|
||||
const maxDim = Math.max(meta.width || 0, meta.height || 0);
|
||||
|
||||
if (maxDim <= PRINT_PX) {
|
||||
// Already small enough — copy as-is
|
||||
await sharp(srcAbs).toFile(dest);
|
||||
} else {
|
||||
await sharp(srcAbs)
|
||||
.resize(PRINT_PX, PRINT_PX, { fit: 'inside', withoutEnlargement: true })
|
||||
.png({ compressionLevel: 6 })
|
||||
.toFile(dest);
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
async function loadStories() {
|
||||
const contentDir = join(root, 'content');
|
||||
const contentDir = join(root, 'content');
|
||||
const resizedDir = join(root, 'output', 'resized');
|
||||
const files = (await readdir(contentDir)).filter(f => f.endsWith('.md')).sort();
|
||||
|
||||
const stories = [];
|
||||
@@ -27,18 +55,24 @@ async function loadStories() {
|
||||
|
||||
if (data.type === 'front-matter') continue;
|
||||
|
||||
// Split body by --- into individual scene texts
|
||||
const sceneTexts = content.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
|
||||
|
||||
const scenes = await Promise.all(
|
||||
(data.scenes || []).map(async (scene, i) => {
|
||||
const imagePath = join(root, scene.image);
|
||||
const imageExists = await fileExists(imagePath);
|
||||
const srcAbs = join(root, scene.image);
|
||||
const imageExists = await fileExists(srcAbs);
|
||||
|
||||
let imageSrc = `../${scene.image}`;
|
||||
if (imageExists) {
|
||||
await resizeForPrint(srcAbs, resizedDir);
|
||||
// Relative path from output/book.html to output/resized/
|
||||
imageSrc = `resized/${scene.image}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...scene,
|
||||
imageExists,
|
||||
// Relative path from output/book.html back to project root
|
||||
image: `../${scene.image}`,
|
||||
image: imageSrc,
|
||||
html: marked.parse(sceneTexts[i] || ''),
|
||||
};
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user