diff --git a/.gitignore b/.gitignore index b9d14ad..c288ba2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ .claude/ node_modules/ output/book.html -# Keep the PDF in git if you want to track versions, otherwise add: +output/cover.html +output/book-meta.json +# Interior PDF is large (~100 MB with placeholder pages) — excluded from git. +# Re-add images and re-run `pnpm all` to regenerate. output/kaleidoskop.pdf +# Cover PDF is small (~140 KB) — tracked in git. +# output/cover.pdf diff --git a/output/cover.pdf b/output/cover.pdf new file mode 100644 index 0000000..88212c9 Binary files /dev/null and b/output/cover.pdf differ diff --git a/package.json b/package.json index 02ac880..7bca284 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "build": "node scripts/build.js", "pdf": "node scripts/pdf.js", + "cover": "node scripts/cover.js", "book": "pnpm build && pnpm pdf", + "all": "pnpm build && pnpm pdf && pnpm cover", "watch": "node --watch scripts/build.js" }, "pnpm": { diff --git a/scripts/build.js b/scripts/build.js index a46022f..f4f6a1e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -64,8 +64,14 @@ async function build() { const outPath = join(root, 'output', 'book.html'); await writeFile(outPath, html, 'utf-8'); - const pageCount = stories.reduce((acc, s) => acc + s.scenes.length * 2, 0) + 5; - console.log(`Built output/book.html — ${stories.length} stories, ~${pageCount} pages`); + // title page + copyright + TOC + (4 scenes × 2 pages per story) + finale + const pageCount = 3 + stories.reduce((acc, s) => acc + s.scenes.length * 2, 0) + 1; + 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); }); diff --git a/scripts/cover.js b/scripts/cover.js new file mode 100644 index 0000000..3b42a62 --- /dev/null +++ b/scripts/cover.js @@ -0,0 +1,99 @@ +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); }); diff --git a/templates/cover.html b/templates/cover.html new file mode 100644 index 0000000..80af994 --- /dev/null +++ b/templates/cover.html @@ -0,0 +1,437 @@ + + +
+ +Zwölf magische Reisen in die Welt der Träume
++ Jede Nacht beginnt eine neue Reise – und dieses Buch hat gleich zwölf davon. + Ob gläserne Wälder, schlafende Riesen oder ein Teetassen-Boot auf einem + Fluss aus Sternenlicht: Jede Geschichte führt sanft in eine neue Zauberwelt + und begleitet dein Kind liebevoll in den Schlaf. +
+
+ Zwölf unabhängige Einschlafgeschichten · Traumhafte Aquarell-Illustrationen
+ Für Kinder von 3 bis 8 Jahren · Ideal zum Vorlesen
+
+ „Die schönsten Welten sind nur
mit geschlossenen Augen zu erreichen."
+
✦ ✦ ✦
+Zwölf magische Einschlafgeschichten
+ +· · ·
+