Releases: Nizoka/pdfnative
v1.3.0 — COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, pixel-diff visual regression, #48 CP-1252 fix
Closes issue #48 (CP-1252
extended characters not extractable under base-14 Helvetica) and delivers the
complete v1.3.0 roadmap with zero deferrals: COLRv1 colour emoji,
USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5
character-level overrides, and a dual-mode pixel-diff visual-regression suite.
100% backward-compatible — every new feature is additive or opt-in, and
pre-existing PDFs are byte-identical for unchanged code paths.
Still zero runtime dependencies. 71 test files / 1982 tests, all green.
Highlights
-
feat(shaping): Telugu script (
te). A new pure-JS GSUB/GPOS
mini-shaper (src/shaping/telugu-shaper.ts) brings Telugu (~95 M speakers,
ISO 15924Telu, U+0C00–U+0C7F) to pdfnative’s 16 existing scripts. It builds
virama-mediated conjunct clusters, forms subjoined-consonant ligatures via the
sharedgsub-driver, and positions above/below vowel signs and modifiers via
the sharedgpos-positioner— with no reph and no pre-base reordering
(Telugu specifics). Bundled fontpdfnative/fonts/noto-telugu-data.js(Noto
Sans Telugu, OFL-1.1). Real-font shaping of తెలుగు / నమస్తే / క్షి /
శ్రీ / జ్ఞ produces zero.notdefand correct conjuncts. Opt-in via
registerFont('te', () => import('pdfnative/fonts/noto-telugu-data.js')).
(src/shaping/telugu-shaper.ts) -
feat(shaping): Five underserved scripts — Amharic/Ethiopic (
am),
Sinhala (si), Tibetan (bo), Khmer (km), Myanmar (my). Five new
pure-JS mini-shapers extend pdfnative from 17 to 22 Unicode scripts,
following the Telugu model (sharedgsub-driver+gpos-positioner,
zero-dependency, pure functions). Ethiopic (U+1200–U+137F) is a syllabic
abugida needing no reordering — detection + font routing only.
Sinhala (src/shaping/sinhala-shaper.ts, U+0D80–U+0DFF) builds
virama conjuncts, reorders the pre-base kombuva (U+0D9A-class), and
decomposes two-part vowels. Tibetan
(src/shaping/tibetan-shaper.ts, U+0F00–U+0FFF) performs vertical
subjoined-consonant stacking. Khmer
(src/shaping/khmer-shaper.ts, U+1780–U+17FF) is USE-lite — coeng
subscripts, pre-base vowels, two-part vowel decomposition. Myanmar
(src/shaping/myanmar-shaper.ts, U+1000–U+109F) is USE-lite — medials,
pre-base medial-ra (U+103C) and e-vowel (U+1031), virama stacking. Khmer and
Myanmar are pragmatic USE-lite implementations with documented limitations
(two-part-vowel MultipleSubst is handled JS-side via shaper decomposition
tables, not the OpenType extractor). Bundled fonts (all OFL-1.1):
noto-ethiopic-data.js,noto-sinhala-data.js,noto-tibetan-data.js
(Noto Serif Tibetan),noto-khmer-data.js,noto-myanmar-data.js. Opt-in via
registerFont('am'|'si'|'bo'|'km'|'my', () => import('pdfnative/fonts/...')). -
feat(core): Opt-in Unicode normalization (
layout.normalize).
PdfLayoutOptions.normalize?: 'NFC'|'NFD'|'NFKC'|'NFKD'|false(default
false) applies nativeString.prototype.normalizeto text before encoding,
so decomposed input (e.g. combining diacritics) composes to the form the
embedded font expects. Off by default → byte-identical output for existing
callers. (src/core/encoding-context.ts) -
fix(crypto): CSPRNG-only randomness. PDF encryption now throws
if no cryptographically secure random source (crypto.getRandomValues) is
available, instead of silently falling back toMath.random. Encryption keys
and IVs are never derived from a non-CSPRNG source.
(src/core/pdf-encrypt.ts) -
feat(core): Configurable document block limit (
layout.maxBlocks).
The previously hard-coded 10 000-block safety cap inassembleDocumentParts()
is now configurable and the default raised to 100 000 (DEFAULT_MAX_BLOCKS).
Large reports (e.g. multi-thousand-page medical or financial documents) no
longer hit a spurious ceiling; callers can raise or lower it per document via
layout.maxBlocks. The over-limit error now names the active limit and how to
change it. (src/core/pdf-document.ts, src/core/pdf-layout.ts) -
feat(parser):
validatePdfUA()— PDF/UA structural validator. A new
read-only, zero-byte-risk developer gate (ISO 14289-1) that parses a PDF and
checks/MarkInfo /Marked,/StructTreeRoot+/ParentTree,/Metadata,
/Lang, and per-page/MCIDuniqueness. Returns
{ valid, errors, warnings }. Complements (does not replace) veraPDF.
(src/parser/pdf-ua-validator.ts) -
fix(shaping, colour emoji): No more tofu from selectors/joiners.
Emoji variation selectors (VS-15/VS-16), the ZWJ/ZWNJ, and Fitzpatrick
skin-tone modifiers that no registered font covers are now dropped during
run-splitting instead of resolving to.notdef(the box). Joiners are
still preserved when an Indic shaper font maps them. NewisZeroWidthFormat()
predicate. (src/shaping/multi-font.ts, src/shaping/script-registry.ts) -
fix(core, colour emoji): Computed Form
/BBox(no clipping).
renderColorGlyph()now derives each colour-glyph Form/BBoxfrom the
transformed contour bounds rather than the hard-coded em box, so colour emoji
that dip below the baseline are no longer clipped. (src/core/pdf-color-glyph.ts) -
feat(fonts): COLRv1 colour emoji. Noto Color Emoji (OFL-1.1) is
bundleable as a curated subset (pdfnative/fonts/noto-color-emoji-data.js,
221 colour glyphs, 936 KB). COLR v0 solid layers and COLR v1 linear / radial
gradients render as native PDF Form XObjects (/ShadingType 2/3 +
/ExtGStateconstant-alpha), one indirect object per unique glyph,
forward-referenced into every page/XObject. The COLR / CPAL /glyf
parsers are self-written and zero-dependency. Opt-in:
registerFont('emoji', () => import('pdfnative/fonts/noto-color-emoji-data.js')).
When not registered, monochrome emoji (Noto Emoji, v1.1.0) is unchanged and
documents are byte-identical. Sweep gradients + Porter-Duff compositing fall
back gracefully to monochrome (documented limitation). (src/core/color-emoji.ts, src/fonts/colr-parser.ts, src/fonts/glyf-outline.ts) -
feat(core): True constant-memory streaming.
buildPDFStreamTrue()
andbuildDocumentPDFStreamTrue()assemble the PDF into its raw object/
framing parts and yield fixed-sizeUint8Arraychunks while freeing each
part as it is emitted — the fully-joined PDF binary never materialises in
memory. Peak memory is bounded by the chunk size plus the single largest
part (a content stream or embedded font subset). Byte-identical output to
buildDocumentPDFBytes()/buildPDFBytes(). The v1.2.0 object-boundary
variants (*StreamPageByPage) and fixed-size variants (*Stream) are
retained. (src/core/pdf-stream-writer.ts) -
feat(shaping): UAX #9 X4–X5 overrides.
resolveBidiRuns()now
performs full character-level direction overrides inside LRO / RLO scopes —
every codepoint within the scope is forced to strong L (LRO) or strong R
(RLO) before the W/N/L rules run, not merely the base paragraph direction
(the v1.2.0 behaviour). Nested embeddings and isolates recurse correctly.
(src/shaping/bidi.ts) -
feat(shaping): USE-lite shaper integration. The v1.2.0 cluster
classifier (classifyUseCategory()) is now the joiner-classification
authority across the Devanagari, Bengali, and Tamil shapers. Orphan ZWJ /
ZWNJ no longer reach the cmap as.notdef; ZWJ between a halant/pulli and
the next consonant continues a conjunct (half-form, eyelash-ra, ya-phalaa)
while ZWNJ breaks it keeping a visible virama. (src/shaping/use-lite.ts) -
test(visual): Dual-mode pixel-diff visual regression. Two
complementary guards over self-contained extreme-script fixtures (Tamil,
Bengali + Devanagari, Arabic) built with the real bundled fonts:- a glyph-position snapshot that extracts every show operator's font,
size, baseline x/y, and glyph IDs into a committed JSON baseline; and - a rendered-glyph pixel diff that parses the embedded
FontFile2
outlines, scan-fills the shaped glyphs at their positions into a
grayscale bitmap, and compares against a committed PNG baseline
(≤1% pixel tolerance) using a self-written, zero-dependency grayscale PNG
encoder/decoder.
A CI workflow (visual-regression.yml)
runs both, gated onsrc/shaping/**,src/fonts/**,src/core/**, and
fonts/**. (tests/visual/) - a glyph-position snapshot that extracts every show operator's font,
-
fix(fonts, #48):
CP-1252 extended characters. Base-14 Helvetica text now carries a
/ToUnicodeCMap, so the Windows-1252 high range (€ ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ
Ž ' ' " " • – — ˜ ™ š › œ ž Ÿ) is correctly extractable and searchable in any
viewer. When alatinfont is registered (Noto Sans VF), these glyphs
additionally embed and render so the Euro sign et al. are visible, not
viewer-tofu. Falls back to the correct WinAnsi byte (byte-stable) when no
latin font is registered. (src/fonts/encoding.ts) -
fix(core, tagged PDF): Per-line MCID allocation in wrapped table cells
and multi-line table captions. Previously a single MCID was allocated per
...
v1.2.0 — addSignaturePlaceholder, ASN.1 fix, page-by-page streaming, UAX #9 embeddings, USE-lite classifier, smart tables
Closes issues #45 (addSignaturePlaceholder() API) and #46 (X.509 issuer/subject DN slice corruption), ships object-boundary page-by-page streaming, completes UAX #9 with embedding controls (LRE/RLE/LRO/RLO/PDF), and lands a USE-lite cluster classifier for future Indic shaper rewires. 100% backward-compatible. Every new feature is additive or opt-in. Pre-existing PDFs are byte-identical for unchanged code paths.
Two roadmap items intentionally slip to v1.3.0: COLRv1 colour emoji (renderer needs PDF shading-dictionary polish) and pixel-diff visual regression (PNG-baseline tooling). Monochrome emoji from v1.1.0 is unchanged. The USE-lite classifier ships as a public API in v1.2.0; rewiring the Devanagari/Bengali/Tamil shapers to consume it is the v1.3.0 follow-up.
Highlights
- feat(crypto, #45): new
addSignaturePlaceholder(pdfBytes, options?)API — inject an AcroForm + invisible signature widget plus a/Sigdictionary into any existing PDF via an incremental update (ISO 32000-1 §7.5.6). Idempotent: returns the input unchanged when an/FT /Sigwidget already exists. Enables the one-callsignPdfBytes(addSignaturePlaceholder(buildDocumentPDFBytes(...)))ergonomic that downstream tooling (pdfnative-cli) previously shipped as a local workaround. - fix(crypto, #46):
parseCertificate()issuer and subjectrawslices now correctly begin with the ASN.1 SEQUENCE tag0x30. ASN.1decodeAt()only patched direct-child offsets, so grandchildren carried offsets relative to their parent's value buffer rather than the original DER — producing malformed slices that broke CMSIssuerAndSerialNumberparsing in Adobe Reader and openssl-cms.decodeAt()now walks descendants recursively to absolutise every offset; a defensiveraw[0] === 0x30assertion lives at theparseName()boundary. - feat(core):
buildDocumentPDFStreamPageByPage()andbuildPDFStreamPageByPage()— emit an existing PDF binary as anAsyncGenerator<Uint8Array>chunked at PDF object boundaries (\nendobj\n). Useful for streaming the assembled PDF over HTTP / NodeWriteStreamwithout holding the full body in memory beyond a single chunk. Internal page-by-page assembly (one page object at a time before the final binary exists) remains a v1.3 target — flagged in the JSDoc. - feat(shaping): UAX #9 explicit embeddings —
normalizeBidiEmbeddings()rewrites LRE / RLE / LRO / RLO / PDF (U+202A–U+202E) to their sealed-isolate equivalents (LRI / RLI / PDI) using a stack with max depth 125 before the BiDi resolver runs.resolveBidiRuns()invokes the normaliser internally, so existing callers gain support transparently. Combined with the v1.1.0 isolates work, pdfnative now handles every UAX #9 directional control in common use. Character-level direction overrides inside LRO/RLO scopes (UAX #9 X4–X5) are simplified — only the base direction is normalised; full override tracking is deferred until users demand it. - feat(shaping): USE-lite cluster classifier in src/shaping/use-lite.ts —
classifyUseCategory(cp)+classifyClusters(cps)return per-cluster{ base, reph, prebase, postbase, premarks, postmarks }with per-script tables for Devanagari, Bengali, and Tamil. Public API ready to ship; consumed by the v1.3.0 shaper rewire. - feat(core): Smart tables — planner-driven table rendering with automatic wrap-on-overflow, multi-page slicing with repeated headers, optional zebra striping, captions, and configurable minimum row height / cell padding. Six new optional
TableBlockfields ship:wrap('auto'|'always'|'never', default'auto'),repeatHeader(defaulttrue),zebra,caption,minRowHeight,cellPadding. Existing tables that fit on one page are byte-identical to v1.1.0 output. Tagged-mode (/Table,/TR,/TH,/TD,/Caption) is preserved across slices via a shared structure-tree accumulator (ISO 14289-1 §7.10.6). See docs/guides/tables.md.
Fixed
- fix(core, tables):
renderTableno longer hardcodes the 4th column (i === 3) as the Amount column with Helvetica-Bold + credit/debit colouring. The styling is now driven by the explicit, opt-inColumnDef.kind === 'amount'field. Combined with the wrap-aware truncate (next bullet), this resolves thetable-smart-autofit.pdfclipping where the Notes column was unintentionally rendered bold and the auto-fit planner — measuring with regular metrics — sized the column too narrowly. The legacybuildPDF()financial path keeps the historicali === 3heuristic for byte-identical v1.0/v1.1 output. (src/core/pdf-renderers.ts, src/types/pdf-types.ts) - fix(core, tables):
emitCellonly applies the v1.1 character-truncate (mx/mxH) whenwrap: 'never'. Underwrap: 'auto'(the v1.2.0 default) andwrap: 'always'the planner has already sized the column to fit the text; the redundant char-truncate previously truncated text that genuinely fits, producing spurious…ellipses in auto-fitted tables. (src/core/pdf-renderers.ts) - fix(crypto, #46): ASN.1
decodeAt()now recursively rewrites every descendant node'soffsetto be absolute against the original DER buffer. Previously, only direct children were patched, soparseName()'sfullDer.subarray(node.offset, …)returned a slice off by exactly the offset of the parent's value field. CMS signatures using these slices inIssuerAndSerialNumbernow validate in Adobe Reader, openssl-cms, and pdfnative's own verify path. Defensiveraw[0] === 0x30assertion added at theparseName()boundary to catch any future regression. (src/crypto/asn1.ts, src/crypto/x509.ts) - fix(shaping): invisible Unicode bidirectional formatting characters (LRM/RLM U+200E/F, LRE/RLE/PDF/LRO/RLO U+202A–E, LRI/RLI/FSI/PDI U+2066–9) are now stripped at the encoder boundary. The BiDi resolver consumed them when it ran, but it only runs on RTL paragraphs — pure-LTR text containing an orphan PDF or isolate marker would otherwise reach the cmap as
.notdefand render as tofu (). New publicstripBidiControls(text)helper exported from the root; applied transparently inpdfString(),helveticaWidth(), and the Unicode encoding context'stextRuns()/ps(). Zero behaviour change on text without control characters. (src/shaping/bidi.ts, src/fonts/encoding.ts, src/core/encoding-context.ts)- fix(fonts, tables): right- and centre-aligned bold text — table headers (Helvetica-Bold viaenc.f2) and table captions — are now measured with Adobe Helvetica-Bold AFM advance widths instead of Helvetica-Regular. Pre-1.2.0, the renderer measured"Amount"at ~25.44pt (Regular) but the glyphs actually rendered ~30.22pt wide (Bold) at 8pt, so the trailing glyph overshot the column boundary by ~2pt and thetwas clipped or overhung into the neighbour column. Fix: newhelveticaBoldWidth(str, sz)public function in src/fonts/encoding.ts and an opt-inboldflag ontxtR/txtC/txtRTagged/txtCTaggedin src/core/pdf-text.ts. Wired through smart-table headers (src/core/pdf-renderers.ts), legacybuildPDF()headers (src/core/pdf-builder.ts), andautoFitColumnsheader measurement (src/core/pdf-column-fit.ts). Visual: thetofAmountnow sits comfortably inside the column on every table sample. Unicode/CIDFont mode uses per-font metrics and is unaffected. (src/fonts/encoding.ts, src/core/pdf-text.ts, src/core/pdf-renderers.ts, src/core/pdf-builder.ts, src/core/pdf-column-fit.ts)
Added
- feat(types, tables): new optional
ColumnDef.kind?: 'amount'field. Opt-in replacement for the pre-1.2.0 hardcodedi === 3heuristic inrenderTable— when set, data cells in the column render in Helvetica-Bold with credit/debit colouring driven byrow.type. Reserved enum (furtherkindvalues may be added in future minor releases). (src/types/pdf-types.ts) - feat(core, mcp): new
PDF_A_CONFORMANCE_TARGETS = ['pdfa1b', 'pdfa2b', 'pdfa2u', 'pdfa3b'] as constandPdfAConformanceTargettype exported from the root. Single source of truth for tooling — thepdfnative-mcpserver'sadd_table/generate_basic_pdftool schemas canimport { PDF_A_CONFORMANCE_TARGETS } from 'pdfnative'and feed the array straight into their JSON-schemaenum:field instead of hardcoding string literals. Materially improves how Gemini-CLI and other LLM agents discover the legalpdfAvalues. (src/core/pdf-tags.ts) - feat(crypto, #45):
addSignaturePlaceholder(pdfBytes, options?)exported from the root. Options:placeholderBytes(default 16 384),fieldName(default'Signature1'),pageIndex(default 0),signingTime/name/reason/location/contactInfo(forwarded to the/Sigdictionary). Throws on encrypted input. Idempotent on already-signed PDFs (verified by a dedicated test case + sample generator). (src/core/pdf-sig-placeholder.ts) - refactor(crypto): new
SigDictMetadatainterface in src/core/pdf-signature.ts — the metadata-only subset ofPdfSignOptions(name,reason,location,contactInfo, `signi...
v1.1.0 — PDF/A Latin embedding, BiDi isolates, Arabic harakat, emoji
Released 2026-04-30
Maximalist minor release. Closes the two largest open epics — issue #28 (PDF/A Latin font embedding) and issue #25 (full UAX #9 BiDi isolates + GPOS MarkBasePos for Arabic harakat) — and adds first-class monochrome emoji support, auto-fit table columns, and per-cell clipping. Folds the alpha.1 / alpha.2 medium-term items into a single stable cut.
100% backward-compatible. All new features are opt-in and gated on font registration or explicit table flags. Pre-existing PDFs are byte-identical. 1726 tests green across 48 files.
Highlights
- fonts(latin): Noto Sans VF (OFL-1.1) is now bundleable as a fallback for PDF/A documents that use non-WinAnsi Latin (curly quotes, em-dash, ellipsis…). Opt-in via
registerFont('latin', () => import('pdfnative/fonts/noto-sans-data.js')). Automatically activates for PDF/A modes when the encoding context detects characters outside WinAnsi. Closes #28. - shaping(bidi): UAX #9 isolate handling — LRI / RLI / FSI / PDI (U+2066–U+2069) are now honoured. Mixed-script paragraphs containing isolated runs are resolved correctly with full recursion. Three-tier dispatcher: public
resolveBidiRuns()finds outermost isolate pairs,resolveBidiRunsForced()recurses with forced level,resolveBidiCore()runs the W1–W7 / N1–N2 / L2 pipeline. - shaping(arabic): GPOS MarkBasePos applied to transparent marks (harakat — fatha, kasra, damma, sukun, shadda, …). Marks now anchor on the preceding base glyph using font-provided GPOS anchor data, falling back to (0, 0) when absent. Closes the visual half of #25.
- shaping(drivers): new shared
gsub-driver.ts(tryLigature()) andgpos-positioner.ts(positionMarkOnBase()) modules. Bengali, Tamil, Devanagari, and Arabic shapers now route through a single GSUB lookup helper and a single GPOS anchor helper instead of three duplicated implementations. - shaping(emoji): monochrome emoji via Noto Emoji (OFL-1.1, 1891 glyphs). Opt-in via
registerFont('emoji', () => import('pdfnative/fonts/noto-emoji-data.js')). Detection covers the full BMP/SMP emoji ranges (U+1F300–U+1FAFF, U+2600–U+27BF, …) plus Fitzpatrick modifiers (U+1F3FB–U+1F3FF), ZWJ (U+200D), and VS-15 / VS-16 (U+FE0E / U+FE0F). Multi-font run splitting routes emoji codepoints to the registered'emoji'font automatically. - core(table):
TableBlock.autoFitColumns(alpha.2) andTableBlock.clipCells(alpha.2) now part of the stable surface. Defaults preserve v1.0.x byte output.
Fixed (PDF/A conformance hardening)
- pdfa(font embedding): Object 3 (
/F1) and Object 4 (/F2) are now Type0 redirector dicts pointing to the embeddedCIDFontType2/FontFile2chain when a Latin font entry is registered — eliminating unembeddedHelvetica/Helvetica-Boldstandard-14 references that broke veraPDF (ISO 19005-1 §6.3.4 / ISO 19005-2 §6.2.11.4.1). - pdfa(xmp utf-8): XMP metadata streams now go through a binary-safe UTF-8 encoder (
utf8EncodeBinaryString()) beforetoBytes(), preserving em-dash, ellipsis, smart quotes, CJK in<dc:title>and matching/Info /Titlebyte-for-byte (ISO 19005-1 §6.7.3 t1). - pdfa(xmp parity):
buildXMPMetadata()emits<dc:description>and<pdf:Keywords>whenever/Info /Subjectand/Info /Keywordsare set, satisfying ISO 19005-1 §6.7.3 t4 / t5 parity rules. Unblocks PDF/A-1b validation for documents carrying subject or keywords metadata. - pdfa(encoding fallback):
createEncodingContext(fontEntries, pdfA=true)disables the WinAnsi/Helvetica fallback. Characters outside the primary CIDFont's cmap render as.notdefinstead of routing to an unembedded Type1 font. - pdfa(annotations
/F 4): Link annotations (/Subtype /Link, both/URIand/GoTo) and form widgets (/Subtype /Widget) now emit/F 4(Print flag set, NoView/Hidden/Invisible cleared) per ISO 19005-2 §6.5.3 / veraPDF rule 6.3.2-1. Required on every annotation in PDF/A-2 / PDF/A-3. - scripts(samples): Five PDF/A-claiming sample generators (
barcode-tagged,compressed-tagged-pdfa2b,header-footer-tagged,tagged-accessibility-complex,toc-tagged) now register alatinfont entry so the generated samples pass veraPDF rule 6.2.11.4.1-1. Thepdfa-variantsandpdfa-latin-embeddinggenerators were already wired in alpha.1. - ci(verapdf): veraPDF validation is now blocking on PRs and pushes — no more
continue-on-error.validate-pdfa.tsauto-detects PDF/A claims via XMPpdfaid:part, so non-PDF/A samples never trigger CI failures.
Added
- fonts(latin):
fonts/noto-sans-data.{js,d.ts}— Noto Sans VF subsetted, 4515 glyphs, 3094 cmap entries. OFL-1.1. - fonts(emoji):
fonts/noto-emoji-data.{js,d.ts}— Noto Emoji monochrome, 1891 glyphs, 1489 cmap entries. OFL-1.1. - shaping(bidi): isolate support — LRI (U+2066), RLI (U+2067), FSI (U+2068), PDI (U+2069) classified as
BNand recursed. Nested isolates supported. Unmatched isolates fall through gracefully. - shaping(arabic): MarkBasePos applied to transparent (joining type 'T') marks.
lastBaseGidtracking through the shaping pipeline including lam-alef ligatures. - shaping(drivers):
src/shaping/gsub-driver.tsexportingtryLigature(gids, ligatures)andsrc/shaping/gpos-positioner.tsexportinggetBaseAnchor,getMarkAnchor,getMark2MarkAnchor,positionMarkOnBase. Bengali / Tamil / Devanagari / Arabic shapers refactored to use them. - shaping(emoji):
EMOJI_RANGES,isEmojiCodepoint,containsEmoji,FITZPATRICK_START/END,ZWJ,VS15,VS16exported fromsrc/shaping/script-registry.ts.detectCharLang()returns'emoji'for emoji codepoints;detectFallbackLangs()adds'emoji'automatically. - scripts(download-fonts): Noto Emoji entry in the manifest for reproducible
npm run download:fonts. - tests:
tests/shaping/phase2-shaping.test.ts(24 tests, GSUB driver + GPOS positioner + BiDi isolates + Arabic GPOS),tests/shaping/emoji.test.ts(15 tests, ranges + predicates + script-detect integration + baked module shape),tests/fonts/pdfa-latin-embedding.test.ts(PDF/A Latin embedding integration).
Changed
- shaping(bidi):
resolveBidiRuns()rewritten as a recursive isolate-aware dispatcher. Behaviour unchanged for inputs without isolate characters — output is byte-identical for all pre-v1.1.0 fixtures. - shaping(types):
fixPunctuationAffinityandfixBracketPairingwidened toreadonly number[]to match the new core pipeline. No public API impact. - shaping(bengali, tamil, devanagari): local
tryLigaturedefinitions removed. Shapers now declare a thintryLig(gids)closure that forwards to the shared driver. Output bytes unchanged.
Documentation
- New release notes (this file).
- README, ROADMAP, and
.github/copilot-instructions.mdupdated to reflect new modules, emoji support, and PDF/A Latin embedding. - New emoji guide at pdfnative.dev/guides/emoji.html.
- PDF/A guide refreshed with Latin embedding example.
Deferred to v1.2.0
- Full UAX #9 embeddings (LRE / RLE / LRO / RLO / PDF) — isolates ship now, embeddings remain rare in practice and require a deeper level-stack refactor.
- True page-by-page constant-memory streaming (
buildDocumentPDFStreamPageByPage()). - COLRv1 colour emoji (this release ships monochrome only).
Upgrade
npm install pdfnative@1.1.0Opt into the new font modules as needed:
import { registerFont } from 'pdfnative';
// PDF/A documents that need non-WinAnsi Latin fallback
registerFont('latin', () => import('pdfnative/fonts/noto-sans-data.js'));
// Emoji rendering (monochrome)
registerFont('emoji', () => import('pdfnative/fonts/noto-emoji-data.js'));No code changes required for users who don't register 'latin' or 'emoji' — pre-existing PDFs are byte-identical.
Credits
- Noto Sans, Noto Emoji © Google LLC, licensed under SIL Open Font License 1.1.
- UAX #9 reference: Unicode Bidirectional Algorithm.
- ISO 32000-1:2008 §9.7 (CIDFont), §9.10 (ToUnicode), §14.8 (PDF/UA), ISO 19005-2 (PDF/A-2).
v1.0.5 — watermark centering & CLI docs
Released 2026-04-27
Patch release. Two bug fixes in the watermark module (vertical centering and Unicode encoding) plus first-class documentation for the official command-line interface, pdfnative-cli. 100% backward-compatible with v1.0.4 — no public API surface changes, no breaking changes, no byte-shift in non-watermarked PDFs.
Highlights
- fix(watermark): text watermarks are now mathematically centered. The previous offset ignored cap-height; affected output now matches the visual center of the page in both axes.
- fix(watermark): Unicode watermarks (Arabic, Hebrew, CJK, Devanagari, Bengali, Tamil, Cyrillic, Greek, Georgian, Armenian) now render correctly when the document uses a CIDFont. Watermark text is encoded through the active encoding context instead of being forced through WinAnsi.
- docs(cli): new CLI Guide documents
pdfnative-cli— the officialrender/sign/inspectcommand-line tool. Covers installation, security model, CI/CD pipelines, and library-vs-CLI decision guidance. - docs(architecture): Ecosystem section now spans both companion packages (
pdfnative-cliandpdfnative-mcp) with parallel treatment in README.md and the architecture guide.
Fixed
- fix(core/watermark): vertical offset of text watermarks now derives from the active font's
capHeight / unitsPerEmratio (with a0.718fallback matching Helvetica) instead of the prior-fontSize/2. Glyphs are now centered on their cap-height baseline midpoint, eliminating the visible vertical drift previously observed at large rotation angles. (src/core/pdf-watermark.ts) - fix(core/watermark): watermark text is now encoded via
enc.ps()(the document's active encoding context) instead ofpdfString()(WinAnsi-only). Documents using a CIDFont (Identity-H) now emit watermarks as 2-byte hex GIDs and render correctly across all 16 supported scripts. (src/core/pdf-watermark.ts)
Added
- feat(tests): 6 new regression tests in tests/core/pdf-watermark.test.ts covering cap-height-based vertical offset, horizontal centering, Latin WinAnsi encoding, Unicode CIDFont 2-byte GID hex encoding, font-metric-driven offset in Unicode mode, and rotation invariance of the visual centering bounding box.
Documentation
- docs(cli): new CLI Guide (
docs/guides/cli.html+docs/guides/cli.md). - docs(index): new CLI feature card on pdfnative.dev homepage with dedicated icon (
.fi-cli). - docs(quickstart): new "Command line — pdfnative-cli" section in the Quick Start guide plus link from the Next steps list.
- docs(architecture): Ecosystem section in architecture guide now documents
pdfnative-clialongsidepdfnative-mcpwith parallel diagrams. - docs(readme): README.md now lists both companion packages in the Ecosystem section, includes a
pdfnative-clinpm version badge, and a CLI bullet in the Highlights list.
Deferred
- #28 (PDF/A Latin font embedding): integration of an embedded Latin font (e.g. Liberation Sans / Arimo) for PDF/A documents has been deferred to v1.1.0. The change requires object renumbering across multiple builders and ships ~30–60 KB of additional bytes per PDF/A output, which is out of scope for a patch release. Tracking issue: #28.
Install
npm install pdfnative@1.0.5Upgrade
No breaking changes. Drop-in replacement for v1.0.4. Documents that did not use watermarks produce byte-identical output to v1.0.4. Documents using watermarks will produce visually-corrected output (vertical centering, Unicode encoding) — verify visual regression baselines if your test suite pixel-compares watermarked PDFs.
Links
v1.0.4 — PDF/A conformance hardening (trailer /ID, /Info ↔ XMP parity, veraPDF CI)
Released 2026-04-25
v1.0.4 is a patch release that begins a multi-step PDF/A conformance
hardening sweep. Two of the five real bugs found by the official veraPDF
reference validator on medical-800p.pdf and barcode-showcase.pdf are
fixed in this release; a third (Latin font embedding under PDF/A) is
scheduled for v1.0.5 and tracked transparently in
issue.
A new CI workflow installs the veraPDF CLI on every PR and validates
every sample PDF that claims PDF/A — preventing future regressions.
100 % API-backward-compatible with v1.0.3.
Highlights
- Trailer
/IDis now emitted on every PDF (was previously
encrypted-only). Fixes ISO 19005-1 §6.1.3 / ISO 32000-1 §14.4. /Info CreationDateandxmp:CreateDateshare a single timestamp
source and now both carry timezone offsets, eliminating veraPDF rule
6.7.3 t1 mismatches.dc:creatoris emitted only when an author is present and is
XML-escaped, fixing rule 6.7.3 mismatches on documents without an
explicit author.- New
npm run validate:pdfascript +verapdf.ymlCI workflow
protect the project against PDF/A regressions going forward.
Fixed
- fix(core): trailer
/IDalways emitted (previously encrypted-only).
Unencrypted ID isMD5("pdfnative|" + title + "|" + creationDate + "|" + totalObjs)for deterministic regeneration. (src/core/pdf-assembler.ts) - fix(core):
/Info CreationDate↔xmp:CreateDatebyte-equivalent
via newbuildPdfMetadata()helper; both include local timezone offset
(D:YYYYMMDDHHmmSS+HH'mm'and ISO 8601±HH:MM). Closes veraPDF rule
6.7.3 t1. (src/core/pdf-tags.ts) - fix(core): XMP
dc:creatoremitted conditionally and XML-escaped;
matches/Info /Authorexactly when present. Closes false-positive
rule 6.7.3 mismatches on author-less documents. (src/core/pdf-tags.ts) - fix(core): XMP packet adds matching
xmp:ModifyDateand
xmp:MetadataDate(ISO 19005-2 §6.7.3 best practice).
Added
- feat(scripts):
scripts/validate-pdfa.ts— batch veraPDF runner.
Walkstest-output/, detects PDF/A claims via XMP, invokes veraPDF CLI,
reports compliant/failed counts. Skips silently when veraPDF is missing.
Prints per-OS install hints (macOS / Linux / Windows / online demo)
and aScanned N; M claim PDF/A, K skipped (not PDF/A)summary line. - feat(ci):
.github/workflows/verapdf.yml— installs veraPDF CLI on
Temurin 17, regenerates samples, runsnpm run validate:pdfa. Pinned
veraPDF version1.26.5. Supportsworkflow_dispatchso the
workflow can be triggered manually against any branch from the
GitHub Actions UI before opening a pull request. - feat(scripts):
npm run validate:pdfascript. - test(core): tests/core/pdf-trailer-id.test.ts
— 18 new tests covering trailer/ID, date format with timezone,
XMP ↔ /Info parity, dc:creator escaping & conditional emission. - docs(guides): new Installing veraPDF locally + Troubleshooting
sections in docs/guides/pdfa.html
document the install path on each OS, the online demo as a no-install
fallback, and why plain ISO 32000-1 files are auto-skipped instead of
being forced through a PDF/A profile. - docs(landing): new "Designed for low-impact computing" section
on docs/index.html — factual sustainability
bullets only (zero deps, on-device generation, no telemetry,
tree-shakeable ESM, streaming output, external tooling stays
external). No carbon claims, no badges we cannot verify. - docs(readme): two factual bullets added to Highlights covering
on-device generation and the absence of network calls.
Changed
- chore(meta): version bump to
1.0.4. - chore(meta): existing CreationDate regex tests updated to allow
the new mandatory timezone offset.
Documentation
- docs: release-notes/v1.0.4.md, tracking issue
release-notes/draft-issue-v1.0.4-pdfa-conformance.md,
v1.0.5 epic [issue](#28 d). - docs: new guide docs/guides/pdfa.html
documents PDF/A modes, current limitations, validator workflow. - docs(kb): new instruction file
.github/instructions/pdfa-conformance.instructions.md
captures ISO clauses, validator rules, and the v1.0.5 roadmap.
Known limitations
- Latin font embedding under PDF/A modes is not yet implemented.
Files generated withtagged: true | 'pdfa1b' | 'pdfa2b' | 'pdfa2u' | 'pdfa3b'still emit unembedded standard 14 Helvetica and therefore
still fail veraPDF rule 6.3.4. Thepdfaid:partclaim in XMP must be
treated as aspirational until v1.0.5 lands. The CI guardrail expects
this and is wired to start blocking PRs once v1.0.5 ships.
Verification
npm run typecheck:all— cleannpm run lint— 0 errorsnpm run test— 1 624 / 1 624 passing (1 606 prior + 18 new)npm run build— ESM + CJS + DTS produced
Migration
No code changes required. Outputs may differ byte-for-byte (new /ID
array, timezone in CreationDate, conditional dc:creator). If your
test fixtures snapshot full PDF bytes, regenerate them.
FAQ
Why does veraPDF still report failures on doc-lists.pdf (or any
other plain sample)? Those samples are not generated with
tagged: true, so they do not claim PDF/A in their XMP packet. The
veraPDF online demo lets you pick a profile manually, which then
forces the file through PDF/A rules and surfaces unrelated failures
by design (missing XMP, unembedded fonts, DeviceRGB without an
OutputIntent — all expected for an ISO 32000-1 document). The
npm run validate:pdfa wrapper avoids this trap by skipping every
file that does not declare pdfaid:part in XMP. See the
Troubleshooting section
for details.
Is this a breaking change? No public API breaks. Output bytes
do change (new /ID, timezone offsets in CreationDate,
conditional dc:creator). Consumers that snapshot full PDF bytes
in their test fixtures will need to regenerate those snapshots.
SemVer-wise this is a patch.
v1.0.3 — extreme-script regression baseline, layout fix, playgrounds
Released 2026-04-25
Patch release adding extreme-script visual regression baselines, fixing a heading layout overflow on long compound titles, and shipping two new interactive playgrounds. No public API change — fully backward-compatible with v1.0.2.
Highlights
- Heading overflow fix — long titles such as
"Test Bengali + Devanagari ULTRA EXTREME — Shaping & Positioning — pdfnative"
no longer overflow the right margin;wrapText()now hard-breaks single
overlong tokens at character boundaries when no whitespace breakpoint
exists. - Extreme shaping samples — four new visual baselines under
test-output/extreme/covering BiDi (Arabic + Hebrew + Thai + Latin +
digits), Tamil ultra-conjuncts, Bengali + Devanagari reph + multi-halant
ligature chains, and isolated Arabic harakat. - Two new playgrounds —
extreme-scripts.htmlfor live shaping
stress-tests with editable presets, andmedical-800.htmlshowcasing
off-thread generation of an 800-page synthetic clinical report via Web
Worker +buildDocumentPDFStream. - Honest scoping — deeper shaping issues exposed by the new baselines
(Arabic isolated harakat anchoring, Thai mark stacking on tall
consonants, multi-stage Indic ligature chains, 3+ RTL-script BiDi) are
documented as known limitations and tracked for the v1.1.0 minor
release. They require GPOS table re-extraction in the pre-built font
data modules and exceed the scope of a SemVer-patch.
Fixed
- fix(core/layout):
wrapText()now hard-breaks single overlong
tokens at character boundaries when no whitespace breakpoint exists.
Long headings and titles previously could overflow the right margin
when no segment fit. Code points are honored so surrogate pairs and
combining sequences remain intact at slice boundaries.
(src/core/pdf-renderers.ts) - fix(docs):
guides/architecture.htmlandguides/faq.htmlfooter
links fromdocs/index.htmlpreviously 404'd because only.mdfiles
existed underdocs/guides/and.nojekylldisables auto-rendering.
Each guide now ships as a real HTML page with a clean URL.
Added
- feat(samples): new
extreme-shaping.tsgenerator producing four
visual-regression baselines undertest-output/extreme/:
extreme-bidi.pdf,extreme-tamil.pdf,
extreme-bengali-devanagari.pdf,extreme-arabic-harakat.pdf.
Wired intoscripts/generate-samples.ts. - test(integration):
tests/integration/extreme-shaping.test.ts—
five end-to-end builds covering the same extreme inputs to guard
against pipeline regressions (1 605 tests total, up from 1 600). - test(core): new regression tests for
wrapTextconfirming
character-level hard-break of overlong tokens and multi-line wrapping
of long em-dash titles. - docs(playgrounds): new interactive playground
docs/playgrounds/extreme-scripts.htmlfor stress-testing BiDi, Tamil
conjuncts, Bengali + Devanagari ligatures, and Arabic harakat directly
in the browser, with editable presets and a code preview. - docs(playgrounds): new
docs/playgrounds/medical-800.html— Web
Worker showcase generating an 800-page synthetic clinical report using
buildDocumentPDFStream, with live progress, byte/chunk counters,
optional Tagged PDF (PDF/A-2b), and a main-thread comparison toggle.
All patient data is generated client-side from a seeded RNG — no real
PHI. - docs(landing): static HTML guide pages (
quickstart.html,
architecture.html,faq.html,troubleshooting.html,
accessibility.html) plus a guides index at/guides/. - docs(guides): new
quickstart.mdcovering Node.js, browser,
multi-language, Web Worker, and streaming setups in a single page; new
accessibility.mdcovering tagged PDF, PDF/UA, PDF/A variants,
structure tree contents, alt-text discipline, and validation tooling.
Rewrotefaq.mdwith sectioned topics and ten concrete code snippets. - docs(readme): added a "Documentation" pointer block linking to the
guides and topdfnative.dev. Added Indic document samples
(doc-bengali,doc-tamil,doc-devanagari) to the Document Builder
Samples table. Added a "Citing pdfnative" section with BibTeX pointing
toCITATION.cff.
Changed
- docs(landing): added project-status badges to the
pdfnative.dev
hero (CI, CodeQL, OpenSSF Scorecard, npm version, monthly downloads,
bundle size, zero deps, TypeScript strict, npm provenance, MIT) to
mirror the README and surface supply-chain signals upfront. - docs(landing): rebuilt the "Try It Live" panel as a curated
10-example gallery (Quick Start, Financial, TOC, Barcode, SVG,
Watermark, Forms, PDF/A, Multi-language with lazy fonts, Streaming).
The runtime now supports top-levelawait, dynamicimport(…), and
exposesstreamDocumentPdf,registerFonts,loadFontData, and
signPdfBytes. - docs(landing): added "Guides" and "Playgrounds" entries in the
navbar and refreshed the footer with direct links to every guide and
both new playgrounds. - docs(landing): synced the test counter to 1 605+ tests.
Known limitations (tracked for v1.1.0)
The new extreme samples surface deeper shaping issues that are tracked
for the next minor release. They require either GPOS table re-extraction
in the pre-built font data modules under fonts/ or new OpenType lookup
implementations in the shaping pipeline, which exceed the scope of a
SemVer-patch:
- Arabic isolated harakat:
تشكيلwithout a base consonant fall back
to default mark positioning rather than precise font-anchored
placement. Visible inextreme-arabic-harakat.pdf. - Thai tall-consonant mark stacking: ป ฝ ฟ ฬ with three or more
combining marks may overlap with the current font anchor data. - Multi-stage Indic ligatures: ক্ষ্ম, क्ष्म, ஸ்ரீ are matched
greedily; some deeply-nested sequences fall back to non-ligated forms. - 3+ script BiDi paragraphs: Arabic + Hebrew + Thai + Latin + digits
in one paragraph may exhibit non-canonical run ordering at boundaries
with neutrals.
The full follow-up plan is tracked in
release-notes/draft-issue-v1.1.0-shaping-epic.md.
Install
npm install pdfnative@1.0.3Upgrade
No breaking changes. Drop-in replacement for v1.0.2.
The only behavioral change is wrapText(), which now hard-breaks
overlong tokens that previously overflowed the page. Existing layouts
that relied on overflow behavior (none expected — overflow was always
visually broken) should re-render with proper wrapping.
Links
v1.0.2 — metadata, samples & governance
v1.0.2 — metadata, samples & governance
Released 2026-04-24
Documentation, samples, governance, and npm metadata polish. No runtime code changes — 100% backward-compatible with v1.0.1.
Highlights
- npm discoverability —
descriptionnow enumerates the 16 supported scripts and headline features;keywordsexpanded from 13 → 27 entries. - Devanagari sample — new
doc-devanagari.pdfcompletes the Indic sample triad (Bengali, Tamil, Devanagari). - 16-script multi-language showcase —
doc-multi-language.pdfcovers all supported scripts in one PDF (up from EN/AR/JA). - Governance — new
maintenanceissue template and a standardized release notes template for future releases.
Changed
- chore(meta): enriched
package.json.descriptionnow enumerates the 16 supported scripts (Arabic, Hebrew, Thai, CJK, Devanagari, Bengali, Tamil, Cyrillic, Greek, Georgian, Armenian, Latin) and headline features (BiDi, PDF/A-1b/2b/3b, AES encryption, digital signatures, AcroForm, barcodes, SVG).keywordsexpanded from 13 to 27 entries (addsarabic,hebrew,bengali,tamil,devanagari,bidi,pdf-a,tagged-pdf,accessibility,encryption,digital-signature,acroform,barcode,qr-code). - docs(contributing):
CONTRIBUTING.mdbranch strategy — default branch corrected frommastertomain, addedchore/*convention for maintenance and release branches.
Fixed
- fix(docs):
README.mdmulti-font table — Bengali and Tamil rows were concatenated on a single line with literal\ncharacters instead of real newlines, rendering as broken markdown on npmjs.com and GitHub. - fix(samples):
doc-devanagari.pdfheading "अध्याय १: परिचय" used a Bengali digit one (U+09E7) instead of a Devanagari digit one (U+0967), producing a.notdeftofu box in the rendered PDF. - fix(build): VS Code ts-server reported a spurious
Cannot find name 'path'onscripts/generators/document-builder.ts. Root cause: the roottsconfig.jsononly includessrc/**, so the editor fell back to a config without@types/node. Addedscripts/tsconfig.jsonextendingtsconfig.scripts.jsonso the editor picks up node types for sample generators. CLI (npm run typecheck:scripts) was already green.
Added
- feat(samples): new
generateDevanagariDoc()inscripts/generators/document-builder.tsproducingtest-output/document/doc-devanagari.pdfwith GSUB conjuncts, reph reordering, matra reordering, and split vowels. - feat(samples):
doc-multi-language.pdfnow covers all 16 supported scripts (Latin, Greek, Cyrillic, Turkish, Vietnamese, Polish, Georgian, Armenian, Thai, Devanagari, Bengali, Tamil, Japanese, Chinese, Korean, Arabic, Hebrew) in a single document. - docs(governance): new
.github/ISSUE_TEMPLATE/maintenance.mdtemplate for release tasks, metadata updates, and governance work. - docs(governance): new
release-notes/TEMPLATE.mdstandardizing future release notes (section structure, conventional commit prefixes, SemVer classification, publication workflow).
Install
npm install pdfnative@1.0.2Upgrade
No breaking changes. Drop-in replacement for v1.0.1.
Links
v1.0.1 — Fix CP1252 bullet encoding
v1.0.1 — Fix CP1252 bullet encoding
This is a patch release fixing a regression where bullet list items rendered
as ? in the default WinAnsi/Helvetica mode, and completing the full CP1252
character set for the toWinAnsi() encoding function.
No API changes. No breaking changes. Upgrade is a drop-in replacement.
Bug Fixes
fix(encoding): bullet list items rendered as ? in default mode ([#1])
Every { type: 'list', style: 'bullet' } block without fontEntries
(the default Helvetica/WinAnsi mode) produced ? instead of • in the
generated PDF.
Root cause: toWinAnsi() in src/fonts/encoding.ts was missing the
CP1252 mapping for the bullet character • (U+2022 → 0x95).
The list renderer in pdf-renderers.ts hard-codes '\u2022' as the bullet
marker, which must survive toWinAnsi() — the absent mapping caused every
default-mode bullet list to render ? instead.
Fix: Added U+2022 → \x95 to toWinAnsi().
Note for users with
fontEntries: this bug does not affect CIDFont /
Identity-H mode — glyph IDs are resolved directly from the fontcmap
andtoWinAnsi()is never called. Only default Helvetica mode was impacted.
fix(encoding): complete CP1252 0x80–0x9F character table
CP1252 defines 27 characters in the 0x80–0x9F range that are absent from
ISO-8859-1. v1.0.0 mapped only 8 of them (€ – — ' ' " " …). This release
adds the remaining 18:
| Unicode | Char | CP1252 | Description |
|---|---|---|---|
| U+2022 | • | 0x95 | Bullet (primary fix) |
| U+201A | ‚ | 0x82 | Single low-9 quotation mark |
| U+0192 | ƒ | 0x83 | Latin small f with hook |
| U+201E | „ | 0x84 | Double low-9 quotation mark |
| U+2020 | † | 0x86 | Dagger |
| U+2021 | ‡ | 0x87 | Double dagger |
| U+02C6 | ˆ | 0x88 | Modifier letter circumflex accent |
| U+2030 | ‰ | 0x89 | Per mille sign |
| U+0160 | Š | 0x8A | Latin capital S with caron |
| U+2039 | ‹ | 0x8B | Single left-pointing angle quotation mark |
| U+0152 | Œ | 0x8C | Latin capital ligature OE |
| U+017D | Ž | 0x8E | Latin capital Z with caron |
| U+02DC | ˜ | 0x98 | Small tilde |
| U+2122 | ™ | 0x99 | Trade mark sign |
| U+0161 | š | 0x9A | Latin small s with caron |
| U+203A | › | 0x9B | Single right-pointing angle quotation mark |
| U+0153 | œ | 0x9C | Latin small ligature oe |
| U+017E | ž | 0x9E | Latin small z with caron |
| U+0178 | Ÿ | 0x9F | Latin capital Y with diaeresis |
All of these previously fell through to the ? replacement path. This fix
is purely additive — no existing mapping has changed.
fix(docs): landing page live demo content
The live demo on pdfnative.dev used ✓ (U+2713)
in table cells. This character has no CP1252 equivalent and cannot be
encoded in WinAnsi mode — it rendered as ?. Replaced with ASCII Yes.
Unrelated financial API properties (type: 'credit', pointed: false)
that were incorrectly included in the buildDocumentPDFBytes() demo have
also been removed.
Testing
- 1600 tests passing (up from 1588 in v1.0.0)
- 12 new test cases covering
U+2022 → \x95and all 18 additional
CP1252 0x80–0x9F mappings added in this release - 141 sample PDFs regenerated — bullet lists visually verified
Upgrading
npm install pdfnative@1.0.1No code changes required. This release is a transparent drop-in upgrade.
Full Changelog
v1.0.0 — Initial Release
v1.0.0 — Initial Release
pdfnative — Pure native PDF generation. Zero vendor dependencies. ISO 32000-1 (PDF 1.7) compliant.
This is the first stable release of pdfnative, extracted from plika.app where it has
been powering production multi-language PDF generation across 16 Unicode scripts.
Install
npm install pdfnativeSigned provenance — this release is published via GitHub Actions OIDC with
npm --provenance.
Build attestation is verifiable at npmjs.com/package/pdfnative.
What's in this release
Zero-dependency PDF engine
Built from scratch in pure TypeScript — no PDFKit, no Puppeteer, no native binaries, no runtime
dependencies. Works in Node.js ≥ 22, browsers, Deno, Bun, and Web Workers out of the box.
Dual ESM + CJS build, tree-shakeable with sideEffects: false.
16 Unicode scripts with OpenType shaping
Full glyph shaping pipelines for Thai (GSUB + GPOS mark-to-base/mark-to-mark), Arabic
(positional GSUB: isol/init/medi/fina + lam-alef), Devanagari, Bengali, Tamil, and Unicode BiDi
(UAX #9) layout including bracket pairing and punctuation affinity.
Supported scripts: Thai, Japanese, Chinese (SC), Korean, Greek, Devanagari, Turkish, Vietnamese,
Polish, Arabic, Hebrew, Cyrillic, Georgian, Armenian, Bengali, Tamil.
Tagged PDF / PDF/A compliance
| Conformance level | Standard |
|---|---|
| Tagged PDF / PDF/UA | ISO 14289-1 |
| PDF/A-1b | ISO 19005-1 |
| PDF/A-2b (default) | ISO 19005-2 |
| PDF/A-2u | ISO 19005-2 |
| PDF/A-3b + embedded files | ISO 19005-3 |
Full structure tree (/Document → /Table → /TR → /TH|/TD, /H1–H3, /P, /L, /Figure,
/Link, /TOC → /TOCI), /ActualText on every marked content sequence, XMP metadata, sRGB ICC
OutputIntent.
Encryption, Signatures & Forms
- AES-128 / AES-256 (V4/R4, V5/R6) with owner + user passwords and granular permission
bitmask - CMS/PKCS#7 digital signatures (ISO 32000-1 §12.8) — RSA PKCS#1 v1.5 and ECDSA P-256 with
X.509 DER parsing; all crypto is zero-dependency pure TypeScript - AcroForm interactive fields — text, multiline, checkbox, radio, dropdown, listbox with full
/APappearance streams (ISO 32000-1 §12.7)
Document builder
12 block types for free-form document composition:
HeadingBlock, ParagraphBlock, ListBlock, TableBlock, ImageBlock, LinkBlock,
SpacerBlock, PageBreakBlock, TocBlock, BarcodeBlock, SvgBlock, FormFieldBlock.
Header/footer templates with {page} / {pages} / {date} / {title} placeholders, text and
image watermarks with transparency, table of contents with dot leaders and internal /GoTo links.
Barcodes, SVG & Streaming
Five barcode formats as pure PDF path operators (zero image dependency): Code 128, EAN-13, QR Code
(ISO 18004), Data Matrix ECC 200, PDF417. SVG element rendering (7 types). AsyncGenerator
streaming output with configurable chunk size.
PDF Parser & Modifier
Read and incrementally modify existing PDFs — tokenizer, xref table/stream parser, FlateDecode
inflate, object parser with discriminated union type guards, PdfReader, and PdfModifier with
non-destructive incremental /Prev chain.
Security
Three CWE mitigations are included and exposed as public API:
| Mitigation | API | Default |
|---|---|---|
| CWE-674 — parser recursion | MAX_PARSE_DEPTH |
1000 levels |
| CWE-400 — zip-bomb / inflate | setMaxInflateOutputSize() / DEFAULT_MAX_INFLATE_OUTPUT |
100 MB |
| CWE-400 — xref chain DoS | MAX_XREF_CHAIN |
100 hops + cycle detection |
URL validation blocks javascript:, file:, and data: schemes as well as control characters
(U+0000–U+001F, U+007F–U+009F) in link annotations. Color values are validated and sanitized
before interpolation into PDF content streams.
Quality metrics
| Metric | Value |
|---|---|
| Tests | 1588+ across 40 files |
| Statement coverage | 95%+ |
| Fuzz / edge-case scenarios | 48 |
| Runtime dependencies | 0 |
| Node.js requirement | ≥ 22 |
| CI matrix | Node 22 + 24 |
| Provenance | Signed via GitHub Actions OIDC |
| Supply-chain | OpenSSF Scorecard monitored |
Breaking changes
None — this is the initial stable release.
Full changelog
See CHANGELOG.md for the
complete list of added features, fixes, and security hardening details.
Links
- Website & live demos — pdfnative.dev
- npm — npmjs.com/package/pdfnative
- API reference — README.md
- Security policy — SECURITY.md
- Changelog — CHANGELOG.md