Changelogs
All notable changes to @barrierbreak/a11ydocs-pdf are documented here. This project follows Semantic Versioning.
v2.5.4 - August 13, 2026
- Visual regression baselines refreshed. No library change — the published code is identical to 2.5.3. The invoice fixture baselines had been stale since 2026-05-29, so
visual:comparefailed on every run and could not have caught a real regression. The drift traces to two intentional changes from 2026-06-29: the table-border work, which drops a cell's first text baseline by 0.75 line heights so the top line stays inside a bordered cell, and the bundled-fonts work, which replaced the Standard-14 advance-width heuristic with real font metrics. Rendering has been unchanged from 2.1.0 through 2.5.3;visual:compareis now clean at zero differing pixels.
v2.5.3 - August 13, 2026
- Cognitive-complexity refactor of the layout and serialization paths. The 35 largest functions in
src/(plus the license dashboard's license-detail page) were split into named steps: document/incremental serialization preparation, page content-stream emission, annotation dictionaries, table and list layout, HTML parsing, template block flow, vertical text positioning, and code-block pagination. Output is byte-identical to 2.5.2 — verified by rebuilding a document exercising every annotation and form-field type, an incremental update touching info/XMP/appended/edited pages and a field value, and a multi-page code listing with line numbers against both trees — and the visual fixtures render the same pixels. No public API change.
v2.5.2 - August 13, 2026
- No insecure randomness fallback.
secureRandomBytes(RSA seed/padding for signing) previously fell back toMath.random()whenglobalThis.crypto.getRandomValueswas missing. It now throwsINVALID_ENCRYPTIONinstead, so a runtime without WebCrypto fails loudly rather than producing predictable bytes. Node 22+ and every supported browser always provide WebCrypto, so no supported runtime is affected. - Catastrophic-backtracking risk in three parsers. The HTML tag scanner (
htmlToBlocks/expandHtmlBlocks), the SVG attribute scanner, and the trailing-whitespace strips could take super-linear time on adversarial input. The tag pattern no longer ends in an ambiguous optional/, the SVG attribute value group is optional so a long name without=never backtracks, and the whitespace strips usetrimEnd()or a linear scan. Parsing results are unchanged. - GIF local color tables no longer mis-advance the reader. Dead local color-table state was removed and the byte cursor now advances explicitly; only the global table was ever used for decoding, as before.
- Repo-wide static-analysis sweep (~420 SonarCloud findings): nested ternaries extracted, repeated unions given type aliases,
Number.parseFloat/.at()/replaceAll/codePointAt/String.rawpreferred, dead stores and unused imports dropped, duplicatePdfDocument/PdfEditDocumenthelpers shared, andimageFitRecttakes a single options object. Rendered output is byte-identical: the visual fixtures produce the same pixels as 2.5.1. - Dockerfiles install with
npm ci --ignore-scripts; the visual-preview example server validates request paths against an allow-list and re-checks the canonicalized path against its served root, and resolvespdftoppm,install_name_tool, andopento absolute paths in trusted bin dirs.
v2.5.1 - August 10, 2026
defaults.fallbackFonts. A document-wide fallback list, consulted by every text path that omits its own — paragraphs, headings, code, lists, tables, rich runs, and HTML — so a symbol font covering glyphs the body font lacks (✓,✕, arrows, CJK) is configured once atcreateDocumentinstead of on every call. Precedence is per-call → flow state → document default; a per-call list replaces the default rather than extending it, and[]opts a single call out.RichTextOptions.fallbackFontsis new too, so rich runs can set the list at block level.page.list()ignored registered font families. A family name in the list'sfont(or nowfallbackFonts) reached layout unresolved instead of being mapped to a concrete face, so it rendered with the fallback font or threwUNSUPPORTED_CHARACTER.page.list()andcontainer.list()now resolve families like every other block;flow.list()was unaffected.
v2.5.0 - August 05, 2026
- Compliance errors name the offending page and text. A built-in font error under
pdfua-*/pdfa-*/wtpdfsaid only that embedded fonts were required, leaving the offending content to be hunted by hand. The message now appends the first three offenders (page 2 · font "Courier" · "const answer = 42;") and the issue carries alocationsarray (PdfComplianceLocation[]: 1-basedpage,kind,font,annotation,name,snippet) with up to 25 entries, on bothvalidateCompliance()and theINVALID_COMPLIANCEerror'sdetails.issues. defaults.codeFont. Code content resolves its font as per-callfont→defaults.codeFont→defaults.font→ built-inCourier, coveringflow.code(),codetemplate blocks, and inline<code>/<kbd>/<samp>runs from HTML. Point it at an embedded monospaced family (e.g. the bundled Liberation Mono) to keep listings out of the built-in-font compliance error.- Code blocks no longer hardcode built-in
Courier. A document whosedefaults.fontis embedded previously still emitted Courier for every code block, silently failing PDF/UA and PDF/A. Setdefaults.codeFontto keep a monospaced face; documents that set neither default are unaffected.
v2.4.0 - July 30, 2026
- Multi-page continuity for code blocks. A listing that spans pages or columns already split line-by-line; now it can say so.
continuedLabeldraws a note at the top of every fragment after the first andcontinuesLabelat the bottom of every fragment that carries on — a fixed string, or a callback given{ fragmentIndex, firstLineNumber, lastLineNumber, totalLines }. Notes are artifacts (never read as code) and reserve their own line, so they never overlap the listing. Styled withcontinuationFontSize,continuationColor, andcontinuationAlign. - Open borders at a split. A bordered block now omits the rule under a fragment that continues and above one that resumes, so consecutive fragments read as one listing instead of a stack of boxes.
openSplitEdges: falserestores a closed box per fragment. - One
Codestructure element per page instead of per fragment. A block that split across the columns of a single page produced a separateCodeelement per column; the fragments now share the page's element, and a new one starts only on a new page.
v2.3.1 - July 30, 2026
- A block missing its content field crashed with a bare
TypeError. Template pagination estimates every block's height before any validation runs, so{ type: "paragraph", code }(acodeblock mistyped),{ type: "heading" }with notext, or arichParagraph/list/tablewith noruns/items/rowsdied onCannot read properties of undefined (reading 'replace' | 'map' | 'length')from deep inside layout, naming neither the block nor the field. Every text-bearing block now throwsPdfEngineError(INVALID_TEXT) naming what was expected and which field it belongs in, from both the render and estimate paths. - An
htmlblock given the wrong field rendered nothing at all.{ type: "html", text: "<p>…</p>" }(rather thanhtml) silently produced no output, so content vanished from the document with no error.htmlToBlocks()and thehtmlblock now throwINVALID_TEXTnaming thehtmlfield for a non-string source; an empty or whitespace-only string still yields no blocks, as before.
v2.3.0 - July 30, 2026
- Accent folding, and
normalizeText: "ascii-only". The built-in fonts encode ASCII only, so European text failed withUNSUPPORTED_CHARACTEReven though it has an obvious ASCII form.htmlToBlocks()'s defaultnormalizeText: "ascii"now strips accents through Unicode decomposition (Muñoz→Munoz,café→cafe) plus an explicit map for letters that do not decompose (Æ→AE,ß→ss,Ø→O,Þ→TH,ı→i,Ł→L,ŋ→ng). Characters with no ASCII form — Devanagari, CJK, emoji — are still left alone rather than silently dropped; the new"ascii-only"mode opts into dropping them for pipelines that must never fail and cannot embed a font. The folding helper is exported asfoldToAscii(text, asciiOnly?). - The Latin-1 named entities.
é,ñ,ü,Ø,ß,Æ,þand the rest of the accented set now decode; previously they passed through as literalétext. UNSUPPORTED_CHARACTERnow says what failed and how to fix it. The message names the character and its code point ("日" (U+65E5)) and points atregisterBundledFonts()/embedTrueTypeFont()/loadGoogleFont()/fallbackFonts/ text normalization.detailsgainsfontandexcerpt— a window of the surrounding text — so the offending field is findable in a large document.
v2.2.1 - July 30, 2026
- A
codeblock with a missing or misnamed source crashed with a bareTypeError. Template pagination estimates every block's height before any text validation runs, so{ type: "code" }with nocodefield (ortextused by mistake, or an undefined database column) reachedestimateCodeHeightand died onCannot read properties of undefined (reading 'replace')with no hint about the cause. Both the render and estimate paths now throwPdfEngineError(INVALID_TEXT) naming thecodefield and the type received. - An empty
codeblock no longer throws or draws an empty band. A blank or whitespace-only source (a CMS field with no content) previously threwINVALID_TEXTfor""yet rendered a stray empty band for" ". Both now render nothing and consume no vertical space.
v2.2.0 - July 30, 2026
htmlToBlocks(), anhtmltemplate block, andflow.html()— HTML to PDF blocks. Converts the HTML that CMS fields and rich-text editors emit into template blocks: headings, paragraphs with inline runs, nested ordered/unordered lists, tables (th/scope/colspan/rowspan/caption),precode blocks, blockquotes, rules, and images. Output is ordinary block data, so it paginates, tags itself, feeds the TOC/auto-outline, and takes stylesheets.htmlblocks are expanded before pagination, so headings inside them still reach the TOC — including fromrenderDocumentJson, which lets pure-JSON callers ship rich text. Entity decoding covers named, decimal, hex, and double-encoded (&mdash;) references, exported on its own asdecodeHtmlEntities().normalizeText: "ascii"(default) folds smart quotes, dashes, ,—, arrows, fractions, and currency symbols to ASCII so the built-in ASCII-only fonts render converted content;"none"keeps the original characters for embedded Unicode fonts. Untrusted input is handled by design: scripts, styles, embeds, and form controls are dropped with their content, link schemes are allow-listed (javascript:renders as plain text), and malformed markup recovers instead of throwing.codetemplate block andflow.code()— preformatted code blocks. Renders monospaced text with leading whitespace and hard line breaks kept verbatim (paragraphs collapse both), on a background band that repeats on every page or column the block spans, so a code listing longer than a page paginates itself. Options cover typography, band fill/border/padding, tab expansion, soft wrap with a hanging indent (or clipping), a line-number gutter, and ordered regex highlight rules;patternaccepts a string so rules stay JSON-serializable forrenderDocumentJson. Tagged as aCodeelement inside aPby default (tag: "P"or"Artifact"to change it). AddscodeBefore/codeAftertoFlowSpacingScale.- A rich paragraph ignored its runs'
highlightcolor.flow.richParagraph()(and sorichParagraphblocks) pushed underline/strike decorations but never the highlight fills, so a run withhighlightrendered without its background. The page-level rich-text API was unaffected.
v2.1.5 - July 20, 2026
- Security: certificate-recipient encryption used a predictable RNG for cryptographic material.
generateRandom20ByteSeed()(RSA public-key encryption seed) and the PKCS#1 v1.5 padding bytes inrsaEncryptPkcs1were both filled withMath.random()— not cryptographically secure, so the seed and padding were theoretically predictable to an attacker who could observe enough output. Both now draw fromglobalThis.crypto.getRandomValues(CSPRNG), falling back toMath.random()only in runtimes without a Web Crypto API. No API change; affects only documents encrypted for certificate recipients.
v2.1.4 - July 20, 2026
- A synchronous output method could double-charge a document mid-flight
writeTo().writeTo()is async and stays un-finalized across its authorize await gap. IftoUint8Array()/toArrayBuffer()/toBlob()was called on the same instance during that gap (writeTo()started but not awaited), it ran its own full license check and finalized the document first;writeTo()then still charged again when it resumed — one logical document, billed twice. The reentrancy guardwriteTo()already used against itself now also blocks the three sync methods, on bothPdfDocumentandPdfEditableDocument.
v2.1.3 - July 20, 2026
- Security: retrying a document after a rejected license check bypassed metering entirely. Every serialize entrypoint (
toUint8Array,toArrayBuffer,toBlob,writeTo, on bothPdfDocumentandPdfEditableDocument) marked the document "finalized" before the license guard ran. If the guard threw (quota exceeded, blocked license, a transient error), the document was left finalized-but-unlicensed: any retry of any output method on that same instance serialized directly with no license check, no metering, no watermark — free output, triggered by exactly the retry pattern the library's own error messages recommend. Fixed by finalizing only after the license-gated operation actually succeeds; a rejected attempt can now be retried and is re-checked every time, just like a document that was never serialized. Legitimate caching (repeat calls after a successful serialize) is unaffected.
v2.1.2 - July 20, 2026
- Regression:
v2.1.1shipped with the wrong baked license server URL. Publishingv2.1.1ran the release build withoutA11YDOCS_LICENSE_SERVER/A11YDOCS_LICENSE_PUBKEYSset, so the bake step fell back to the local dev defaults (http://localhost:8787, no public keys) instead of preservingv2.1.0's correct baked production values.BecausebakedServerUrl ?? process.env.A11YDOCS_LICENSE_SERVERonly falls through onnull/undefined— not on a wrong-but-truthy string — this also silently ignored anyA11YDOCS_LICENSE_SERVERa caller set explicitly, so the override didn't help either. Re-baked and republished with the correct values (verified directly against the published tarball, not just the local build) asv2.1.2.v2.1.1should not be used.
v2.1.1 - July 20, 2026
renderDocumentJson()now auto-authorizes. It'sasync(real font/image loading) but its final serialize used the document's synchronoustoUint8Array(), so — unlike every other async output method — it never awaited license authorization. Zero-config (Node env vars) and browser callers had to know to callcheckout(pages)manually first, undocumented. Fixed by authorizing once layout has run and the real page count is known.renderDocumentJsonSync()is unaffected: it genuinely cannot await, same asPdfDocument.toUint8Array()— callcheckout()first as documented.- Dev-bypass / auto-guard precedence.
ensureLicenseAuthorized()(the async pre-check before serialize) checkedA11YDOCS_LICENSE_DEVbefore attempting to install the zero-config auto-guard, but the synchronous consume step right after it checks the auto-guard first. With both a realA11YDOCS_LICENSE_KEYandA11YDOCS_LICENSE_DEV=1set — an easy dev/staging misconfiguration — the async pre-check silently skipped authorization (thinking dev bypass covered it) while the sync step installed the real auto-guard with a fresh zero lease and threw "not yet authorized," on every async output method, not justrenderDocumentJson. Reordered to match.
v2.1.0 - July 17, 2026
- Canonical v3 + signed-proof guard install. The authorize signature now covers the watermark flag and text (
kid|key|granted|remaining|resetAt|nonce|watermark|watermarkText, text last so embedded|cannot shift fields) — a tampering proxy can no longer strip a trial watermark from a valid response. Server, core, and licensing client all moved to v3 together (nothing was deployed on v2). New public APIgetLicenseInstallChallenge()+setLicenseGuardWithProof(guard, proof): an external client authorizes against a core-issued single-use challenge and presents the signed response as install proof; the core verifies it against the baked public keys and installs the guard on the verified path. A trial proof pins the server's watermark over any guard decision, so a trial key cannot unlock clean output. The licensing client performs this handshake automatically on its first successfulcheckout(). - Auto-guard now verifies signed authorize responses. When the server's Ed25519 public keys are baked into the build (
A11YDOCS_LICENSE_PUBKEYS/A11YDOCS_LICENSE_PUBKEYenv at build time, kid-keyed for rotation), every/v1/authorizeresponse must carry a valid signature over the canonicalkid|key|granted|remaining|resetAt|noncepayload — bound to the request's fresh nonce, so a captured "allow" cannot be replayed and a spoofed server (DNS/proxy tampering) grants nothing (fail closed: no lease, generation refused; never a hard block). Works in Node (node:crypto) and the browser (WebCrypto). With no baked keys (dev builds) verification is skipped; the bake script warns loudly if a production URL is baked without keys. Known gap until canonical v3: thewatermarkflag is not part of the signed payload. - Watermark-unless-verified licensing. Only the server-backed auto-guard (
A11YDOCS_LICENSE_KEY) produces watermark-free output. Guards installed through the publicsetLicenseGuard()are now unverified: they can still meter and block, but every page they authorize is stamped with a fixedUNLICENSED - EVALUATION ONLYwatermark that the guard cannot override or blank. TheA11YDOCS_LICENSE_DEV=1bypass still allows generation but stampsDEVELOPMENT - NOT FOR DISTRIBUTION. This closes the two-line metering bypass (setLicenseGuard({ consume() {} })) — it now yields watermarked evaluation output instead of clean PDFs. Guards installed by@barrierbreak/a11ydocs-pdf-licensingare also unverified until the signed-proof handshake ships; production deployments should use the auto-guard. The playground continues to work, watermarked. hsl()color helper.hsl(h, s, l)converts to an RGB color, alongside the existingrgb/cmyk/gray/hex/colorhelpers. Saturation/lightness accept a0–1fraction, a percentage string ("87%"), or a0–100number;color()also parses CSShsl(...)strings (e.g.color("hsl(87, 87%, 80%)")).- Report pagination controls. Every template block now accepts
breakBefore/breakAfter(force a fresh region before/after),keepTogether(never split; for paragraphs this disables column line-splitting), andwidows/orphans(minimum lines to carry into / leave before a break when a paragraph splits across columns). - Table of contents. A
{ type: "toc" }template block builds a linked, page-numbered contents list from the document's heading blocks. Page numbers resolve after layout, so the TOC can sit at the front and point forward. Tagged for PDF/UA as aTOC→TOCI→Reference→Linkhierarchy, with the entry text inside theLink(accessible name) and the link annotation nested in the sameLink. Options:maxLevel,font,fontSize,lineHeight,indent,link,pageNumbers, a decorativeleader("dots"|"line"|"none") withleaderColor(drawn as an artifact so AT never announces it),levelStyles(per-level font/size/color), andnumbering(hierarchicalLblsection numbers like1.2). Page numbers are right-aligned into a column. - Cross-references. Any block can declare a named
anchor; a{ type: "crossRef", to, text }block links to it and substitutes the resolved page number (via a{page}placeholder or appended). - Footnotes. A
footnoteon a text block appends a numbered marker and renders the note at the bottom of the page it lands on; the paginator reserves the space so notes never overlap content. - Smarter tables.
columnWidths: "auto"sizes columns to their content (natural width scaled to fill, shrinking toward the longest word).footerRows: Nrepeats the lastNrows at the bottom of every page when a table splits (mirroringheaderRows). - Image fit, positioning, and clipping. Raster image placements accept
fit("fill"|"contain"|"cover"),position(9 anchors), andclip("none"|"circle"|"ellipse"), applied consistently across page, flow, and template image blocks. - Page background color.
createDocumentaccepts apageBackgroundcolor that fills every page (viaaddPageandrenderTemplate) behind all content, tagged as an artifact;renderTemplatealso accepts apageBackgroundthat overrides it per template. - Watermarks.
renderTemplateaccepts awatermarkoption (a string orTemplateWatermarkOptions) that draws a diagonal, centered, semi-transparent mark behind every page, auto-sized to span the page and always tagged as an artifact. - Master-page backgrounds.
renderTemplateaccepts abackgroundcallback returning blocks drawn behind every page's content (tints, rules, logos). - Rich text in list items. List items accept
InlineTextRun[]bodies (mixed fonts, sizes, colors, bold/italic, links) in addition to plain strings, across page-levellist(), flow lists, and template list blocks — with layout estimation handling the rich runs correctly. - Text formatting run options. Superscript/subscript, baseline
rise, render modes (strokeoutline,fillStroke,invisible), and inlinehighlight(marker-pen fill behind text) — on whole text calls,richText/richParagraphruns, and template paragraphs. - Per-cell table borders. Cells accept individual border overrides (per-side width/color) on top of the table-wide
borders/borderColoroptions. - Liberation fonts bundled. Metric-compatible embedded replacements for Helvetica/Times/Courier (SIL OFL) ship in the package with fetch/verify scripts — the recommended fonts for PDF/UA output, where standard-14 name references don't satisfy the embedded-font requirement.
- Host metadata telemetry. The licensing auto-guard reports best-effort host context (hostname, OS, CPU/memory, cloud provider hints, job id/description via
setJobContext()orA11YDOCS_JOB_*env) with authorize/usage calls, so operators can slice usage by workload. Node-only, never includes document content. - TOC, footnotes, and cross-references now inherit the selected font from any source. They previously ignored the template's font and rendered in the built-in default. They now resolve the font from
page.font, the documentdefaults.font, or the stylesheetparagraphstyle (still overridable per feature/level). PdfPage.container()andPdfPage.structure()now inherit the document's font families and text defaults. Text drawn through a page-level structure container previously lost the default font and registered families (falling back to Helvetica); this also drove the TOC font regression above.- Master-page background text is no longer clipped off the top of the page. The
backgroundflow started at the exact top edge, so a background paragraph's first line was drawn above the page and never appeared. It now starts at the top content edge like the body flow. - Horizontal text scaling now affects layout.
horizontalScaling(theTzoperator) is now included in width measurement, so wrapping, alignment, justification, and highlight/annotation rectangles are correct for condensed/expanded text.
v2.0.0 - June 25, 2026
- PDF generation is now secure by default. With no license guard installed, generation (
toUint8Array/toBlob/toArrayBuffer/save) is refused with the newPdfErrorCode.LICENSE_REQUIRED. Install a guard via the licensing client (@barrierbreak/a11ydocs-pdf-licensing) orsetLicenseGuard(). For the library's own tests, examples, and local trials, set the environment variableA11YDOCS_LICENSE_DEV=1(Node only) to allow unlicensed generation. In the browser there is noprocess.env, so a guard must be installed explicitly (the playground installs a permissive one). - License guard hook for usage metering / limits. New
@barrierbreak/a11ydocs-pdf/license-guardsubpath exposessetLicenseGuard(guard)andisLicenseDevBypass(): a guard'sconsume({ documents, pages })is called immediately before a document is serialized, once per finalized document, and may throwPdfErrorCode.LICENSE_QUOTA_EXCEEDEDto block generation. Both dimensions are reported, so usage can be shown as e.g. "200 PDFs, 5200 pages". The licensing client (lease/checkout, Ed25519 verification, fail-closed,meter: "pdfs" | "pages") ships separately in the private package@barrierbreak/a11ydocs-pdf-licensing, and a reference license server (Hono + Drizzle + better-auth + Postgres) lives inlicense-server/. New error codesLICENSE_REQUIREDandLICENSE_QUOTA_EXCEEDED.
v1.9.0 - June 24, 2026
- AES PDF encryption now works in the browser. AES (
aes-128,aes-256,aes-256-r6) previously required Node'scryptoand threw"AES PDF encryption requires Node.js crypto support"in browsers. The library now falls back to a built-in, zero-dependency AES-CBC + SHA-256/384/512 implementation whennode:cryptois unavailable, so encrypting and decrypting run client-side. Output is byte-for-byte compatible with the Node path (verified against Node's native crypto), so a document encrypted in the browser opens with the same password in any reader. Node continues to use its native crypto for speed. The only browser requirement is a secure-random source for the IV/key, read from the Web Crypto API. A "Browser AES encryption" playground sample and docs were added.
v1.8.0 - June 24, 2026
- Optional veraPDF validation adapter. New
validateWithVeraPdf(bytes, options)shells out to a veraPDF executable when one is available and returns the supplied custom-checker issues otherwise — veraPDF is never bundled and adds no dependencies. The result reports itssource("verapdf"or"custom"), whether veraPDF wasavailable, thecompliantflag, and issues normalized toPdfComplianceIssue. The flavour is auto-derived from theconformanceprofile (e.g.pdfa-2b→2b,pdfua-1→ua1) or set explicitly; the executable resolves fromverapdfBin, thenVERAPDF_BIN, thenverapdfonPATH. Node-only:node:*modules are imported lazily so browser bundles never reach this code. The defensive report parser is exported separately asparseVeraPdfReport.
v1.7.2 - June 24, 2026
- Page- and container-level
table()still ignoredtextDefaults(incomplete v1.7.1 fix).v1.7.1seeded the cell base font fromtextDefaultsonly on the flow (flow.table) path. The directpage.table()and structure-containertable()methods passed calloptionsstraight through as the cell base and the table-wide font, so with no per-callfonta styled/header cell hit the hardcodedbase.font ?? "Helvetica"and plain cells inherited an unseeded table font —pdfua-1INVALID_COMPLIANCEagain. Both methods now seedfont/fontSize/color/kerningfromtextDefaults(call options still win). - Page- and container-level
list()had the same gap.page.list()and the structure-containerlist()passedoptionstonormalizeListOptionswithout mergingtextDefaults, so a list with no explicitfontfell back to"Helvetica". They now seed list defaults fromtextDefaultstoo, matching the flow list path fixed in v1.7.1.
v1.7.1 - June 24, 2026
- Lists and tables ignored
defaultFont/ documenttextDefaults. Paragraphs and headings inherit document-wide text defaults, but the list and table block paths merged only the flow-configured defaults — never the documenttextDefaults— so list items and table cells fell back to the hardcoded"Helvetica". Underpdfua-1this producedINVALID_COMPLIANCE(visible text in a non-embedded font). List render/estimate and table render/row-height paths now mergetextDefaultsat lowest priority (matching paragraph behavior), and table cell base styling seedsfont/fontSize/color/kerningfromtextDefaultstoo. A font set "for the whole page" now applies to lists and tables without an explicit per-block override. - List height estimate threw when block options were present. The estimate path only injected the auto-width when a list block carried no options, so passing any option (e.g.
font) dropped the width andnormalizeListOptionsthrewwidth must be a finite number. The estimate path now always seeds the flow width, still overridable by an explicitoptions.width.
v1.7.0 - June 16, 2026
renderTemplatenow wraps all content under a singleDocumentstructure root by default. A template produces a complete document, but without an explicitstructureRootits tagged content (headings, paragraphs, figures, tables) was emitted as a flat list of top-level structure elements directly underStructTreeRoot— PDF/UA-1 expects a single top-levelDocumentelement.renderTemplatenow defaults the structure root toDocumentwhen the document was created without one. PassstructureRoot: falsetocreateDocumentto opt out and keep bare roots; an explicitstructureRootis still respected. The low-leveladdPage/manual-structure API is unaffected (no implicit wrapper).
v1.6.2 - June 16, 2026
- Template running header drawn last in the content stream.
v1.6.1hoisted a running header's structure roots to the front of the tag tree, but the body was still drawn first in the page content, so the header logoFigurereceived the highest marked-content id (MCID). Reading order and content order disagreed — content-order tools and PDF/UA validators saw the logo last. The header's content operators are now hoisted to the front of the page content stream alongside its structure roots, so the header is drawn first (lowest MCID) and reading order matches content order. Footers/page numbers are unaffected (they correctly remain last). Safe because text operators are self-contained (BT/ETwith an absoluteTm) and image operators are wrapped in their ownq/Q. richParagraphthrew on non-string run text. A run whosetextwasnullorundefined(e.g.localStorage.getItem(...)returningnullon a miss) crashednormalizeRichRunswithCannot read properties of null (reading 'length'). Such runs are now dropped like empty runs, so bad input degrades to nothing instead of aborting the whole render.
v1.6.1 - June 15, 2026
- Tagged links never referenced from the structure tree. A link annotation was only tied to a
Linkstructure element when the caller passedtag: "Link"explicitly. Inline links —flow.richParagraphruns, page-levelrichTextruns, table-cell links, and paragraph text-annotation links — and the publicpage.link()omitted it, so the annotation was emitted with no OBJR back-reference (an untagged annotation, a PDF/UA-1 failure). An unspecified link tag now defaults toLink; passtag: "Artifact"to opt out for decorative links. - Inline link text was stranded outside its
Linkelement.flow.richParagraphplaced the visible link glyphs under the surroundingPand left theLinkelement holding only the annotation. The link's text is now grouped inside theLinkstructure element together with its OBJR. - Marked content read after child elements. A structure element's
/Karray always listed child elements before the node's own marked content, so a paragraph like"Page URL: " + linkwas announced as the link first, then the leading text./Know interleaves a node's own marked content with its child elements in content (reading) order; pure containers still keep their children's insertion order, so the hoisted running header is unaffected. - Per-cell header cells emitted no
/Scope. A table cell promoted toTHpurely viacell.header(outside anyheaderRows/headerColumnsband — e.g. a row-label column) was written without a/Scope, which auditors flag as a missing row header. Such cells now default to/Scope /Row(still overridable viacell.scope). - Template running header read last in the tag tree. Headers/footers render after the body, so a header logo
Figurelanded at the end of the page's structure order instead of the start. Header structure roots are now hoisted to the front of the reading order; footers remain last (their correct final-appearance position).
v1.6.0 - June 10, 2026
- WOFF2 decoding — new
@barrierbreak/a11ydocs-pdf/woff2subpath export.decodeWoff2(bytes)converts a.woff2file (the format browsers and font CDNs serve) into standard TTF/OTF bytes ready forembedTrueTypeFont()/registerFontFamily(). Implements a zero-dependency Brotli (RFC 7932) decompressor — full prefix codes, context modeling, block switching, and the 122 KB static dictionary with all 121 word transforms — plus WOFF2 glyf/loca triplet reconstruction and hmtx transform reversal. Ships as a separate subpath so the embedded dictionary never loads with the core library.isWoff2(bytes)andbrotliDecompress(bytes)are also exported. loadGoogleFont()now works in browsers: when Google serves a woff2 file, the decoder is loaded on demand and the result is returned as TTF bytes. Node continues to receive a plain.ttf/.otfwith no decoder loaded.- Table cell styling: per-cell
font,color,align, andbackgroundoverrides onTableCellDefinition(joiningbold/italic). Cell fonts resolve registered families and built-in variants. - Table decoration options:
borderColor/borderWidthdraw the cell grid,headerBackgroundfills header cells, andzebra(boolean or color) stripes alternate body rows. All decoration is emitted as tagged-PDF artifacts. - Rich text in table cells: cells accept inline
runsinstead oftext— mixed fonts, sizes, colors, bold/italic, underline/strike, and links wrap together inside the cell, with row heights tracking the tallest line. fallbackFontsentries naming a registered font family never matched (the family name was passed through as if it were a built-in font, so fallback glyph coverage silently failed). Family names infallbackFontsnow resolve to the matching face, honoring the call'sbold/italicflags and case-insensitive matching.
v1.5.5 - June 10, 2026
- Table cells accept
boldanditalicflags ({ text: "Results", header: true, bold: true }). The flags resolve against the table font: a registered family picks the matching face, a built-in base family picks its standard-14 variant. Works inpage.table(),flow.table(), structure-container tables, and templatetableblocks; row heights account for the styled face. - Registered font families did not resolve in
flow.table(),flow.list(), and their height estimators — a template withpage: { font: "Roboto" }and atableorlistblock threwUnknown font "Roboto"even though the family was registered. All table/list entry points now resolve families like the text APIs do.
v1.5.4 - June 10, 2026
-
Font family names are now matched case-insensitively (like CSS
font-family). Registering a font as"Roboto"and referencing it as"roboto"or"ROBOTO"in text/template APIs now works correctly. This applies toregisterFontFamily(),renderTemplate({ fonts }), and all text APIs. -
loadExtensionFontBytes()now strips"./"prefixes from paths before resolving viachrome.runtime.getURL(), which would otherwise produce URLs that fail to fetch. -
loadExtensionFontBytes()now accepts absolute extension URLs (chrome-extension://...,moz-extension://...,https://...), passing them through without callinggetURL. -
loadExtensionFontBytes()wrapsfetchfailures with a detailed diagnostic message mentioningweb_accessible_resources, file path, and case-sensitivity — saving debugging time when a content script can't reach a bundled font.
v1.5.3 - June 10, 2026
renderTemplate()now accepts afontsoption — aRecord<string, FontFamilyInput>of family-name → faces that are registered before the template renders. Faces accept raw bytes, embedded handles, or built-in names.loadExtensionFontFaces(paths)— loads bundled extension font files and returnsFontFamilyInputwithout registering, so it composes directly withrenderTemplate({ fonts }).
v1.5.2 - June 10, 2026
loadExtensionFont(doc, family, paths)andloadExtensionFontBytes(path)— load font files bundled with a Chrome extension viachrome.runtime.getURL()and register them as a font family in one call. Fonts are fetched in parallel and passed toregisterFontFamily(). Exported as@barrierbreak/a11ydocs-pdf/chrome-extension-fonts.
v1.5.1 - June 10, 2026
PdfFlow.richParagraph()andPdfPage.richText()produced text ops without atag, so the tagged PDF structure tree showed thePelement with empty marked-content identifiers. The block-level tag now flows through to each run's options so the structure tree carries the correct MCIDs.
v1.5.0 - June 06, 2026
renderDocumentJson(document)andrenderDocumentJsonSync(document)— render a PDF from a single, fully serializablePdfDocumentJsonvalue. Fonts are declared by source (base64/ filepath/url/ Google Fonts) and referenced by name; images carry asrcinstead of raw bytes; headers/footers are static block arrays with{{pageNumber}}/{{pageCount}}/{{title}}/{{author}}tokens; document options (encryption, conformance, language, tagged, metadata) live on the root.renderDocumentJsonis async;renderDocumentJsonSynchandles fully inline (base64) documents.- Absolute positioning in
PdfDocumentJson: add aposition({ x, y, width?, height?, page?, origin? }) to a block to draw it at exact PDF-point coordinates instead of flowing. Positionable types:paragraph, the image formats,rect,textField,checkBox,signatureField. PdfDocument.getPage(index, origin?)andPdfDocument.pageCount— return aPdfPagebuilder for an already-created page so finished pages can be drawn on with the absolute-coordinate APIs.PdfErrorCode.INVALID_DOCUMENT_JSONfor a malformedPdfDocumentJson.ParsedPdfDocument.extractText()returned empty text for pages whose content stream was FlateDecode-compressed (common once a page carried a table, list, or header/footer). The extractor decoded the already-inflated lazy-stream bytes a second time and discarded the page; text andToUnicodedecoding now tolerate the pre-decoded bytes.
v1.3.0 - June 02, 2026
- Shape helpers on
PdfPage:rect(),line(),circle(),ellipse()with a simplifiedShapeStyle(inferred fill/stroke paint mode). - Text measurement:
PdfDocument.measureText(),measureTextBlock(), andmeasureRichText()return width/height/line-count without drawing. PdfPage.image()auto-detects the raster format (PNG, JPEG, GIF, BMP, TIFF, WebP, JPEG 2000) from the data.addPage({ origin: "top-left" })flips the y-axis so coordinates grow downward from the top edge — applied across the page drawing, annotation, and form-field methods.underlineandstrikeoptions on text and inline runs, drawn as decorative lines.- Named colors: text/shape
coloroptions accept names like"red", plus acolor()helper that resolves named and#hexcolors. - Shape and image helpers on
PdfStructureContainer(rect,line,circle,ellipse,image,path) andellipse/lineonPdfFlow. registerFontFamily()accepts raw font bytes per face (embedded automatically) in addition to handles and built-in names (FontFamilyInput).- Document-wide shape defaults via
createDocument({ shapeDefaults }), merged beneath per-call styles by the page shape helpers. - Layout-feedback methods
PdfPage.placeText()andplaceTextBlock()that draw and return{ width, height, (lineCount,) endY }for manual stacking. - "Choosing an API" guide page mapping common goals to the right API.
- The
fontoption now reports a clear "did you mean" error (INVALID_FONT) for an unknown string font instead of silently falling back, suggesting the closest standard-14 name. Unknown page-size names throwINVALID_PAGE_SIZEwith the same suggestion treatment. PdfStructureContainerbuilder methods (text,textBlock,list,table, image,link) now returnthisfor chaining, consistent withPdfPageandPdfFlow.
v1.2.1 - June 02, 2026
- The
fonttext option now accepts a font-family name registered withregisterFontFamily()(and anystring-typed value), instead of rejecting it at compile time.PdfFontwidened toFontName | PdfEmbeddedFont | (string & {}), preserving built-in name autocomplete.
v1.2.0 - June 02, 2026
- Inline rich text:
PdfPage.richText()andPdfFlow.richParagraph()lay out an array ofInlineTextRunsegments that flow on the same line and wrap together, each with its own font, weight, size, color, and optionallink. richParagraphtemplate block (TemplateRichParagraphBlock) for inline rich text inrenderTemplate().- Inline rich text supports
align: "justify", line-leveldirection("ltr" | "rtl" | "auto"), andwritingMode: "vertical"(single-column stacking); flow and template rich paragraphs wrap and split across columns and pages. - Vertical alignment:
verticalAlign("top" | "middle" | "bottom") on table cells (TableOptionsdefault and per-TableCellDefinition). - Vertical alignment within a fixed-height box via
height+verticalAlignonTextBlockOptionsandRichTextOptions. - New exported types:
InlineTextRun,RichTextOptions,FlowRichTextOptions,TemplateRichParagraphBlock,VerticalAlign.
v1.1.0 - June 02, 2026
boldanditalictext options that resolve to the matching font face for the standard-14 base families (Helvetica, Times, Courier)PdfDocument.registerFontFamily()to group font faces (built-in or embedded) under a name, selectable with thebold/italicflags- Document-wide text defaults via
createDocument({ defaults })(font, bold, italic, fontSize, color, kerning, direction) - Unit helpers
mm(),cm(),inch(), andpt()for authoring layouts in physical units PdfDocument.save(path)convenience alias forwriteToFile()PdfPagebuilder methods (text, images, annotations, form fields, graphics state, transforms) now returnthisfor fluent chainingbold/italic, registered font families, and document defaults resolve uniformly acrosspage.text()/textBlock(), thePdfFlowcursor API,renderTemplate()blocks, and structure containers
v1.0.0 - May 11, 2026
- Zero-dependency PDF 1.4+ generation engine in TypeScript
- PDF 1.5 object streams and 2.0 encryption support
- Text rendering with 14 standard fonts plus embedded TrueType (Unicode, kerning, ligatures, RTL/Arabic)
- Image support: JPEG, PNG (transparency/alpha)
- Tagged PDF for accessibility: PDF/UA-1, structure trees, MCID-based marked content
- Flow-based coordinate-free layout engine
- Template-based report/document rendering with headers, footers, page numbers, and auto-outline
- Forms (AcroForm): text, checkbox, radio, choice, push button, signature
- Annotations: URI links, highlights, notes, free-text
- Encryption: rc4-40, rc4-128, AES-128, AES-256
- PDF parsing and incremental editing: metadata, pages, forms, overlays
- Page transfer utilities: extract, split, merge, append
- Streaming output via ByteSink abstraction
- XMP metadata support
- Linearized (Fast Web View) output for single-page documents
- PDF version auto-detection and conformance validation