Verification

Parity & conformance evidence

Everything below is generated from Forme's CI on every commit — not hand-written, and it fails the build if it drifts from reality. It covers what CI actually proves: PDF/UA-1 and PDF/A conformance per document (via veraPDF), native-vs-WASM byte determinism, and test coverage. Failures and unsupported cases are included where they exist. It is not a browser-parity claim and not a coverage percentage.

Generated from CI run 38948505069 at bb79da4 (v0.28.0) on 2026-10-16 18:46:02 UTC. Raw artifact →

Conformance

PDF/UA-1 and PDF/A, per document

Validated with veraPDF 1.30.2. The corpus is 5 shipped templates (invoice, receipt, report, shipping-label, letter) and 4 HTML fixtures (letterhead, dashed-borders, statement, zebra-invoice). Each PDF/A column is the archival-and-accessible render — the same file validated against the PDF/A profile and PDF/UA-1.

DocumentPDF/UA-1PDF/A-2b + UA-1PDF/A-2a + UA-1PDF/A-3b + UA-1PDF/A-3a + UA-1PDF/A-4 + UA-1PDF/A-4f + UA-1
template/invoice✓✓✓✓✓✓✓
template/receipt✓✓✓✓✓✓✓
template/report✓✓✓✓✓✓✓
template/shipping-label✓✓✓✓✓✓✓
template/letter✓✓✓✓✓✓✓
html/letterhead✓✓✓✓✓✓—
html/dashed-borders✓✓✓✓✓✓—
html/statement✓✓✓✓✓✓—
html/zebra-invoice✓✓✓✓✓✓—
northmoor/invoice-standard✓——————
northmoor/invoice-detailed✓——————
northmoor/credit-note✓——————
northmoor/receipt✓——————
northmoor/quote✓——————
northmoor/statement✓——————
northmoor/purchase-order✓——————
northmoor/remittance-advice✓——————
northmoor/expense-report✓——————
northmoor/payslip✓——————
northmoor/letterhead✓——————
northmoor/cover-letter✓——————
northmoor/memo✓——————
northmoor/employment-contract✓——————
northmoor/nda✓——————
northmoor/service-agreement✓——————
northmoor/offer-letter✓——————
northmoor/termination-letter✓——————
northmoor/reference-letter✓——————
northmoor/policy-acknowledgement✓——————
northmoor/report-annual✓——————
northmoor/report-monthly✓——————
northmoor/lab-report✓——————
northmoor/inspection-report✓——————
northmoor/shipping-label✓——————
northmoor/packing-slip✓——————
northmoor/delivery-note✓——————
northmoor/certificate✓——————
northmoor/product-catalog✓——————
northmoor/meeting-minutes✓——————
unicode✓✓✓✓✓✓✓
page-background✓✓✓✓✓✓✓

Any failed profile shows its veraPDF clause list here; there are currently none. PDF/A-2u/3u are supported and reachable but not gated separately (the a-levels are the strictest of each part).

E-invoice container

Factur-X / ZUGFeRD (PDF/A-3)

One render call produces the human-readable invoice PDF and embeds the caller-supplied EN 16931 CII XML as a conformant PDF/A-3 associated file (pdfa:3b + pdfUa + facturX{EN 16931} via @formepdf/core renderDocument). The XML is ZUGFeRD corpus EN16931_Einfach.cii.xml (FeRD, Apache-2.0). Forme validates the container, not the invoice semantics — the XML content is the caller's.

CheckResult
veraPDF PDF/A-3b✓
veraPDF PDF/UA-1✓
Mustang (ZUGFeRD/Factur-X reference validator)✓

Determinism

Byte-for-byte, across build targets

The same document renders to byte-identical PDFs across the native binary and the WASM targets, over the four HTML fixtures. These are distinct strengths of evidence and shown as such: native, node, and web are compared byte-for-byte; the bundler target embeds a hash-identical WASM module (so it computes identically by construction) and is exercised by functional browser and workerd renders — it is not itself byte-diffed here.

ComparisonFixtureBytesIdentical
node↔webletterhead3,845✓
native↔nodeletterhead3,845✓
node↔webreport5,546✓
native↔nodereport5,546✓
node↔webzebra-invoice3,612✓
native↔nodezebra-invoice3,612✓
node↔webdashed-borders1,909✓
native↔nodedashed-borders1,909✓
WASM hash-identity (bundler / web / node)
✓ identical module
sha256 c4e782b4f45b61edf4524f3086dda3e2…

