v0.15.0 - PDF/UA-1 conformance + @formepdf/html in the browser & on the edge
The accessibility-and-reach release. Forme now produces PDF/UA-1 conforming
documents (verified end-to-end by veraPDF in CI) and tags every render by
default; @formepdf/html gains real browser and Cloudflare Workers
support; @formepdf/vue joins the adapter family with a cross-framework
equivalence guarantee; and a batch of engine fixes lands.
⚠️ Migration — position: absolute resolves against the nearest positioned ancestor
This retires a v0 divergence and matches every browser. It changes shipped JSX
output, which is why it's a minor bump rather than a patch.
The rule. An absolute element's containing block is now its nearest ancestor
with position: relative or absolute. If no ancestor is positioned, it
resolves against the page content box. Previously it always resolved against its
direct parent, positioned or not.
Detection recipe. If you use position: 'absolute' inside a container that
has no position set, add position: 'relative' to that container.
Otherwise the absolute box now resolves against the page (or a higher positioned
ancestor) — most visibly with negative offsets, which will push content off the
page.
Worked example (from this repo). The catalog template's "SALE"/"NEW" badge
is position: 'absolute'; top: -18; right: -18 — negative offsets meant to
overhang a product card's corner. The card had no position, so under the old
rule the badge sat on the card; under the new rule it escaped to the page and
overflowed by 16pt. Our own structural-regression gate caught it before it
shipped. The one-line fix is the migration:
- <View style={{ /* card */ }}>
+ <View style={{ position: 'relative', /* card */ }}>How to find affected templates. Grep for position: 'absolute' and check
whether each one's parent sets position. If you use @pdf-testkit structural
snapshots, your baselines will flag it exactly as ours did.
⚠️ Default change — tagged PDFs by default
Document.tagged now defaults to true: every render emits a structure tree
unless you explicitly set tagged: false.
PDF output bytes change for every render; geometry and visual output do not;
set tagged: false to restore prior bytes.
The tag tree is built after layout, so this is structurally additive — no
element moves. But byte-diffing consumers and anyone holding a PDF snapshot
baseline (pdf-testkit and friends) will see every output shift as the structure
objects appear. Expected; re-baseline once.
PDF/UA-1 — verified, 9 of 9
A nine-document corpus passes veraPDF 1.30.2 against the
PDF/UA-1 profile: the five shipped @formepdf/templates (invoice, receipt,
report, shipping-label, letter) and four HTML fixtures (letterhead,
dashed-borders, statement, zebra-invoice). It's a CI gate
(scripts/verify-pdfua.mjs + a pdfua-conformance job that installs veraPDF and
validates the whole corpus), so the number can't quietly regress.
Getting there taught the tagged-PDF writer the structure real documents use:
heading levels, lists wrapped in /LBody, table header /Scope and cell
/ColSpan, links as /Link structure elements bound to their annotations via
OBJR + /StructParent, figure /Alt, an emptied /RoleMap (a standard type
self-mapping is the circular RoleMap veraPDF rejects), and /Lang.
pdfUamode turns it on for JSX:<Document pdfUa lang="en-US" fonts={standardFonts()}>.- HTML path:
--tagged/--pdf-uaon the CLI,tagged/pdfUa/lang
inrenderHtmloptions, and<img alt>maps to/Alt. @formepdf/fonts-standard(new, optional) ships the Liberation
Sans/Serif/Mono families (SIL OFL) as metric-compatible substitutes for the
base-14 Helvetica/Times/Courier. PDF/UA requires embedded fonts; the base-14
set isn't. RegisteringstandardFonts()embeds a simple TrueType at
write-time only — the substitution is in the font dictionary, AFM/Widths
are kept, so layout geometry is byte-identical by construction. A per-glyph
width carve-out covers the ~6 glyphs per family where Liberation diverges from
the AFM metrics (PDF/A width-consistency). It's a separate package on purpose:
core carries no font payload — users who don't need conformance don't inherit
the 5.8 MB.- Warnings channel. Renders now return
warnings: string[]alongsidepdf
andlayout(renderPdfWithLayout, the browser/worker entries, the HTML
wrapper). IfpdfUais requested but no embeddable font is registered, the
render still succeeds and names the gap instead of emitting a PDF that
falsely claims conformance. This previously was a native-onlyeprintlnthat
vanished under WASM — the silent-fail the font design exists to prevent now
reaches WASM callers.
@formepdf/html — now runs in the browser and on the edge
The 0.14.0 debut shipped only a Node build. This release adds the two targets
that make the headline claim real, mirroring @formepdf/core:
- Browser bundlers (Vite, webpack, esbuild, Turbopack): import from
@formepdf/html/browser. The bundler instantiates the WASM at load — no
init step. - Cloudflare Workers / edge: import from
@formepdf/html/workerand call
await init(wasm)once at request time with theWebAssembly.Moduleyou
import from@formepdf/html/pkg-web/forme_pdf_html_bg.wasm. (Workers can't use
the bundler build — its top-level WASM init conflicts with Wrangler's
WASM-as-ESM contract; the worker entry with explicitinitis the supported
edge path, exactly as@formepdf/coredoes it.) - Node / npx: the default import, unchanged.
All three entries expose the identical renderHtml / renderHtmlWithLayout API
(including the warnings array) and are byte-for-byte deterministic — the three
targets embed the same WASM, verified in CI against the fixture corpus, plus a
headless-Chromium render and a workerd render. Each target's WASM is ~7.45 MB
uncompressed (it gzips down substantially over the wire — same class as
@formepdf/core); a Workers user watching bundle limits should size for it.
Also in the HTML path:
- Local
<link rel="stylesheet">resolution in the CLI — stylesheets are
read and inlined in source order (the library itself never fetches). @mediafeature queries (width/height) resolve against the page
content box.:nth-last-child/:nth-last-of-type.position: relativeoffsets — paint-only, flow preserved.vertical-align: baselinein table cells.- Dashed and dotted border styles.
float/clearare unsupported but now warn with a remedy rather than
silently mislaying content.
New & changed packages
@formepdf/vue(new) — Vue 3 SFCs → Forme documents, with a
cross-framework equivalence gate: the same document authored in Vue and in
React serializes to deep-equal Forme JSON. React/Svelte/Vue/Preact all agree.@formepdf/sharedgains public surface — the HTMLparserandencode
layers are hoisted here so the framework adapters (Svelte, Vue, Preact) share
one implementation instead of each carrying a copy. This makessharedthe
root of the publish order: shared → core → html → svelte → vue → preact →
react → renderer → templates → cli → fonts-standard → rest.
Engine fixes (user-visible)
- No more leading blank page when the first element carried a
break-before. - Empty styled
<p>(padding/background, no text) is no longer dropped — it
paints. - Table cell CSS
heightis a minimum row height, not a cap; taller
content grows the row. - Wrapping headings auto-size correctly — a heading that wraps to multiple
lines no longer measures as zero height (it was missing from the height
measurement, collapsing tagged headings). - Circular
/DivRoleMap fixed — the identity mapping invalidated the whole
tagged tree under validators; the RoleMap is now empty (every role Forme emits
is already a standard PDF type). /Link,/LBody,/ColSpanstructure tagging — links, list-item bodies,
and spanning table cells now carry correct structure (see PDF/UA above).
Extension
- Live preview for Svelte, Vue, and Preact templates, alongside React and
HTML. - Preact templates were detected but failed at render; they now render.
Full changelog
engine ·
@formepdf/html ·
@formepdf/core ·
@formepdf/vue ·
@formepdf/fonts-standard ·
@formepdf/shared
All other @formepdf/* packages, the forme-pdf crate, and the Python/Go SDKs
get version-alignment bumps to 0.15.0.