Skip to content

Releases: danmolitor/forme

v0.16.0 - PDF/A conformance

Choose a tag to compare

@danmolitor danmolitor released this 01 Sep 18:23

Forme now produces PDF/A-2 conforming archival documents — levels 2b, 2u,
and 2a
— verified by veraPDF in CI, and composable with PDF/UA-1 so a single
file can be both archival and accessible (the configuration
government/education/healthcare archives actually require).

⚠️ Correction — PDF/A output was never conformant before this release

If you used pdfa: "2b" (or "2a") in 0.15.0 or earlier, those files are not
valid PDF/A
— they carry an OutputIntent whose ICC profile was invalid, so no
conformant PDF/A reader would have accepted them as archival.

The embedded sRGB profile (srgb2014.icc) was never an ICC profile. A curl
that fetched it around 0.9.0 returned a Cloudflare "Just a moment…" HTML
challenge page instead of the binary; it was embedded unchecked and shipped in
every build since. Nothing caught it because nothing validated the output. It's
now replaced with a real, generated sRGB profile and validated in CI — and a
test asserts the embedded bytes are a valid ICC profile so this can't recur.

How to check: any file produced with pdfa before 0.16.0 will fail veraPDF's
PDF/A profile — verapdf -f 2b your-file.pdf reports FAIL. Re-render those
files with 0.16.0 (same input, same options) and they'll pass.

PDF/A-2b / 2u / 2a — verified

The same nine-document corpus the PDF/UA gate uses (five shipped templates + four
HTML fixtures) passes veraPDF's PDF/A-2b and PDF/A-2a profiles. Getting there:

  • A real sRGB OutputIntent — a generated, license-clean sRGB profile (~588
    bytes, ICC v4.3, Little CMS / MIT) embedded in the engine, replacing the
    invalid one above.
  • PDF/A XMP metadata — pdfaid part/level, plus the pdfaExtension description
    for the PDF/UA identification schema so PDF/A and PDF/UA compose.
  • A deterministic trailer /ID — derived from a content hash, not a
    timestamp or random bytes. Identical input still produces byte-identical
    output, so Forme's determinism guarantee (native == WASM, reproducible builds)
    holds in PDF/A mode too — a property archival pipelines care about.
  • /F (Print) on link annotations, and the font-embedding check now accepts
    the pdfUa metric-compatible substitution — so pdfA + pdfUa no longer throws.
  • No width-consistency issue: the AFM-widths-by-construction font design
    (with its carve-out) satisfies PDF/A's width check — geometry stays identical
    across standard and archival output.

Archival and accessible

PDF/A composes with PDF/UA-1. Set both:

import { standardFonts } from '@formepdf/fonts-standard';

<Document pdfa="2a" pdfUa lang="en-US" fonts={standardFonts()}></Document>

Every corpus file is CI-gated against PDF/A-2b, PDF/A-2a, and PDF/UA-1
together
(scripts/verify-pdfa.mjs) — the claim is enforced, not observed.

API

  • JSX: <Document pdfa="2b" | "2u" | "2a"> (new: "2u").
  • HTML: pdfA in renderHtml options; --pdf-a <level> on the forme-html CLI.
  • Needs an embeddable font (@formepdf/fonts-standard). If none is registered, a
    PDF/A render fails by name with the remedy rather than emitting a file that
    falsely claims conformance.

Full changelog

engine ·
@formepdf/html ·
@formepdf/react

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
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.

v0.14.0 - HTML + print-CSS input path

Choose a tag to compare

@danmolitor danmolitor released this 31 Aug 16:45

The release that adds a second front door to the engine: @formepdf/html, an HTML + print-CSS input path — "Satori for paginated documents." Write HTML with @page rules, feed it to renderHtml() or the forme-html CLI, get a paginated, deterministic PDF from the same Rust engine that renders JSX. No headless browser anywhere in the pipeline. Also: a layout-shape contract change for tables, four engine layout fixes, and HTML preview support across the renderer, CLI dev server, and VS Code extension.

Added

