import puppeteer from 'puppeteer'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { access, stat, readFile, writeFile, rename, unlink } from 'fs/promises'; import { execFile } from 'child_process'; import { promisify } from 'util'; import matter from 'gray-matter'; const execFileAsync = promisify(execFile); const __dir = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dir, '..'); const inputPath = resolve(root, 'output', 'book.html'); const rawPath = resolve(root, 'output', 'kaleidoskop-raw.pdf'); const marksPath = resolve(root, 'output', 'pdfmarks.ps'); const outputPath = resolve(root, 'output', 'kaleidoskop.pdf'); async function fileExists(path) { try { await access(path); return true; } catch { return false; } } // Encode a JS string as UTF-16BE hex for PostScript pdfmarks (handles umlauts etc.) function toUtf16BeHex(str) { const hex = []; for (let i = 0; i < str.length; i++) { const cp = str.charCodeAt(i); hex.push(((cp >> 8) & 0xff).toString(16).padStart(2, '0')); hex.push((cp & 0xff).toString(16).padStart(2, '0')); } return 'FEFF' + hex.join('').toUpperCase(); } function buildPdfmarks({ title, author, subject }) { return [ '[ /Title <' + toUtf16BeHex(title) + '>', ' /Author <' + toUtf16BeHex(author) + '>', ' /Subject <' + toUtf16BeHex(subject) + '>', ' /Creator (Puppeteer + Ghostscript)', ' /Producer (kaleidoskop build pipeline)', ' /DOCINFO pdfmark', ].join('\n'); } async function runGhostscript({ title, author, subject }) { const marks = buildPdfmarks({ title, author, subject }); await writeFile(marksPath, marks, 'utf-8'); const gs = 'gs'; const args = [ '-dBATCH', '-dNOPAUSE', '-dQUIET', '-sDEVICE=pdfwrite', '-dCompatibilityLevel=1.4', '-dPDFSETTINGS=/prepress', '-dEmbedAllFonts=true', '-dSubsetFonts=false', '-dDownsampleColorImages=false', '-dDownsampleGrayImages=false', '-dDownsampleMonoImages=false', `-sOutputFile=${outputPath}`, marksPath, rawPath, ]; try { await execFileAsync(gs, args); } catch (err) { console.error('Ghostscript failed:', err.message); // Fall back to raw PDF so the build still produces output await rename(rawPath, outputPath); console.warn('Falling back to raw Puppeteer PDF (no GS post-processing).'); return false; } return true; } async function generate() { try { await access(inputPath); } catch { console.error('output/book.html not found — run `pnpm build` first'); process.exit(1); } // Read metadata from front-matter for PDF info dict const fmRaw = await readFile(resolve(root, 'content', '00-front-matter.md'), 'utf-8'); const { data: fm } = matter(fmRaw); const title = fm.title || 'Das Kaleidoskop der Schlummerwelten'; const author = fm.author || ''; const subject = fm.subtitle || 'Kinderbuch'; 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://${inputPath}`, { waitUntil: 'networkidle0' }); // Write to temp file first so Ghostscript can read it await page.pdf({ path: rawPath, width: '8.75in', height: '8.75in', printBackground: true, margin: { top: 0, bottom: 0, left: 0, right: 0 }, }); await browser.close(); const rawSize = (await stat(rawPath)).size; console.log(`Raw PDF: ${(rawSize / 1_048_576).toFixed(1)} MB`); console.log('Running Ghostscript (font embedding + metadata)…'); const gsOk = await runGhostscript({ title, author, subject }); if (gsOk) { const finalSize = (await stat(outputPath)).size; console.log(`Final PDF: ${(finalSize / 1_048_576).toFixed(1)} MB`); await unlink(rawPath).catch(() => {}); await unlink(marksPath).catch(() => {}); console.log('PDF written to output/kaleidoskop.pdf (fonts embedded, metadata set)'); } } generate().catch(err => { console.error(err); process.exit(1); });