Regression evidence

Structural assertions from Chrome reference & third-party corpus

Boolean structural assertions (page count, break positions, table structure, valign) read from Chrome's rendering of these fixtures and asserted exactly. NOT a general browser-parity claim or a measured diff.

✓ baseline_alignment_shares_one_baseline_across_font_sizes✓ br_produces_a_multi_line_address_block✓ fixture_renders_to_a_single_page_pdf✓ flex_container_bare_text_runs_are_their_own_items✓ flex_container_inline_children_become_separate_items✓ flex_n_shorthand_sets_basis_zero_per_spec✓ heading_list_and_image_map_to_engine_node_types✓ letter_and_word_spacing_inherit_to_descendant_text✓ margin_collapse_h1_to_p_gap_is_max_not_sum✓ margin_collapse_p_to_p_gap✓ nested_flex_row_intrinsic_width_includes_its_gaps✓ stylesheet_block_is_ignored_not_rendered✓ table_maps_with_header_and_all_rows✓ tracked_uppercase_text_measures_at_its_styled_width✓ unknown_css_lands_in_warnings_not_errors✓ whitespace_collapses_through_the_whole_pipeline

Hand-reduced repros distilled from the ironpress parity corpus (third-party test material, MIT). Not upstream copies.

Source: gastongouron/ironpress (MIT).

✓ break_before_page_after_content_still_breaks✓ break_before_page_on_first_block_is_suppressed✓ empty_styled_paragraph_renders_its_box✓ table_cell_vertical_align_positions_markers

Test coverage

Suite counts

Passing tests per suite, parsed from an actual run. Stated as a fact, not a percentage.

engine (lib + integration)682 passed✓
html crate276 passed✓
@formepdf/core103 passed✓
@formepdf/react284 passed✓
@formepdf/preact73 passed✓
@formepdf/svelte74 passed✓
@formepdf/vue29 passed✓
@formepdf/renderer29 passed✓
@formepdf/mcp78 passed✓
@formepdf/resend5 passed✓
@formepdf/tailwind65 passed✓
@formepdf/vscode11 passed✓
total1,709 passed✓

Benchmarks

Measured performance — including where we lose

Process/isolate start through first PDF byte, including module instantiation. Steady state after warm-up; median and p95 over the published iteration count. Puppeteer reuses one browser across iterations (setContent + page.pdf each). All targets and Puppeteer render byte-identical HTML from the fixed corpus, so every column is the same document. Corpus is fixed, committed, and hash-checked (6 documents).

Two environments are published side by side and neither is cherry-picked: a dev machine (clean, quiet, single run) shows the engine on good hardware; CI ubuntu-latest (a shared, noisy runner) is the conservative number and the one that is regenerated on every push. Where a document ran on one but not the other, the gap is shown rather than the row hidden.

✓ dev (clean) — Apple M3 Max, 16 cpu · darwin 24.6.0 · node v24.10.0 · measured at 0ae8336 (2026-09-07), may lag current commit✓ CI (shared) — AMD EPYC 9V45 96-Core Processor, 4 cpu · linux 6.17.0-1022-azure · node v20.20.2 · measured at bb79da4 (2026-10-16)

Cold start — start to first PDF byte

Targetdev (clean)CI (shared)
native binary12 ms6 ms
workerd isolate (edge est.)~66 ms~64 ms
web (browser)91 ms121 ms
node WASM111 ms236 ms
Puppeteer372 ms230 ms

The ~7.1MB WASM module compiles and instantiates in 1-10ms — cold start is dominated by first-render JIT warmup, not module size: the module is large on disk and cheap to instantiate. Puppeteer here is on a warm machine; cold serverless is 3-10s, and on many consumption tiers Chrome cannot boot at all.

workerd caveat miniflare runs a workerd subprocess whose routing overhead the real edge does not pay; wall time OVERSTATES production cold start (true edge isolate spin-up ~5ms). Instantiate/render split is accurate. Real production cold start needs a deployment (out of scope).

Serverless memory

What fits under the Cloudflare Workers 128MB ceiling

Peak WASM linear memory per document (engine allocation — the same on any machine). This decides whether a document is renderable on Workers at all.

DocumentPagesPeak WASM memoryFits 128MB
receipt17 MB✓
report-6p68 MB✓
letterhead-paged510 MB✓
compliance27 MB✓
invoice-50p5060 MB✓
ledger-500p500839 MB✗ no