@formepdf/html — first public release. The engine compiled to WASM behind an HTML front end:

  • Stylesheets, not just inline styles: type/class/id/universal selectors, compounds, descendant/child combinators, grouping, the :nth-child and :nth-of-type families — with full cascade semantics including !important.
  • Paged media: @page size and margins, :first variants, margin boxes with counter(page) / counter(pages), break-* (plus legacy page-break-* aliases), orphans/widows, and @media media-type evaluation — print is the native media type.
  • Tables: border-collapse emulation, <thead> repetition across page breaks, colspan/rowspan, vertical-align (and legacy valign).
  • Typography: justified text, text-transform, letter-spacing, and provided fonts via options.fonts / --font, with a documented migration recipe for web fonts.
  • The warnings contract: everything outside the supported subset is named — skipped stylesheet links, @imports, @font-face families, unsupported properties. Nothing is silently dropped.

Determinism is load-bearing: CI renders the fixture corpus through both the native binary and the WASM build and requires byte-identical PDFs.

Engine — @page :first, vertical-align, min/max constraints. Page one can carry its own PageConfig (size, margins, fixed-element filtering via First/NotFirst); table cells align content top/middle/bottom; and max-width/min-width/min-height now clamp properly — auto width + finite max-width + auto margins is the centered-column idiom, and it works.

Renderer / CLI / VS Code — HTML preview everywhere. renderHtmlFromFile / renderHtmlFromSource in @formepdf/renderer return the same PDF-bytes + LayoutInfo shape as the JSX pipeline, so every preview surface lights up unchanged: the CLI dev server and the VS Code extension both live-preview .html files with the full component tree, inspector, and layout overlays. The extension now bundles two WASM snapshots and hash-verifies both against their package sources at build time.

⚠️ Layout-shape contract change — Table wrapper node

ElementNodeType gains 'Table'. Layout used to unwrap <Table> into loose sibling TableRow nodes; it now emits a Table container element per page fragment with the rows nested inside. Two reasons: table-level border/background finally have a paint target, and structural consumers (tagged PDF /Table, extractors) get a real table node. If you walk LayoutInfo for TableRow nodes directly, look inside Table wrappers now — or use getTableRows() from @formepdf/core/layout, which handles both shapes (loose rows from stored pre-0.14 layouts still return).

Fixed

  • Runs-based text measured zero intrinsic width. measure_intrinsic_width ignored runs (and measured leaf Headings as 0, and whole multi-line strings instead of the widest line) — flex rows could collapse styled text to one character per line.
  • col_span was ignored when indexing column widths. Every cell after a colspan cell sat one column too far left; the spanning cell now consumes its columns' combined width in both layout and row-height measurement.
  • wrap: false on tables was silently ignored. Row-by-row pagination never consulted breakability; an unbreakable table that fits a fresh page now moves there whole (break-inside: avoid in the HTML path).
  • A flex line taller than the page emitted a blank leading page before overflowing anyway; the break check now skips when the current page is empty.

Docker images — security refresh

formepdf/forme and formepdf/rasterizer are published again at 0.14.0 (multi-arch, amd64 + arm64), rejoining the shared version line after being frozen at 0.10.5. Rebuilt on Alpine/musl with PDFium chromium/8021 — Docker Scout reports zero vulnerable packages on both, and the server image dropped from 150 MB to 24 MB.

Full changelog

@formepdf/html · engine · @formepdf/core · @formepdf/renderer · vscode · @formepdf/svelte

