Linea (the vendor's original format.pdf design), Monogram and Serenity are rebuilt from the reference PDFs as one parametric family on the engine, with the user's logo in the measured slot, a home for every GST field and A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and a generated Ms Madi outline for Serenity's vertical word. Text-only logo placeholders (Monolith, Tangerine) use a doc-derived allowance box. A Carbon TemplatePicker with committed thumbnails opens from the page setup controls.
83 lines
3.3 KiB
JavaScript
83 lines
3.3 KiB
JavaScript
// 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();
|
|
}
|
|
}
|