Files

140 lines
4.6 KiB
JavaScript
Raw Permalink Normal View History

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();
// Large illustrated books with many local images need generous timeouts
page.setDefaultNavigationTimeout(120_000);
page.setDefaultTimeout(120_000);
await page.goto(`file://${inputPath}`, { waitUntil: 'load', timeout: 120_000 });
// Wait until every <img> has finished decoding (handles large local images)
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalHeight > 0),
{ timeout: 120_000 },
);
// 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 },
timeout: 120_000,
});
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); });