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.
89 lines
5.7 KiB
JavaScript
89 lines
5.7 KiB
JavaScript
// Side-by-side review of the Neutral templates: the reference original (left) against our render (right), plus
|
|
// the multi-page, Letter and long-text samples. Run: npm run templates:compare [-- linea monogram serenity]
|
|
// Output: /tmp/invoice-c3/. With --numbers it also prints text baselines (reference vs ours) for tuning.
|
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { CANVA_DIR, loadCanvas, loadRenderer, rasterize, textItems } from "./lib.mjs";
|
|
|
|
const OUT = "/tmp/invoice-c3";
|
|
const SCALE = 1.6;
|
|
const PDF_NO = { marble: 1, highlighter: 2, monolith: 3, linea: 4, monogram: 5, serenity: 6 };
|
|
const args = process.argv.slice(2);
|
|
const numbers = args.includes("--numbers");
|
|
const ids = args.filter((a) => !a.startsWith("--"));
|
|
const targets = ids.length ? ids : ["linea", "monogram", "serenity"];
|
|
|
|
const LONG = "Unit 4, Third Floor, The Grand Heritage Business Centre, Plot 1187-B, Old Airport Road Extension, Opposite the Municipal Corporation Ward Office, Domlur Second Stage, Bengaluru Urban District, Karnataka State, India - 560071; Registered office: Level 12, Tower C, Global Technology Park, Outer Ring Road, Marathahalli, Bengaluru - 560103";
|
|
const TOKEN = "Supercalifragilisticexpialidocious-Pneumonoultramicroscopic-1234567890";
|
|
|
|
async function main() {
|
|
mkdirSync(OUT, { recursive: true });
|
|
const { createCanvas } = await loadCanvas();
|
|
const r = await loadRenderer();
|
|
r.ensureSetup();
|
|
const side = async (left, right, file) => {
|
|
const gap = 12;
|
|
const c = createCanvas(left.width + right.width + gap, Math.max(left.height, right.height));
|
|
const ctx = c.getContext("2d");
|
|
ctx.fillStyle = "#888888";
|
|
ctx.fillRect(0, 0, c.width, c.height);
|
|
ctx.drawImage(left, 0, 0);
|
|
ctx.drawImage(right, left.width + gap, 0);
|
|
writeFileSync(path.join(OUT, file), c.toBuffer("image/png"));
|
|
};
|
|
|
|
for (const id of targets) {
|
|
const no = PDF_NO[id];
|
|
const canvaBytes = readFileSync(path.join(CANVA_DIR, `${no}.pdf`));
|
|
const reference = await rasterize(canvaBytes, 1, SCALE);
|
|
const rows = id === "linea" ? 7 : 5;
|
|
const NAMES = ["Brand consultation", "Logo design", "Website design", "Social media templates", "Brand photography", "Brand guide", "Social media templates"];
|
|
// Short one-line descriptions like the reference sample, so positions can be compared row by row.
|
|
const items = (hsn) => NAMES.slice(0, rows).map((description, i) => ({ id: i + 1, description, mode: "rate", rate: 100, unit: "hour", quantity: 1, amount: 0, sortOrder: i, ...(hsn ? { hsnSac: "998397" } : {}) }));
|
|
const sample = (over = {}) => ({ name: `s-${id}`, items: items(over.registered !== false), signature: true, invoice: { poNumber: "", notes: "Payment due within 30 days.\nLate payments attract 2% monthly interest." }, ...over });
|
|
const render = async (spec, size = "A4") => {
|
|
const prefs = r.prefsFor(size, { templateId: id });
|
|
return r.renderFixture(spec, prefs);
|
|
};
|
|
|
|
const withLogo = await render(sample({ logo: "real" }));
|
|
await side(reference, await rasterize(withLogo.result.bytes, 1, SCALE), `compare-${id}.png`);
|
|
const noLogo = await render(sample({ registered: false, logo: "none" }));
|
|
await side(reference, await rasterize(noLogo.result.bytes, 1, SCALE), `compare-${id}-nologo.png`);
|
|
if (numbers) {
|
|
const a = await textItems(canvaBytes);
|
|
const b = await textItems(withLogo.result.bytes);
|
|
console.log(`--- ${id}: reference (${a.length}) vs ours (${b.length}); baseline y, x`);
|
|
for (const it of a) console.log(` reference ${String(it.baseline).padStart(6)} ${String(it.x).padStart(6)} w${String(it.w).padStart(6)} s${it.size} ${it.str.slice(0, 40)}`);
|
|
for (const it of b) console.log(` ours ${String(it.baseline).padStart(6)} ${String(it.x).padStart(6)} w${String(it.w).padStart(6)} s${it.size} ${it.str.slice(0, 40)}`);
|
|
}
|
|
|
|
if (id === "linea") {
|
|
// The opt-in logo band (off by default so Linea keeps matching format.pdf).
|
|
const shown = await r.renderFixture(sample({ logo: "real" }), r.prefsFor("A4", { templateId: id, logo: { show: true } }));
|
|
writeFileSync(path.join(OUT, `${id}-A4-logo-optin.png`), (await rasterize(shown.result.bytes, 1, SCALE)).toBuffer("image/png"));
|
|
}
|
|
for (const logo of ["square", "wide"]) {
|
|
const x = await render(sample({ logo }));
|
|
writeFileSync(path.join(OUT, `${id}-A4-logo-${logo}.png`), (await rasterize(x.result.bytes, 1, SCALE)).toBuffer("image/png"));
|
|
}
|
|
const sixty = await render({ name: "s60", items: 60, signature: true });
|
|
for (let p = 1; p <= Math.min(2, sixty.result.pages); p++) writeFileSync(path.join(OUT, `${id}-A4-60rows-p${p}.png`), (await rasterize(sixty.result.bytes, p, SCALE)).toBuffer("image/png"));
|
|
const letter = await render(sample({ logo: "real" }), "LETTER");
|
|
writeFileSync(path.join(OUT, `${id}-Letter-tax.png`), (await rasterize(letter.result.bytes, 1, SCALE)).toBuffer("image/png"));
|
|
const long = await render({
|
|
name: "long",
|
|
items: 4,
|
|
signature: true,
|
|
logo: "real",
|
|
vendor: { vendorAddress: LONG, vendorEmail: `${TOKEN}@example-studios-india.co.in`, vendorName: TOKEN },
|
|
invoice: { clientName: TOKEN, clientAddress: LONG, notes: `Note: ${TOKEN} ${LONG}` },
|
|
});
|
|
writeFileSync(path.join(OUT, `${id}-A4-longtext.png`), (await rasterize(long.result.bytes, 1, SCALE)).toBuffer("image/png"));
|
|
console.log(`${id}: pages ${withLogo.result.pages}/${sixty.result.pages}/${letter.result.pages}/${long.result.pages}; audit issues`, [withLogo, noLogo, sixty, letter, long].map((x) => x.result.issues.length).join("/"));
|
|
for (const x of [withLogo, noLogo, sixty, letter, long]) for (const i of x.result.issues) console.log(" ", i.severity, i.code, `p${i.page}`, i.message.slice(0, 160));
|
|
}
|
|
}
|
|
|
|
await main();
|