All other @formepdf/* packages, the forme-pdf crate on crates.io, the Python SDK, and the Go SDK got version-alignment bumps to 0.14.0.

v0.13.0 - bookmark outline fixes + invoice discount column

Choose a tag to compare

@danmolitor danmolitor released this 31 Aug 16:38

Three defects in bookmark handling — one of which shipped real PDF corruption (duplicate outline entries) — plus a per-line discount column in the invoice template. Also the release where the VS Code extension joins the shared version line.

Fixed

Engine — three bookmark defects, one shared root cause.

  • Duplicate PDF outline entries. A bookmarked container that both overflows a page and carries visual styling wrote its outline entry twice — two identical /Outlines entries and /Count 2 for a single bookmark prop. A genuine document defect, not a reporting one. The overflow path emits a zero-height marker so unstyled views can't lose their bookmark; styled views also built a wrapper carrying the same bookmark, and collect_bookmarks recurses — so it found both. Wrappers no longer carry bookmark; the marker is the sole carrier on every path.
  • Bookmarks on content that fits its page now appear in LayoutInfo. The marker was only emitted on the overflow path, so consumers walking layout output for Bookmark nodes silently missed most bookmarks. The PDF was never wrong here — the outline was always complete — this was a LayoutInfo blind spot. PDF output is byte-identical after the fix (verified by hash: 8f41f7e9fb58b3cb before and after).
  • The marker reports nodeType: "Bookmark" instead of "None". The unset node type fell back to kind.to_string() and leaked the DrawCommand::None variant name — a value never present in ElementNodeType.

All three came from the same divergence: two hand-maintained copies of the marker-building code. Both paths now share one bookmark_marker() helper.

⚠️ Behavior notes

  • Outline destination moves for one narrow case. Markers now sit at the view's outer top edge, so every container path resolves a bookmark to the same coordinate. Only an unstyled, overflowing, breakable view with top padding or border shifts (Letter, 36pt margin, padding 20: destination Y 736.00 → 756.00). Styled views are unchanged.
  • Consumers matching nodeType === 'None' (which the types never permitted) must switch to 'Bookmark'. "None" was an accident of the fallback, not a semantic role.

Added

@formepdf/templates — per-line discount on the invoice template. A decimal fraction off the line total, matching taxRate's units rather than introducing a second convention. Applies before tax. The items table grows a fifth column; discounted lines render -15% in green, undiscounted ones an em dash. invoiceExample expanded from 5 to 19 line items (three pages), so the fixture actually exercises header repetition and page breaks — the template's main job.

VS Code extension — version jump 0.10.5 → 0.13.0

The extension now tracks the monorepo version. It bundles the engine and @formepdf/renderer wholesale, so its independently-drifting number told you nothing about what was in the VSIX. No features were skipped; 0.11.x and 0.12.x simply never shipped as extension releases. Also picks up the sidebar fixes: bookmarked containers no longer show None in the component tree, and bookmarks on fitting content appear at all.

How these were found

Dogfooding, continued from v0.12.1: structural regression baselines (via pdf-testkit) extended to the shipped templates. The catalog template's bookmarks tripped all three engine defects — the duplicate-entry bug had survived because every pre-existing bookmark test asserted contains, which two identical entries satisfy perfectly well. The new test asserts counts.

Full changelog

engine · @formepdf/core · @formepdf/templates · vscode

All other @formepdf/* packages got version-alignment bumps to 0.13.0. No functional changes.

v0.12.1 — LayoutInfo type/runtime drift fix + accessor helpers

Choose a tag to compare

@danmolitor danmolitor released this 26 Aug 14:34

Patch release. No runtime behavior changed — the engine emits the exact same JSON it always did. The declared TypeScript types for renderDocumentWithLayout()'s output had drifted from that JSON in eight places, first flagged internally, then again by an external consumer's dogfood test. This release fixes them at the root and adds enforcement so it doesn't happen a third time.

Fixed

@formepdf/core — declared types now match runtime output.

  • ElementNodeType is now a closed literal union of the 30 nodeType values the engine actually emits (was string). Code like if (element.nodeType === 'Heading') — silently wrong before, since reality is discrete 'H1''H6' — is now a TypeScript compile error. Same treatment for ElementKind (10 values) and 11 style enums (ElementFlexDirection, ElementJustifyContent, ElementAlignItems, ElementAlignContent, ElementFlexWrap, ElementFontStyle, ElementTextAlign, ElementTextDecoration, ElementTextTransform, ElementOverflow, ElementPosition). All exported for narrowing.
  • ElementStyleInfo expanded from 19 to 34 fields. Previously missing: alignContent, breakBefore, breakable, columnGap, rowGap, flexGrow, flexShrink, letterSpacing, minOrphanLines, minWidowLines, overflow, position, top/right/bottom/left, width/height, textDecoration, textTransform.
  • textContent on ElementInfo is now typed as string | null | undefined (was string?). Only populated on TextLine leaves — every non-TextLine node emits null at runtime. The old declaration made consumers reach for the wrong node.
  • Every layout-time transform now documented explicitly on the ElementInfo JSDoc: <Table> unwraps into sibling TableRow nodes, <OrderedList> becomes List + ListItem + Lbl, <Fixed> splits into FixedHeader/FixedFooter, headings are discrete H1H6, <Text> block content is split into TextLine leaves, inline elements don't get their own nodes, <PageBreak> produces no node.

Added

@formepdf/core/layout — new subpath export with stable accessor helpers. Additive; the raw ElementInfo tree is unchanged.

import {
  getNodeText, getTextLines,
  getHeadingLevel, getTableRows, getFixedRegions,
  getListItems, getListItemMarker,
  walkElements, findElements, findFirstElement,
  isNodeType,
} from '@formepdf/core/layout';

The helpers encapsulate each documented layout-time transform in a narrow, deliberately-maintained surface. Consumers get to say getNodeText(paragraph) instead of hand-rolling "walk TextLine children, gather textContent, join lines." When the transforms change in a future release, the helpers absorb the change — consumers ride through transparently.

See the Layout API docs for the full reference and the "prefer helpers unless you need raw" guidance.

⚠️ Arguable-break notes — type-tightening exposes latent bugs

This is technically a patch because the underlying bug was in our declarations, not consumer code. But if your TypeScript code compiled against @formepdf/core@0.12.0 and now fails against 0.12.1, one of these is almost certainly why:

  • style.flexDirection === 'row' — silently wrong before (runtime always emitted 'Row', Rust-side PascalCase); now a compile error. Standard types-tighten territory. Fix: use the PascalCase values, now exported as ElementFlexDirection. Same story for the other 10 style enums.
  • element.textContent changing from string | undefined to string | null | undefined will flag code that assumed non-null. Fix: use the new getNodeText() helper (it handles this correctly by walking TextLine children), or explicitly handle null (which is what the runtime always emitted anyway).

Both cases were bugs before the release; TypeScript is now catching them for you. The CHANGELOG in @formepdf/core has more detail.

Enforcement

Three-directional invariant now enforced in @formepdf/core's own CI:

  1. Every emitted nodeType / kind / style enum → member of its declared union (runtime test)
  2. Every declared ElementNodeType → appears in the rich fixture (coverage tripwire — catches "component shipped without structural coverage")
  3. Every union member → present in the test file's key record (compile-time check — catches "union grew without updating the test")

This closes the drift risk at all three angles. Runtime drift trips one test; declaration-side gaps trip a compile error before the file even runs.

Full changelog

All other @formepdf/* npm packages (shared, react, svelte, preact, renderer, cli, hono, next, resend, mcp, sdk, tailwind, templates) got version-alignment bumps to 0.12.1. No functional changes.

v0.12.0 — @formepdf/preact adapter

Choose a tag to compare

@danmolitor danmolitor released this 26 Aug 01:50

Third-party framework support gets its second entry. Preact 10 now has a native authoring adapter, alongside the existing React and Svelte adapters.

New

@formepdf/preact — Preact 10 adapter. Same component set as @formepdf/react (30+ components: Document, Page, View, Text, H1H6, lists, inline formatting, tables, media, charts, form fields, layout primitives) with identical props and byte-identical serialized JSON. Requested via GitHub issue; parity is enforced by a fixture suite.

npm install @formepdf/preact @formepdf/core   
  /** @jsxImportSource preact */                                                                                                                                                                                   
  import { Document, Page, View, Text, renderDocument } from '@formepdf/preact';                                                                                                                                   
                                                                                                                                                                                                                   
  const pdf = await renderDocument(                                                                                                                                                                                
    <Document>                                                                                                                                                                                                     
      <Page size="Letter" margin={36}>                                                                                                                                                                             
        <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice</Text>                                                                                                                                          
      </Page>                                                                                                                                                                                                      
    </Document>                                                                                                                                                                                                    
  );      

