// Shared by the template scripts (thumbnails, compare): bundles the TypeScript renderer for node and // rasterises PDFs with pdf.js + @napi-rs/canvas. import { build } from "esbuild"; import { mkdirSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { ROOT } from "../logo/lib.mjs"; export { ROOT }; export const CANVA_DIR = path.join(ROOT, "templates", "reference"); /** * Bundles scripts/templates/entry.ts into node_modules/.cache (so bare imports such as react resolve from * node_modules) and imports it. Node packages stay external; only the app's own sources are bundled. */ export async function loadRenderer() { const dir = path.join(ROOT, "node_modules", ".cache", "voiced-templates"); mkdirSync(dir, { recursive: true }); const outfile = path.join(dir, `bundle-${Date.now()}.mjs`); await build({ entryPoints: [path.join(ROOT, "scripts/templates/entry.ts")], outfile, bundle: true, packages: "external", platform: "node", format: "esm", jsx: "automatic", logLevel: "error", }); return import(pathToFileURL(outfile).href); } export async function loadCanvas() { try { return await import("@napi-rs/canvas"); } catch { console.error("@napi-rs/canvas is not installed (it is an optional dev dependency); run npm install"); process.exit(1); } } /** Rasterises page `pageNo` of PDF bytes at `scale` on a white background; returns the canvas. */ export async function rasterize(bytes, pageNo = 1, scale = 1.6) { const { createCanvas } = await loadCanvas(); const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); const task = pdfjs.getDocument({ data: new Uint8Array(bytes), useSystemFonts: false, disableFontFace: true, isEvalSupported: false, verbosity: 0 }); const doc = await task.promise; try { const page = await doc.getPage(pageNo); const vp = page.getViewport({ scale }); const canvas = createCanvas(Math.ceil(vp.width), Math.ceil(vp.height)); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, canvas.width, canvas.height); await page.render({ canvasContext: ctx, canvas, viewport: vp }).promise; return canvas; } finally { await task.destroy(); } } /** Text items of page 1 with baselines measured from the page top, for numeric comparison. */ export async function textItems(bytes, pageNo = 1) { const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); const task = pdfjs.getDocument({ data: new Uint8Array(bytes), useSystemFonts: false, disableFontFace: true, isEvalSupported: false, verbosity: 0 }); const doc = await task.promise; try { const page = await doc.getPage(pageNo); const vp = page.getViewport({ scale: 1 }); const content = await page.getTextContent(); return content.items .filter((it) => "str" in it && it.str.trim() !== "") .map((it) => { const m = vp.transform; const [px, py] = [it.transform[4], it.transform[5]]; const [x, y] = [m[0] * px + m[2] * py + m[4], m[1] * px + m[3] * py + m[5]]; return { str: it.str, x: Math.round(x * 10) / 10, baseline: Math.round(y * 10) / 10, w: Math.round(it.width * 10) / 10, size: Math.round(Math.hypot(it.transform[0], it.transform[1]) * 10) / 10 }; }); } finally { await task.destroy(); } }