Releases: danmolitor/forme
Release list
v0.16.0 - PDF/A conformance
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 — sopdfA + pdfUano 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:
pdfAinrenderHtmloptions;--pdf-a <level>on theforme-htmlCLI. - 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
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.
v0.14.0 - HTML + print-CSS input path
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-childand:nth-of-typefamilies — with full cascade semantics including!important. - Paged media:
@pagesize and margins,:firstvariants, margin boxes withcounter(page)/counter(pages),break-*(plus legacypage-break-*aliases),orphans/widows, and@mediamedia-type evaluation — print is the native media type. - Tables:
border-collapseemulation,<thead>repetition across page breaks, colspan/rowspan,vertical-align(and legacyvalign). - Typography: justified text,
text-transform,letter-spacing, and provided fonts viaoptions.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-facefamilies, 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_widthignoredruns(and measured leafHeadings as 0, and whole multi-line strings instead of the widest line) — flex rows could collapse styled text to one character per line. col_spanwas 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: falseon 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: avoidin 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
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
/Outlinesentries and/Count 2for a singlebookmarkprop. 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 samebookmark, andcollect_bookmarksrecurses — so it found both. Wrappers no longer carrybookmark; 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 forBookmarknodes silently missed most bookmarks. The PDF was never wrong here — the outline was always complete — this was aLayoutInfoblind spot. PDF output is byte-identical after the fix (verified by hash:8f41f7e9fb58b3cbbefore and after). - The marker reports
nodeType: "Bookmark"instead of"None". The unset node type fell back tokind.to_string()and leaked theDrawCommand::Nonevariant name — a value never present inElementNodeType.
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
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.
ElementNodeTypeis now a closed literal union of the 30 nodeType values the engine actually emits (wasstring). Code likeif (element.nodeType === 'Heading')— silently wrong before, since reality is discrete'H1'–'H6'— is now a TypeScript compile error. Same treatment forElementKind(10 values) and 11 style enums (ElementFlexDirection,ElementJustifyContent,ElementAlignItems,ElementAlignContent,ElementFlexWrap,ElementFontStyle,ElementTextAlign,ElementTextDecoration,ElementTextTransform,ElementOverflow,ElementPosition). All exported for narrowing.ElementStyleInfoexpanded 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.textContentonElementInfois now typed asstring | null | undefined(wasstring?). Only populated onTextLineleaves — every non-TextLinenode emitsnullat runtime. The old declaration made consumers reach for the wrong node.- Every layout-time transform now documented explicitly on the
ElementInfoJSDoc:<Table>unwraps into siblingTableRownodes,<OrderedList>becomesList+ListItem+Lbl,<Fixed>splits intoFixedHeader/FixedFooter, headings are discreteH1–H6,<Text>block content is split intoTextLineleaves, 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 asElementFlexDirection. Same story for the other 10 style enums.element.textContentchanging fromstring | undefinedtostring | null | undefinedwill flag code that assumed non-null. Fix: use the newgetNodeText()helper (it handles this correctly by walkingTextLinechildren), or explicitly handlenull(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:
- Every emitted
nodeType/kind/ style enum → member of its declared union (runtime test) - Every declared
ElementNodeType→ appears in the rich fixture (coverage tripwire — catches "component shipped without structural coverage") - 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
@formepdf/core— the substantive work
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
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, H1–H6, 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
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
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 (H1–H6), 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/coreSee 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
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_fittest_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— all0.10.5 - PyPI:
formepdf—0.10.5 - crates.io:
forme-pdf—0.10.5 - Go:
github.com/formepdf/forme-go—v0.10.5 - Docker Hub:
formepdf/rasterizer:0.10.5,formepdf/forme:0.10.5 - VS Code Marketplace:
forme-pdf—0.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.5Full changelogs
- Engine:
engine/CHANGELOG.md - Server:
server/CHANGELOG.md - Individual packages:
packages/*/CHANGELOG.md
v0.10.4
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 templatesOther 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