Why this exists (vs preact/compat aliasing with @formepdf/react): no compat shim in your bundle (~7-8KB gzipped saved), no unmet-peer warning from npm about React not being installed, Preact-native JSX runtime. See the Preact guide.

Other packages

All other @formepdf/* npm packages (shared, react, svelte, core, renderer, cli, hono, next, resend, mcp, sdk, tailwind, templates) got version-alignment bumps to 0.12.0. No functional changes.

Full changelogs

  • @formepdf/preact

v0.11.1 — SVG stroke-linecap / stroke-linejoin fix

Choose a tag to compare

@danmolitor danmolitor released this 21 Aug 03:19

Patch release. Fixes a real user-reported SVG rendering bug.

Fixed

SVG stroke-linecap and stroke-linejoin are now honored. The engine's SVG parser was silently dropping both attributes on every element — SvgCommand::SetLineCap / SvgCommand::SetLineJoin only ever
fired from the Canvas API, never from SVG content. Every SVG stroke rendered with the PDF default (butt caps, miter joins) regardless of what the source said.

Reported against a real handwritten-signature repro: 68 short cubic bezier paths with stroke-linecap="round" rendered as visible flat rectangular cap protrusions at every segment terminus instead of blending
into smooth round semicircles. Fixed.

Attribute values also inherit through <g> group ancestors on the same stack as fill / stroke / stroke-width / opacity, so <g stroke-linecap="round">…</g> works as expected.

Who this affects

Anyone rendering SVG content — through <Svg content="..." /> in @formepdf/react or @formepdf/svelte, renderPdf on @formepdf/core, or the forme-pdf Rust crate — where any element uses
stroke-linecap other than the default butt, or stroke-linejoin other than the default miter. Very common pattern for signature capture widgets, chart annotations, and hand-drawn overlays.

Install

npm install @formepdf/react@0.11.1 @formepdf/core@0.11.1                                                                                                                                                         
# or                                                                                                                                                                                                             
npm install @formepdf/svelte@0.11.1 @formepdf/core@0.11.1 

Rust:
forme-pdf = "0.11.1"

No API changes. Drop-in.

Full changelogs

  • engine — the fix + 4 new integration tests
  • @formepdf/core — WASM rebuild carrying the fix

All other npm packages (shared, react, svelte, renderer, cli, hono, next, resend, mcp, sdk, tailwind, templates) got version-alignment bumps only.

v0.11.0 — Svelte adapter + @formepdf/shared

Choose a tag to compare

@danmolitor danmolitor released this 10 Aug 03:23

Two brand-new packages join the family, and the existing packages get an internal refactor to support them.

New packages

@formepdf/svelte — Svelte 5 authoring adapter with the full component set as .svelte files. Layout (Document, Page, View, Text), semantic headings (H1H6), lists, inline formatting, tables, media (Image, Svg, QrCode, Barcode, Canvas, Watermark), all five chart types, form fields, and PageBreak / Fixed. Same props as @formepdf/react, so anything you know from JSX just works. Includes a formePreview() SvelteKit route helper for the live preview UI and one-call renderDocument() / renderDocumentWithLayout() wrappers over @formepdf/core.

npm install @formepdf/svelte @formepdf/core

See the Svelte guide.

@formepdf/shared — framework-neutral serialization core. Extracted from @formepdf/react so both React and Svelte adapters (and future ones) share the same document-model types, Style mapping, CSS shorthand parsing, Font registration store, Canvas recorder, chart kind builders, and semantic-component defaults. Internal-facing but published for adapter authors.

Changed

  • @formepdf/react — internal refactor: framework-neutral internals moved to @formepdf/shared and re-exported. Public API is unchanged — all 215 tests pass without modification.
  • @formepdf/core — new renderSerializedDoc(doc, options?) and renderSerializedDocWithLayout(doc, options?) exports. Accept a pre-serialized FormeDocument (JSON), used by the Svelte adapter to hand off after its SSR-then-parse pass.
  • All other npm packages: version alignment only, no functional changes.

Full changelogs

  • @formepdf/svelte
  • @formepdf/shared
  • @formepdf/react
  • @formepdf/core

Svelte adapter contributed by @cmjoseph07 in #21.

v0.10.5

Choose a tag to compare

@danmolitor danmolitor released this 03 Jul 15:19

Forme 0.10.5

Patch release. Fixes two table page-break regressions reported on GitHub. Zero breaking changes — safe drop-in from 0.10.4.


What's fixed

1. Orphan header at the bottom of a page

When a <Table> with <Row header> sat low enough on a page that the header alone fit in the remaining space but the first body row did not, the header rendered at the bottom of the page with nothing beneath it, then repeated above the actual rows on the next page.

Before:

Page 1: [ …content… ]  [Header]                ← orphaned
Page 2: [Header]  [Row 1]  [Row 2]  …

After:

Page 1: [ …content… ]
Page 2: [Header]  [Row 1]  [Row 2]  …

Closes GitHub issue reported against 0.10.4.

2. Long-token header text contamination

When a header cell contained a long token with no line-break opportunities (e.g. "Amount".repeat(40)), it wrapped to many lines, and the table starting low on a page could leak header text onto the previous page while the actual table rendered across three pages instead of two.

Same fix — the table now moves cleanly to a fresh page where the tall header has full page height to wrap into. No cross-page contamination, correct page count.


What changed under the hood

The 0.10.4 pre-fit check in layout_table gated only on total_header_h > remaining_height. It now also folds in the first body row's measured height:

let needed = total_header_h + first_body_h;
if needed > cursor.remaining_height() && needed <= fresh_page_available {
    // page-break the table before the header, not after it
}

The fresh-page cap keeps the 0.10.4 !is_header cell-overflow guard as the safety net for the rare case where header + first row is genuinely taller than a page.

Two new integration tests — both verified to fail without the fix and pass with it:

  • test_table_header_no_orphan_when_first_body_row_doesnt_fit
  • test_table_long_header_text_no_page_contamination

Which packages carry the fix

The fix lives in the Rust engine, so every consumer of engine 0.10.5 picks it up automatically:

  • npm: @formepdf/react, @formepdf/core, @formepdf/renderer, @formepdf/cli, @formepdf/hono, @formepdf/next, @formepdf/mcp, @formepdf/resend, @formepdf/sdk, @formepdf/tailwind, @formepdf/templates — all 0.10.5
  • PyPI: formepdf0.10.5
  • crates.io: forme-pdf0.10.5
  • Go: github.com/formepdf/forme-gov0.10.5
  • Docker Hub: formepdf/rasterizer:0.10.5, formepdf/forme:0.10.5
  • VS Code Marketplace: forme-pdf0.10.5

Upgrade

# npm — bump both simultaneously; @formepdf/core carries the WASM
npm install @formepdf/react@0.10.5 @formepdf/core@0.10.5

# Python
pip install --upgrade formepdf==0.10.5

# Rust
cargo update -p forme-pdf

# Go
go get github.com/formepdf/forme-go@v0.10.5

# Docker (self-hosted)
docker pull formepdf/forme:0.10.5

Full changelogs

v0.10.4

Choose a tag to compare

@danmolitor danmolitor released this 05 Jun 15:11

Forme 0.10.4

Four layout bug fixes, all user-reported. The
table header one is silent — upgrade if you use
<Row header>.


Fixed

Tables with <Row header> no longer inflate
page count 3–5×

When a table started low enough on a page that
the header didn't fit before a page break, the
engine emitted multiple near-duplicate pages —
the same body rows repeated, with the header
visibly "doubling and sliding one column to the
right" on each successive page. A 24-row,
5-column table produced 8 pages where the same
content without a header row produced 3.

// Before: 8 pages, garbled doubled headers
// After:  3 pages, correct

  
  
    {headerCells}
    {rows.map(r => {...})}
  

<View> wrapping a <Table> no longer
auto-grows to roughly the page height

measure_node_height had no handler for
Table or TableRow, so they fell into a
generic path that summed cell heights instead
of taking the max — a 3-column row of 16pt
cells measured to 48pt, and any wrapping
<View> inherited that inflation. Now
delegates to the same helpers layout_table
already uses, so measurement matches what
renders.

<Svg viewBox="…"> content scales to fit
the display box

SVG paths previously rendered at raw viewBox
coordinates and overflowed — the viewBox
parameters were parsed but unused, and the
PDF scale was always 1.0. Now implements the
SVG viewport algorithm with xMidYMid meet
as the default preserveAspectRatio (uniform
min(sx, sy) scale + centering).

// Before: paths spilled outside the 200×80 box
// After:  scaled to fit

  …paths…

marginTop: 'auto' works in column layouts

Previously a no-op in flexDirection: 'column'
parents — only the horizontal version worked.
Now distributes slack the same way: top-only
pushes to bottom, both autos center, bottom-only
carries forward. Auto margins consume slack
before justifyContent, per the CSS spec.

// "Sign here" now sits at the bottom, not the top

  
    Sign here
  

Upgrade

npm install @formepdf/core@0.10.4 @formepdf/react@0.10.4
# plus any of: cli renderer hono next mcp resend sdk tailwind templates

Other consumers:

  • Rust: cargo add forme-pdf@0.10.4
  • Python: pip install formepdf==0.10.4
  • Go: go get github.com/formepdf/forme-go@v0.10.4
  • Docker: docker pull formepdf/forme:0.10.4
    docker pull formepdf/rasterizer:0.10.4
  • VS Code: update "Forme PDF Preview" in the
    Extensions panel