Skip to content

feat(preview): @formepdf/preview — byte-true in-browser PDF preview + CI parity gate - #89

Merged
danmolitor merged 3 commits into
mainfrom
feat/preview-component
Sep 9, 2026
Merged

danmolitor merged 3 commits into
mainfrom
feat/preview-component

Conversation

@danmolitor

Copy link
Copy Markdown
Owner

@formepdf/preview — byte-true in-browser PDF preview (v1, HTML path)

Renders HTML through the same engine the server uses and shows the actual PDF in an iframe, so preview and output agree by construction — not by approximation. This is the thing a Gotenberg/Puppeteer pipeline gets for free (the browser is both renderer and preview); now Forme gets it because the engine is both, and it paginates properly.

Why it can't silently disagree with output

The audit found three ways a preview can diverge. All three are closed in code:

  • Display — blob → iframe → the browser's native viewer shows the real bytes. No pdf.js-canvas rasterizer approximating the file.
  • Options — there is no previewOptions. You pass the same options object to <FormePreview> and to your server's renderHtml. renderForPreview is a faithful passthrough; the guarantee is the absence of transforms.
  • Fonts — the silent one. The browser HTML path can't fetch fonts, so the contract is bytes-only: standardFonts() adapts @formepdf/fonts-standard to renderHtml's shape, and fontFingerprint/fontsMatch turn a font fall-back mismatch into a visible banner.

What's here

  • render.ts — the parity core (WASM-free, renderHtml injected): renderForPreview, standardFonts, fontFingerprint, fontsMatch.
  • FormePreview.tsx — lazy ~7.7MB engine import (out of the initial bundle + a real loading gate), debounce, last-good-frame, loading/error, warnings surfaced, font-mismatch banner.
  • README — the 7.7MB bundle cost is the first thing after the intro, not a footnote.

The gate (the point of this PR)

A parity guarantee that's true today and unenforced quietly stops being true — and this one breaks silently. So there's a Test Preview (parity gate) CI step that asserts renderForPreview (browser-proxy /worker entry) is byte-identical to the Node renderHtml across a hermetic inline corpus, including the 2-pass counter(page)/counter(pages) path. Divergence fails the build.

Fails-first discipline: the byte-identity test was written and made red (before render.ts existed), then green. 11/11 pass; tsc clean.

Scope

v1 is the HTML input path (matches the live prospect's pipeline exactly; simplest parity story). Deferred: JSX input, worker-render mode, a pdf.js-canvas mode (documented approximation), and a jsdom test for the React shell (the parity core — the load-bearing part — is covered; a broken iframe is visible immediately, a broken parity guarantee isn't).

🤖 Generated with Claude Code

danmolitor and others added 3 commits September 9, 2026 11:53
Package skeleton and the byte-identity parity test written before the component
(the test is the promise). Not yet runnable — src/render.ts does not exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v1, HTML path)

Renders HTML through the same engine as the server and shows the actual PDF in
an iframe (native viewer), so preview and output agree by construction:
- Display: blob → iframe, not a pdf.js-canvas approximation — shows the real bytes.
- Options: one shared options object; no previewOptions to drift.
- Fonts: bytes-only contract + standardFonts() adapter + fontFingerprint/fontsMatch
  to turn a silent font fall-back into a visible mismatch banner.

renderForPreview is a faithful passthrough (the parity guarantee lives in its
absence of transforms). Component: lazy ~7.7MB engine import (out of the initial
bundle), debounce, last-good-frame, loading/error, warnings surfaced.

Fails-first parity test (red before render.ts existed, now green): the browser
path is BYTE-IDENTICAL to server renderHtml on the same (html, options),
including embedded fonts. 7/7 tests pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parity guarantee is silent when broken — the preview would just start
disagreeing with output and nothing would say so. So it must be a gate, not a
one-off test.

- Expand the parity suite to a hermetic inline corpus (invoice table, multipage,
  borders/backgrounds, and page-counters — the 2-pass sentinel path, the
  trickiest to keep byte-identical). Inline so a fresh CI checkout needs no
  external fixtures (@formepdf/html's fixtures aren't git-tracked).
- Add a 'Test Preview (parity gate)' step in the packages job, after the HTML
  WASM + fonts-standard builds it depends on. Divergence fails the build.
- State in renderForPreview that the guarantee IS the absence of transforms, so
  nobody helpfully adds a preview-specific default later.

11/11 tests pass locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@forme-review

forme-review Bot commented Sep 9, 2026

Copy link
Copy Markdown

⛔ Changes rejected

28 documents changed structurally · 0 errors, 0 warnings, 28 info

dist/review/northmoor-termination-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 71 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 71 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-statement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 168 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 168 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-service-agreement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 325 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 325 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-report-monthly.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 192 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 192 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-remittance-advice.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 135 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 135 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-reference-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 50 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 50 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-receipt.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 105 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 105 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-quote.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 135 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 135 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-purchase-order.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 134 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 134 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-product-catalog.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 237 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 237 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-policy-acknowledgement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 153 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 153 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-payslip.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 177 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 177 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-packing-slip.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 146 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 146 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-offer-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 86 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 86 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-nda.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 208 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 208 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-memo.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 98 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 98 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-meeting-minutes.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 148 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 148 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-letterhead.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 115 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 115 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-lab-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 169 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 169 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-invoice-standard.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 117 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 117 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-invoice-detailed.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 416 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 416 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-inspection-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 229 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 229 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-expense-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 179 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 179 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-employment-contract.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 316 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 316 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-delivery-note.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 156 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 156 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-credit-note.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 104 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 104 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-cover-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 52 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 52 matched elements moved or resized below the 24pt threshold.

View full diff ↗

dist/review/northmoor-certificate.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be

Severity Event Location Rule
info Structure changed but no named event fired (1 of 61 matched elements shifted below the threshold) document uncharacterized-change

Snapshot changed but no nameable event fired: 1 of 61 matched elements moved or resized below the 24pt threshold.

View full diff ↗

12 documents unchanged: dist/review/template-shipping-label.pdf, dist/review/template-report.pdf, dist/review/template-receipt.pdf, dist/review/template-letter.pdf, dist/review/template-invoice.pdf, dist/review/northmoor-shipping-label.pdf, dist/review/northmoor-report-annual.pdf, dist/review/html-zebra-invoice.pdf, dist/review/html-statement.pdf, dist/review/html-letterhead.pdf, dist/review/html-dashed-borders.pdf, dist/review/facturx-en16931.pdf

No page thumbnails here: GitHub proxies and caches comment images, which would put a rendered customer document in a third-party cache. Open the full diff to see the pages.

Forme Review · 40 runs · powered by pdf-testkit 0.4.1

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📄 pdf-testkit — invoice-layout.json

✅ No semantic changes detected.

@danmolitor
danmolitor merged commit 71730cb into main Sep 9, 2026
10 of 11 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 9, 2026
@danmolitor
danmolitor deleted the feat/preview-component branch September 9, 2026 18:00
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant