cv-2026/scripts/generate-pdf.mjs

104 lines
2.8 KiB
JavaScript

import puppeteer from 'puppeteer';
import { resolve, basename, dirname, extname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
import { createServer } from 'http';
import { readFile } from 'fs/promises';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolve(__dirname, '..');
const OUTPUT_DIR = resolve(PROJECT_ROOT, 'output/pdf');
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
};
async function generatePdf(htmlPath, outDirOverride) {
const absolutePath = resolve(htmlPath);
if (!existsSync(absolutePath)) {
console.error(`File not found: ${absolutePath}`);
process.exit(1);
}
const outDir = outDirOverride ? resolve(outDirOverride) : OUTPUT_DIR;
mkdirSync(outDir, { recursive: true });
const pdfName = basename(absolutePath, '.html') + '.pdf';
const pdfPath = resolve(outDir, pdfName);
// Serve the project root so the HTML can load /templates/cv-style.css and any other assets.
const server = createServer(async (req, res) => {
const filePath = resolve(
PROJECT_ROOT,
decodeURIComponent(req.url).replace(/^\//, ''),
);
try {
const data = await readFile(filePath);
const ext = extname(filePath);
res.writeHead(200, {
'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
});
res.end(data);
} catch {
res.writeHead(404);
res.end('Not found');
}
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
const relPath = absolutePath.replace(PROJECT_ROOT + '/', '');
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox'],
});
const page = await browser.newPage();
await page.goto(`http://127.0.0.1:${port}/${relPath}`, {
waitUntil: 'networkidle0',
timeout: 30000,
});
// Wait for web fonts (if any) to be ready before rasterizing.
await page.evaluate(async () => {
if (document.fonts && document.fonts.ready) {
await document.fonts.ready;
}
});
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
preferCSSPageSize: true,
});
await browser.close();
server.close();
console.log(`PDF generated: ${pdfPath}`);
return pdfPath;
}
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node generate-pdf.mjs <html-file> [out-dir]');
process.exit(1);
}
const [htmlFile, outDir] = args;
generatePdf(htmlFile, outDir);