diff --git a/benchmarks/comparison.bench.ts b/benchmarks/comparison.bench.ts index 355c33d..58c1e56 100644 --- a/benchmarks/comparison.bench.ts +++ b/benchmarks/comparison.bench.ts @@ -1,20 +1,28 @@ /** * Library comparison benchmarks. * - * Compares @libpdf/core against pdf-lib for overlapping operations. + * Compares @libpdf/core against pdf-lib and @cantoo/pdf-lib for overlapping operations. * Results are machine-dependent and should be used for relative comparison only. */ +import { PDFDocument as CantooPDFDocument } from "@cantoo/pdf-lib"; import { PDFDocument } from "pdf-lib"; import { bench, describe } from "vitest"; import { PDF } from "../src"; -import { getHeavyPdf, getSynthetic100, getSynthetic2000, loadFixture } from "./fixtures"; +import { + fintracPdfPath, + getHeavyPdf, + getSynthetic100, + getSynthetic2000, + loadFixture, +} from "./fixtures"; // Pre-load fixtures const pdfBytes = await getHeavyPdf(); const synthetic100 = await getSynthetic100(); const synthetic2000 = await getSynthetic2000(); +const fintracBytes = await loadFixture(fintracPdfPath); describe("Load PDF", () => { bench("libpdf", async () => { @@ -24,6 +32,10 @@ describe("Load PDF", () => { bench("pdf-lib", async () => { await PDFDocument.load(pdfBytes); }); + + bench("@cantoo/pdf-lib", async () => { + await CantooPDFDocument.load(pdfBytes); + }); }); describe("Create blank PDF", () => { @@ -36,6 +48,11 @@ describe("Create blank PDF", () => { const pdf = await PDFDocument.create(); await pdf.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.create(); + await pdf.save(); + }); }); describe("Add 10 pages", () => { @@ -58,6 +75,16 @@ describe("Add 10 pages", () => { await pdf.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.create(); + + for (let i = 0; i < 10; i++) { + pdf.addPage(); + } + + await pdf.save(); + }); }); describe("Draw 50 rectangles", () => { @@ -92,6 +119,22 @@ describe("Draw 50 rectangles", () => { await pdf.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.create(); + const page = pdf.addPage(); + + for (let i = 0; i < 50; i++) { + page.drawRectangle({ + x: 50 + (i % 5) * 100, + y: 50 + Math.floor(i / 5) * 70, + width: 80, + height: 50, + }); + } + + await pdf.save(); + }); }); describe("Load and save PDF", () => { @@ -104,6 +147,11 @@ describe("Load and save PDF", () => { const pdf = await PDFDocument.load(pdfBytes); await pdf.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.load(pdfBytes); + await pdf.save(); + }); }); describe("Load, modify, and save PDF", () => { @@ -120,6 +168,13 @@ describe("Load, modify, and save PDF", () => { page.drawRectangle({ x: 50, y: 50, width: 100, height: 100 }); await pdf.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.load(pdfBytes); + const page = pdf.getPage(0); + page.drawRectangle({ x: 50, y: 50, width: 100, height: 100 }); + await pdf.save(); + }); }); // ───────────────────────────────────────────────────────────────────────────── @@ -140,6 +195,14 @@ describe("Extract single page from 100-page PDF", () => { newDoc.addPage(page); await newDoc.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.load(synthetic100); + const newDoc = await CantooPDFDocument.create(); + const [page] = await newDoc.copyPages(pdf, [0]); + newDoc.addPage(page); + await newDoc.save(); + }); }); describe("Split 100-page PDF into single-page PDFs", () => { @@ -172,6 +235,22 @@ describe("Split 100-page PDF into single-page PDFs", () => { }, { warmupIterations: 1, iterations: 3 }, ); + + bench( + "@cantoo/pdf-lib", + async () => { + const pdf = await CantooPDFDocument.load(synthetic100); + const pageCount = pdf.getPageCount(); + + for (let i = 0; i < pageCount; i++) { + const newDoc = await CantooPDFDocument.create(); + const [page] = await newDoc.copyPages(pdf, [i]); + newDoc.addPage(page); + await newDoc.save(); + } + }, + { warmupIterations: 1, iterations: 3 }, + ); }); describe(`Split 2000-page PDF into single-page PDFs (${(synthetic2000.length / 1024 / 1024).toFixed(1)}MB)`, () => { @@ -204,6 +283,22 @@ describe(`Split 2000-page PDF into single-page PDFs (${(synthetic2000.length / 1 }, { warmupIterations: 0, iterations: 1, time: 0 }, ); + + bench( + "@cantoo/pdf-lib", + async () => { + const pdf = await CantooPDFDocument.load(synthetic2000); + const pageCount = pdf.getPageCount(); + + for (let i = 0; i < pageCount; i++) { + const newDoc = await CantooPDFDocument.create(); + const [page] = await newDoc.copyPages(pdf, [i]); + newDoc.addPage(page); + await newDoc.save(); + } + }, + { warmupIterations: 0, iterations: 1, time: 0 }, + ); }); describe("Copy 10 pages between documents", () => { @@ -227,6 +322,19 @@ describe("Copy 10 pages between documents", () => { await dest.save(); }); + + bench("@cantoo/pdf-lib", async () => { + const source = await CantooPDFDocument.load(synthetic100); + const dest = await CantooPDFDocument.create(); + const indices = Array.from({ length: 10 }, (_, i) => i); + const pages = await dest.copyPages(source, indices); + + for (const page of pages) { + dest.addPage(page); + } + + await dest.save(); + }); }); describe("Merge 2 x 100-page PDFs", () => { @@ -262,4 +370,181 @@ describe("Merge 2 x 100-page PDFs", () => { }, { warmupIterations: 1, iterations: 3 }, ); + + bench( + "@cantoo/pdf-lib", + async () => { + const doc1 = await CantooPDFDocument.load(synthetic100); + const doc2 = await CantooPDFDocument.load(synthetic100); + const merged = await CantooPDFDocument.create(); + + const pages1 = await merged.copyPages(doc1, doc1.getPageIndices()); + + for (const page of pages1) { + merged.addPage(page); + } + + const pages2 = await merged.copyPages(doc2, doc2.getPageIndices()); + + for (const page of pages2) { + merged.addPage(page); + } + + await merged.save(); + }, + { warmupIterations: 1, iterations: 3 }, + ); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Form filling comparison (FINTRAC - CID font PDF with stripped glyph outlines) +// +// This PDF uses a Type0/Identity-H CID font whose glyph outlines have been +// stripped, forcing the appearance generator to fall back to Helvetica. +// ───────────────────────────────────────────────────────────────────────────── + +/** Field values used for the FINTRAC form. */ +const fintracFields = { + transaction: "123 main st", + realtor: "No one", + date: "2026-02-02", + full_name: "John Doe", + client_address: "123 Any Street, Toronto, ON, M0M 0M0", + date_of_birth: "1968-09-05", + nature_of_business: "Software Development", + id_number: "D6101-40706-60905", + issuing_authority: "Ontario", + issuing_country: "Canada", + expiry_date: "2012-11-26", +}; + +/** Checkbox fields for the FINTRAC form. */ +const fintracCheckboxes = { + driverslicense_button: true, + passport_button: false, + third_party_no_button: true, + question_1_yes: true, + question_2_no: true, + question_3_no: true, + question_4_no: true, + question_5_yes: true, + relationship_nature_residential: true, +}; + +describe("Fill FINTRAC form fields", () => { + bench("libpdf", async () => { + const pdf = await PDF.load(fintracBytes); + const form = pdf.getForm()!; + + form.fill({ + ...fintracFields, + ...fintracCheckboxes, + }); + + await pdf.save(); + }); + + bench("pdf-lib", async () => { + const pdf = await PDFDocument.load(fintracBytes); + const form = pdf.getForm(); + + for (const [name, value] of Object.entries(fintracFields)) { + form.getTextField(name).setText(value); + } + + for (const [name, value] of Object.entries(fintracCheckboxes)) { + const cb = form.getCheckBox(name); + + if (value) { + cb.check(); + } else { + cb.uncheck(); + } + } + + await pdf.save(); + }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.load(fintracBytes); + const form = pdf.getForm(); + + for (const [name, value] of Object.entries(fintracFields)) { + form.getTextField(name).setText(value); + } + + for (const [name, value] of Object.entries(fintracCheckboxes)) { + const cb = form.getCheckBox(name); + + if (value) { + cb.check(); + } else { + cb.uncheck(); + } + } + + await pdf.save(); + }); +}); + +// NOTE: pdf-lib and @cantoo/pdf-lib log errors to stderr during flatten on this +// PDF because they can't resolve widget page refs. The benchmarks still complete +// and produce valid timing data — the noise is expected. +describe("Fill and flatten FINTRAC form", () => { + bench("libpdf", async () => { + const pdf = await PDF.load(fintracBytes); + const form = pdf.getForm()!; + + form.fill({ + ...fintracFields, + ...fintracCheckboxes, + }); + + form.flatten(); + await pdf.save(); + }); + + bench("pdf-lib", async () => { + const pdf = await PDFDocument.load(fintracBytes); + const form = pdf.getForm(); + + for (const [name, value] of Object.entries(fintracFields)) { + form.getTextField(name).setText(value); + } + + for (const [name, value] of Object.entries(fintracCheckboxes)) { + const cb = form.getCheckBox(name); + + if (value) { + cb.check(); + } else { + cb.uncheck(); + } + } + + form.flatten(); + await pdf.save(); + }); + + bench("@cantoo/pdf-lib", async () => { + const pdf = await CantooPDFDocument.load(fintracBytes); + const form = pdf.getForm(); + + for (const [name, value] of Object.entries(fintracFields)) { + form.getTextField(name).setText(value); + } + + for (const [name, value] of Object.entries(fintracCheckboxes)) { + const cb = form.getCheckBox(name); + + if (value) { + cb.check(); + } else { + cb.uncheck(); + } + } + + form.flatten(); + await pdf.save(); + }); }); diff --git a/benchmarks/fixtures.ts b/benchmarks/fixtures.ts index 4cf11f2..c755a45 100644 --- a/benchmarks/fixtures.ts +++ b/benchmarks/fixtures.ts @@ -162,3 +162,4 @@ export async function getSynthetic2000(): Promise { export const smallPdfPath = "fixtures/basic/rot0.pdf"; export const mediumPdfPath = "fixtures/basic/sample.pdf"; export const formPdfPath = "fixtures/forms/sample_form.pdf"; +export const fintracPdfPath = "fixtures/issues/form-filling/FINTRAC.pdf"; diff --git a/bun.lock b/bun.lock index d1f812e..fa00eed 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "pkijs": "^3.3.3", }, "devDependencies": { + "@cantoo/pdf-lib": "^2.6.1", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0", "@types/bun": "^1.3.5", @@ -52,6 +53,8 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@cantoo/pdf-lib": ["@cantoo/pdf-lib@2.6.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "crypto-js": "^4.2.0", "node-html-better-parser": ">=1.4.0", "pako": "^1.0.11", "tslib": ">=2" } }, "sha512-Nr/N5kR0xEzibtXei25E8LX9ThYsAN+Wob9jGZ1MSkMzWfxSo1fQwHc/BumE11bMMKEzn7jG5nT+kGlzAaAb2Q=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -364,16 +367,22 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -446,6 +455,8 @@ "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], @@ -458,6 +469,8 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -520,6 +533,8 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-html-better-parser": ["node-html-better-parser@1.5.8", "", { "dependencies": { "html-entities": "^2.3.2" } }, "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw=="], + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], @@ -598,6 +613,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -644,7 +661,7 @@ "tsdown": ["tsdown@0.18.4", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "defu": "^6.1.4", "empathic": "^2.0.0", "hookable": "^6.0.1", "import-without-cache": "^0.2.5", "obug": "^2.1.1", "picomatch": "^4.0.3", "rolldown": "1.0.0-beta.57", "rolldown-plugin-dts": "^0.20.0", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.2", "unrun": "^0.2.21" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "*", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-J/tRS6hsZTkvqmt4+xdELUCkQYDuUCXgBv0fw3ImV09WPGbEKfsPD65E+WUjSu3E7Z6tji9XZ1iWs8rbGqB/ZA=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -680,11 +697,7 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@cantoo/pdf-lib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -694,10 +707,6 @@ "@pdf-lib/upng/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "asn1js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -712,11 +721,9 @@ "pdf-lib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - - "pkijs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "pvtsutils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/package.json b/package.json index 107c866..dcd7f38 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "pkijs": "^3.3.3" }, "devDependencies": { + "@cantoo/pdf-lib": "^2.6.1", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0", "@types/bun": "^1.3.5", diff --git a/scripts/bench-comment.ts b/scripts/bench-comment.ts index b6df46b..5f215bf 100644 --- a/scripts/bench-comment.ts +++ b/scripts/bench-comment.ts @@ -93,6 +93,12 @@ for (const file of data.files) { lines.push("|:---|---:|---:|---:|---:|"); for (const b of group.benchmarks) { + // Handle benchmarks that errored out (no samples collected) + if (b.hz == null || b.sampleCount == null) { + lines.push(`| ${b.name} | FAILED | - | - | 0 |`); + continue; + } + lines.push( `| ${b.name} | ${formatMs(b.mean)} | ${formatMs(b.p99)} | ${formatRme(b.rme)} | ${b.sampleCount} |`, ); diff --git a/scripts/bench-report.ts b/scripts/bench-report.ts index d16617d..37dc19b 100644 --- a/scripts/bench-report.ts +++ b/scripts/bench-report.ts @@ -168,6 +168,13 @@ function generateMarkdown(data: BenchmarkOutput): string { for (const bench of sorted) { const name = bench.name; + + // Handle benchmarks that errored out (no samples collected) + if (bench.hz == null || bench.sampleCount == null) { + lines.push(`| ${name} | FAILED | - | - | - | 0 |`); + continue; + } + const hz = formatHz(bench.hz); const mean = formatTime(bench.mean); const p99 = formatTime(bench.p99); @@ -180,9 +187,12 @@ function generateMarkdown(data: BenchmarkOutput): string { lines.push(""); // Add comparison summary for groups with multiple benchmarks - if (sorted.length >= 2) { - const fastest = sorted[0]; - const rest = sorted.slice(1); + // (only include benchmarks that actually produced results) + const validSorted = sorted.filter(b => b.hz != null); + + if (validSorted.length >= 2) { + const fastest = validSorted[0]; + const rest = validSorted.slice(1); for (const slower of rest) { const ratio = (fastest.hz / slower.hz).toFixed(2);