Skip to content

v0.15.0 - PDF/UA-1 conformance + @formepdf/html in the browser & on the edge

Choose a tag to compare

@danmolitor danmolitor released this 01 Sep 05:26
· 24 commits to main since this release
73902af

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.

  • pdfUa mode turns it on for JSX: <Document pdfUa lang="en-US" fonts={standardFonts()}>.
  • HTML path: --tagged / --pdf-ua on the CLI, tagged / pdfUa / lang
    in renderHtml options, 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. Registering standardFonts() 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[] alongside pdf
    and layout (renderPdfWithLayout, the browser/worker entries, the HTML
    wrapper). If pdfUa is 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-only eprintln that
    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/worker and call
    await init(wasm) once at request time with the WebAssembly.Module you
    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 explicit init is the supported
    edge path, exactly as @formepdf/core does 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).
  • @media feature queries (width/height) resolve against the page
    content box.
  • :nth-last-child / :nth-last-of-type.
  • position: relative offsets — paint-only, flow preserved.
  • vertical-align: baseline in table cells.
  • Dashed and dotted border styles.
  • float / clear are 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/shared gains public surface — the HTML parser and encode
    layers are hoisted here so the framework adapters (Svelte, Vue, Preact) share
    one implementation instead of each carrying a copy. This makes shared the
    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 height is 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 /Div RoleMap 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, /ColSpan structure 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.