Files
valknar 475bbf0a19 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>
2026-05-04 21:37:56 +02:00

111 lines
3.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, '..');
// 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 resizedDir = join(root, 'output', 'resized');
const files = (await readdir(contentDir)).filter(f => f.endsWith('.md')).sort();
const stories = [];
for (const file of files) {
const raw = await readFile(join(contentDir, file), 'utf-8');
const { data, content } = matter(raw);
if (data.type === 'front-matter') continue;
const sceneTexts = content.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
const scenes = await Promise.all(
(data.scenes || []).map(async (scene, i) => {
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,
image: imageSrc,
html: marked.parse(sceneTexts[i] || ''),
};
})
);
stories.push({ ...data, scenes });
}
return stories;
}
async function loadFrontMatter() {
const raw = await readFile(join(root, 'content', '00-front-matter.md'), 'utf-8');
return matter(raw).data;
}
async function build() {
const [stories, frontMatter] = await Promise.all([loadStories(), loadFrontMatter()]);
const html = env.render('book.html', { stories, frontMatter });
const outPath = join(root, 'output', 'book.html');
await writeFile(outPath, html, 'utf-8');
// imprint + title page + TOC + (4 scenes × 2 pages per story)
const pageCount = 3 + stories.reduce((acc, s) => acc + s.scenes.length * 2, 0);
await writeFile(
join(root, 'output', 'book-meta.json'),
JSON.stringify({ pageCount, storyCount: stories.length, builtAt: new Date().toISOString() }, null, 2),
'utf-8'
);
console.log(`Built output/book.html — ${stories.length} stories, ${pageCount} pages`);
}
build().catch(err => { console.error(err); process.exit(1); });