Add the Neutral template family, template picker and text-only logo allowances
Linea (the vendor's original format.pdf design), Monogram and Serenity are rebuilt from the Canva 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.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// 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", "Modern Neutral Invoice Template");
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user