Practical guidance: comfortable under ~50 pages, approaching the ceiling near ~100 pages, use node or native beyond that. A margin, not a threshold — a slightly wider table shifts the number.

Local miniflare has generous memory and will run documents the real edge would OOM: the 500-page ledger completes locally but its 839MB peak would exceed the 128MB edge limit.

Warm render

Forme vs Puppeteer, identical HTML

Median warm render (node target for Forme; Puppeteer reuses one browser). Both environments shown; the rows where Forme loses are here, in order, not reordered.

DocumentPagesForme devPptr devForme CIPptr CIForme PDFPptr PDF
receipt117 ms48 ms9 ms26 ms3 KB46 KB
report-6p621 ms54 ms13 ms30 ms8 KB79 KB
letterhead-paged527 ms52 ms19 ms33 ms9 KB66 KB
compliance219 ms50 ms23 ms29 ms5 KB66 KB
invoice-50p27171 ms233 ms265 ms417 ms106 KB1946 KB
ledger-500p2672.74 s2.79 s4.93 s4.97 s1039 KB19599 KB

Forme is ~2× faster on small documents with ~15× smaller output, and — after the allocation fixes below — now edges a warm pooled Chrome on the large table-heavy documents too (50p and 500p), where it previously trailed ~2–4×. Iteration counts per cell are in the raw artifact.

Where the time goes

The large-document gap, measured then closed

Forme originally trailed Puppeteer ~3.8× on ledger-500p, decomposed as two independent linear factors: ~2× from the page-number second layout pass (ledger crosses the 2→3 digit-width boundary at page 100, so it re-lays-out to widen the page-count field), and ~1.9× from per-node allocation churn. That decomposition was directionally right and quantitatively wrong in the engine's favor: fixing the allocation churn (−74% allocations, −88% bytes allocated; benchmarks/harness/dhatprofile) dropped per-pass layout from ~7.3 to ~1.8 ms/page — a ~4× per-pass gain, not the estimated ~1.9×. It isolates cleanly because parse and serialize (both untouched) didn't move. That alone flipped both large docs — ledger 9997→2284 ms, invoice 534→147 ms — from behind Puppeteer to ahead (ledger 2284 vs 2665 ms, invoice 147 vs 232 ms; same M3 Max, Puppeteer as a stable control moved <2%). The ~2× sentinel factor is NOT eliminated: ledger still runs 2 passes — it is now masked by the throughput win, i.e. forme leads while doing twice Chrome's layout work. Removing that second pass on digit-change docs is the remaining known inefficiency (scoping it to just the running element proved unsound — see the sentinel tracked-fix note) and would roughly halve ledger's layout again.

The full layout tree is retained until serialize (~2 MB/page), so peak memory is linear in document size and freed at exit — not a leak. The allocation fixes did NOT move peak (ledger wasm ~890 MB before and after): they removed transient churn, not retained state — a throughput/allocator-pressure win, not a memory-ceiling one.

Phase (native)invoice-50pledger-500p
parse16 ms120 ms
layout93 ms1.74 s
serialize20 ms228 ms
layout before fix368 ms7.23 s
layout ms/page/pass1.861.74

Per-pass layout dropped from ~7.3 to ~1.8 ms/page (the allocation fix) and stays flat 50→500 pages — linear, no super-linear scaling. Parse and serialize (untouched) didn't move — the control that isolates the layout gain.

Allocation churn (counting allocator)
ledger-500p: 183M → 46M allocations (−75%, ~2,892/row/pass)
peak live 998 MB — unchanged by the fix, freed to 3MB at exit. Churn removed, not retained state: this is a speed win, not a capacity one (the 128MB table below is unchanged).

Tracked fixes (no dates promised):

Self-audit

A number we found wrong on our own homepage

The published ~26ms figure was measured on a document doing two layout passes; the honest single-pass number is ~21ms. Benchmarking our own engine surfaced an unconditional second layout pass — a document with no page-number placeholder was re-laying out the whole tree for nothing. We fixed it; here is the before and after on identical documents.

DocumentPagesBeforeAfterPasses
receipt119 ms17 ms2→1
report-6p627 ms21 ms2→1
compliance224 ms20 ms2→1
no-counter-100p1001.98 s1.03 s2→1
no-counter-300p3005.89 s3.09 s2→1

A/B: pre-fix WASM rebuilt from HEAD~1 vs the fix, same machine/harness, node target, warm median.

Known gaps & deliberate divergences

What isn't claimed