Files
kaleidoskop/scripts/cover.js
T
valknar 1037b84eaa Add KDP cover PDF generator
scripts/cover.js computes the exact cover canvas dimensions from the
page count in book-meta.json (written by build.js), using the KDP
Premium Color spine formula (0.002347 in/page), and renders a
Nunjucks template to a single PDF containing back cover, spine, and
front cover with bleed (0.125 in) and safe-zone overlay guides.

- `pnpm cover`  — generate output/cover.pdf
- `pnpm all`    — build interior + both PDFs in one command
- Cover artwork slots: images/cover/front.png, images/cover/back.png

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:29:10 +02:00

100 lines
3.6 KiB
JavaScript
Raw 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 puppeteer from 'puppeteer';
import { readFile, writeFile, access } from 'fs/promises';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import nunjucks from 'nunjucks';
const __dir = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dir, '..');
// KDP constants — Premium Color paper, 8.5×8.5 in trim
const BLEED_IN = 0.125;
const TRIM_W_IN = 8.5;
const TRIM_H_IN = 8.5;
const SPINE_PER_PAGE = 0.002347; // inches per page, Premium Color
const SAFE_IN = 0.125; // safe zone inset from trim edge (front/back)
const SPINE_SAFE_IN = 0.0625; // safe zone inset from spine fold (each side)
const MIN_SPINE_TEXT = 79; // KDP minimum pages for spine text
async function fileExists(p) {
try { await access(p); return true; } catch { return false; }
}
async function generate() {
// Read page count produced by build.js
const metaPath = resolve(root, 'output', 'book-meta.json');
let pageCount = 100;
if (await fileExists(metaPath)) {
const meta = JSON.parse(await readFile(metaPath, 'utf-8'));
pageCount = meta.pageCount;
console.log(`Page count: ${pageCount} (from output/book-meta.json)`);
} else {
console.warn(`book-meta.json not found — using default page count ${pageCount}`);
console.warn('Run `pnpm build` first for an accurate spine width.\n');
}
// Compute dimensions
const spineWidth = pageCount * SPINE_PER_PAGE;
const totalWidth = BLEED_IN + TRIM_W_IN + spineWidth + TRIM_W_IN + BLEED_IN;
const totalHeight = BLEED_IN + TRIM_H_IN + BLEED_IN;
console.log(`Spine width : ${spineWidth.toFixed(4)} in`);
console.log(`Cover canvas: ${totalWidth.toFixed(4)} × ${totalHeight.toFixed(4)} in\n`);
// Check for optional artwork files
const frontImage = await fileExists(resolve(root, 'images/cover/front.png'))
? '../images/cover/front.png' : null;
const backImage = await fileExists(resolve(root, 'images/cover/back.png'))
? '../images/cover/back.png' : null;
// Render HTML from template
const env = nunjucks.configure(resolve(root, 'templates'), { autoescape: false });
const html = env.render('cover.html', {
pageCount,
spineWidth: spineWidth.toFixed(4),
totalWidth: totalWidth.toFixed(4),
totalHeight: totalHeight.toFixed(4),
bleed: BLEED_IN,
trimW: TRIM_W_IN,
trimH: TRIM_H_IN,
safe: SAFE_IN,
spineSafe: SPINE_SAFE_IN,
hasSpineText: pageCount >= MIN_SPINE_TEXT,
frontImage,
backImage,
});
const htmlOut = resolve(root, 'output', 'cover.html');
await writeFile(htmlOut, html, 'utf-8');
// Generate PDF
const systemChromium = '/usr/bin/chromium';
const useSystem = await fileExists(systemChromium);
console.log(`Launching Puppeteer (${useSystem ? 'system Chromium' : 'bundled Chrome'})…`);
const browser = await puppeteer.launch({
headless: true,
...(useSystem ? { executablePath: systemChromium } : {}),
});
const page = await browser.newPage();
await page.goto(`file://${htmlOut}`, { waitUntil: 'networkidle0' });
const pdfOut = resolve(root, 'output', 'cover.pdf');
await page.pdf({
path: pdfOut,
width: `${totalWidth}in`,
height: `${totalHeight}in`,
printBackground: true,
margin: { top: 0, bottom: 0, left: 0, right: 0 },
});
await browser.close();
console.log('Cover PDF written to output/cover.pdf');
console.log('\nNote: KDP recommends CMYK for covers. This PDF is RGB.');
console.log('Convert with Adobe Acrobat or Ghostscript before upload if needed.');
}
generate().catch(err => { console.error(err); process.exit(1); });