// Prints a /previz page from the static export to a PDF next to its assets. // npm run build && node scripts/previz-pdf.mjs crowd // Drives Edge/Chrome over the DevTools protocol instead of --print-to-pdf: Chrome sizes the // print layout from the paper it is given, not from CSS @page, so the paper (A4 landscape) // has to be passed here for the desktop layout to kick in. import { spawn } from "node:child_process"; import { createReadStream, existsSync, mkdtempSync, statSync, writeFileSync } from "node:fs"; import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { extname, join, resolve } from "node:path"; const slug = process.argv[2] ?? "crowd"; const out = resolve("out"); const dest = resolve("public/previz", slug, `${slug}-previz.pdf`); const browser = process.env.CHROME ?? [ "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "C:/Program Files/Google/Chrome/Application/chrome.exe", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/usr/bin/google-chrome", ].find(existsSync); if (!browser) throw new Error("No Chrome/Edge found; set CHROME=/path/to/browser"); if (!existsSync(join(out, "previz", slug, "index.html"))) throw new Error("Run `npm run build` first"); const types = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".jpg": "image/jpeg", ".png": "image/png", ".svg": "image/svg+xml", ".mp4": "video/mp4", ".woff2": "font/woff2", ".txt": "text/plain" }; const server = createServer((req, res) => { let file = join(out, decodeURIComponent(new URL(req.url, "http://x").pathname)); if (existsSync(file) && statSync(file).isDirectory()) file = join(file, "index.html"); if (!file.startsWith(out) || !existsSync(file)) return res.writeHead(404).end(); res.writeHead(200, { "Content-Type": types[extname(file)] ?? "application/octet-stream" }); createReadStream(file).pipe(res); }).listen(0); const port = server.address().port; const proc = spawn(browser, [ "--headless=new", "--disable-gpu", "--remote-debugging-port=0", `--user-data-dir=${mkdtempSync(join(tmpdir(), "previz-pdf-"))}`, "about:blank", ]); const wsUrl = await new Promise((ok, fail) => { let buf = ""; proc.stderr.on("data", (d) => { buf += d; const m = buf.match(/DevTools listening on (ws:\/\/\S+)/); if (m) ok(m[1]); }); proc.on("exit", () => fail(new Error("Browser exited early"))); }); // Attach to the blank tab and talk to it const targets = await (await fetch(wsUrl.replace("ws://", "http://").replace(/\/devtools\/browser\/.*/, "/json"))).json(); const ws = new WebSocket(targets.find((t) => t.type === "page").webSocketDebuggerUrl); await new Promise((ok) => ws.addEventListener("open", ok, { once: true })); let id = 0; const pending = new Map(); const events = []; ws.addEventListener("message", ({ data }) => { const msg = JSON.parse(data); if (msg.id && pending.has(msg.id)) { const { ok, fail } = pending.get(msg.id); pending.delete(msg.id); if (msg.error) fail(new Error(msg.error.message)); else ok(msg.result); } else if (msg.method) events.push(msg.method); }); const send = (method, params = {}) => new Promise((ok, fail) => { pending.set(++id, { ok, fail }); ws.send(JSON.stringify({ id, method, params })); }); try { await send("Page.enable"); await send("Page.navigate", { url: `http://localhost:${port}/previz/${slug}/` }); while (!events.includes("Page.loadEventFired")) await new Promise((r) => setTimeout(r, 100)); // Lazy images and fonts: load everything, then give layout a beat await send("Runtime.evaluate", { expression: `Promise.all([document.fonts.ready, ...[...document.images].map(i => (i.loading = "eager", i.decode().catch(() => {})))])`, awaitPromise: true, }); await new Promise((r) => setTimeout(r, 500)); const { data } = await send("Page.printToPDF", { landscape: true, paperWidth: 8.27, // A4, inches (landscape swaps them) paperHeight: 11.69, marginTop: 0, marginBottom: 0, marginLeft: 0, marginRight: 0, printBackground: true, scale: 0.68, // lays out ~1650px wide: each section fits a page }); writeFileSync(dest, Buffer.from(data, "base64")); console.log(`Wrote ${dest} (${Math.round(Buffer.byteLength(data, "base64") / 1024)} KB)`); } finally { ws.close(); proc.kill(); server.close(); }