From 3621659819a8210ecd60e5327e436c3e1ff8ad71 Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:55:17 +0200 Subject: [PATCH 01/15] docs: align core docs with pdfnative-mcp 1.6.0 (28 tools, PAdES ladder, MCP 2026-07-28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the ecosystem manifest (single source of truth) to pdfnative-mcp 1.6.0: version, pin ^1.7.0, toolCount 28 (+ add_ltv, timestamp_pdf, update_metadata, inspect_layout), six prompts, and the mcp-tool-count assertion canonical "28 tools". versions.js fallbacks follow (incl. the stale pdfnative 1.6.0 engine fallback, corrected to 1.7.0). The MCP guide gains four new tool sections, the 10-variable environment table with the operator network charter (TSA / OCSP / CRL egress class, SSRF guard), the six-prompt table, the v1.6.0 error codes, the 13 block kinds / layout / build-time encrypt / print production / honest PDF/A coverage on generate_basic_pdf, PAdES guidance on sign_pdf, and a "What's new in v1.6.0" release-history entry with the six migration notes. The stale claim that RFC 3161 validation was "not part of this server" is corrected — verify_pdf now validates /DocTimeStamp tokens. README, llms.txt (root + docs copy, byte-identical), AGENTS.md, ROADMAP.md, onboarding, architecture and agentic-workflows follow the manifest. Two pre-existing inconsistencies are fixed in passing: onboarding said "v1.4.0 ships 24 tools" and ROADMAP said "Built on pdfnative 1.6.0 as of v1.5.0". All MCP facts verified against the local D:/Github/pdfnative-mcp source (package.json, release-notes/v1.6.0.md, src/server.ts). --- AGENTS.md | 2 +- README.md | 32 +++-- ROADMAP.md | 2 +- docs/assets/ecosystem.json | 22 +++- docs/assets/versions.js | 4 +- docs/guides/agentic-workflows.md | 2 +- docs/guides/architecture.md | 6 +- docs/guides/mcp.md | 220 +++++++++++++++++++++++++++---- docs/guides/onboarding.md | 2 +- docs/llms.txt | 4 +- llms.txt | 4 +- 11 files changed, 239 insertions(+), 61 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 58031f1..55ddf5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ See [.github/copilot-instructions.md](.github/copilot-instructions.md) §Archite ## Ecosystem context - [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli) v1.3.0 — terminal wrapper (render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern). Pins pdfnative `^1.6.0` in its `package.json` (semver-accepts 1.7.0); coordinates via that explicit pin. -- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) v1.5.0 — Model Context Protocol server exposing 24 AI tools (adds `add_chart`, `read_form_fields`/`fill_form` and `encrypt_pdf`/`decrypt_pdf` on top of the v1.4.0 annotation and governance tools). Pins pdfnative `^1.6.0` (semver-accepts 1.7.0). Same coordination model. +- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) v1.6.0 — Model Context Protocol server exposing 28 AI tools (adds `add_ltv`, `timestamp_pdf`, `update_metadata` and `inspect_layout` on top of the v1.5.0 chart, form and encryption tools; completes the PAdES ladder B-B → B-LTA and speaks MCP 2026-07-28 with legacy fallback). Pins pdfnative `^1.7.0`. Same coordination model. - [pdfnative-react](https://github.com/Nizoka/pdfnative-react) v1.1.0 — React renderer: declarative JSX compiled on-device to pdfnative blocks via a custom reconciler. React 19 is a peer dependency of *that* package only — pdfnative itself stays zero-dependency. Same coordination model. Downstream-impacting changes (new public APIs, removed APIs, behaviour shifts) must be documented in the **Downstream integration notes** section of the relevant `release-notes/vX.Y.Z.md`. diff --git a/README.md b/README.md index 4903d5b..d46cdea 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pdfnative ships as four coordinated packages — pick whichever entry point fits |---|:---:|---| | [`pdfnative`](https://www.npmjs.com/package/pdfnative) | **v1.7.0** | The library itself — call from Node, browsers, Workers, Deno, Bun. | | [`pdfnative-cli`](https://www.npmjs.com/package/pdfnative-cli) | **v1.3.0** | Render JSON → PDF, sign (RSA + ECDSA-SHA256, native constant-time crypto by default), inspect, verify (PAdES-T + OCSP/CRL), **merge / split / extract** pages, **annotate** (markup annotations), **govern** (AI-governance / HITL gate), batch, and emit JSON Schemas from the shell. Pins pdfnative `^1.6.0` (semver-accepts 1.7.0): 22 scripts + COLRv1 emoji, `--font math`, PDF bookmarks (`--outline`), layout introspection (`--inspect-layout` / `--debug-layout`), and an agent-native `--json`/`E_*`/`--dry-run`/`--summary` contract. | -| [`pdfnative-mcp`](https://www.npmjs.com/package/pdfnative-mcp) | **v1.5.0** | Use pdfnative from Claude Desktop, Cursor, Continue, Zed (or any stdio MCP client) — **24 production tools** including the page-tree trio `merge_pdfs`, `split_pdf`, `extract_pages`, markup `annotate_pdf`, the network-free `draft_governance_issue` (AI-governance / HITL), plus `validate_pdf`, `verify_pdf`, `add_attachment`, `extract_attachments`, and `extract_text`; watermark support, Unicode `normalize`, token-frugal read modes (`verbosity` / `fields`), `pdfA` flags, enriched authoring options (`outline`, `pageLabels`, nested lists, `viewerPreferences`, `cellBorders`, `cellVAlign`), the explicit `math` script, an MCP `prompts` capability, a constant-time `node:crypto` signing provider, DNS-rebinding-protected HTTP transport, and per-tool `_meta.apiVersion`. Pins pdfnative `^1.6.0` (semver-accepts 1.7.0). | +| [`pdfnative-mcp`](https://www.npmjs.com/package/pdfnative-mcp) | **v1.6.0** | Use pdfnative from Claude Desktop, Cursor, Continue, Zed (or any MCP client, stdio or HTTP) — **28 production tools** spanning the engine's full document model: all 13 block kinds in `generate_basic_pdf`, layout options, build-time `encrypt` that keeps the AcroForm, image watermarks, print production (page boxes, bleed, marks, `/UserUnit`, OutputIntent), charts v2 (stacked / area / scatter, dual axis, log & time scales), the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`, `verify_pdf ltv: true`), `update_metadata`, the read-only `inspect_layout` pagination preview, honest PDF/A (`embedFonts` / `strict` / diagnostics), six MCP prompts, token-frugal read modes (`verbosity` / `fields`), the network-free `draft_governance_issue` (AI-governance / HITL), and the MCP **2026-07-28** spec with automatic legacy fallback. No outbound request by default — the only permitted egress is operator-configured TSA / OCSP / CRL. Pins pdfnative `^1.7.0`. | | [`pdfnative-react`](https://www.npmjs.com/package/pdfnative-react) | **v1.1.0** | Write PDFs as declarative JSX — ``, ``, ``, ``, ``, ``… compiled on-device to pdfnative blocks by a custom React 19 reconciler. Render functions (`renderToBytes` / `renderToStream` / `renderToFile`), client hooks & components (`usePdf`, `PDFViewer`, `PDFDownloadLink`), and a versioned `DocSpec` grammar (`docSpecSchema()`) for AI agents. Peer: pdfnative ^1.6.0, React ^19.0.0, Node ≥22. (A 1.5 engine would silently drop the new `` block.) | ```bash @@ -81,7 +81,7 @@ Detailed docs: [CLI guide](docs/guides/cli.md) · [MCP guide](docs/guides/mcp.md - **NPM provenance** — signed builds via GitHub Actions OIDC - **On-device generation** — runs in Node, browsers, Workers, Deno, Bun. No SaaS round-trip; documents never leave the calling process unless your application explicitly sends them - **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home -- **AI client integration** — use pdfnative from Claude Desktop, Cursor, Continue, and Zed via [`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) — **24 production tools** (generate, tables, barcodes, forms, sign, verify, validate, attachments, extraction, inspect, plus page-tree `merge_pdfs` / `split_pdf` / `extract_pages`) +- **AI client integration** — use pdfnative from Claude Desktop, Cursor, Continue, and Zed via [`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) — **28 production tools** (generate, tables, barcodes, forms, sign with the full PAdES ladder, verify, validate, attachments, extraction, inspect, metadata, layout preview, plus page-tree `merge_pdfs` / `split_pdf` / `extract_pages`) - **Command-line interface** — render, sign, verify, inspect, and batch-render PDFs from the shell with [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) — zero-config, scriptable, agent-native (`--json`/`E_*`/`--dry-run`), ideal for CI/CD pipelines - **React renderer** — author PDFs as declarative JSX with [`pdfnative-react`](https://github.com/Nizoka/pdfnative-react): ``/`
`/`` components, `usePdf`/`PDFViewer` client hooks, on-device rendering with no DOM or headless browser @@ -1053,7 +1053,7 @@ See the [CLI Guide](https://pdfnative.dev/guides/cli.html) for the full v1.3.0 r ### pdfnative-mcp — Model Context Protocol server -[`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) v1.5.0 is a **Model Context Protocol server** that bridges pdfnative to any MCP-compatible AI client. Once configured, your AI assistant can generate PDFs and native vector charts, embed barcodes, create, read, fill and flatten forms, sign and verify documents, encrypt and decrypt PDFs, validate PDF/UA structure, embed and extract attachments, extract Unicode text, render international text, merge, split and extract pages, annotate existing PDFs, draft governance-compliant GitHub issues (human-in-the-loop), and inspect existing PDFs — all without writing code. +[`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) v1.6.0 is a **Model Context Protocol server** that bridges pdfnative to any MCP-compatible AI client. Once configured, your AI assistant can generate PDFs and native vector charts, embed barcodes, create, read, fill and flatten forms, sign documents through the complete PAdES ladder (B-B → B-LTA with RFC 3161 timestamps and embedded revocation material), verify signatures and document timestamps, encrypt and decrypt PDFs, validate PDF/UA structure, embed and extract attachments, extract Unicode text, render international text, merge, split and extract pages, annotate existing PDFs, rewrite document metadata, preview pagination without producing a PDF, draft governance-compliant GitHub issues (human-in-the-loop), and inspect existing PDFs — all without writing code. **v1.0.0:** first stable MCP release with 12 tools, `verify_pdf`, `add_attachment` (Factur-X / ZUGFeRD PDF/A-3), `extract_text`, smart-table options, auto-placeholder signing, and `_meta.apiVersion`. @@ -1066,7 +1066,9 @@ See the [CLI Guide](https://pdfnative.dev/guides/cli.html) for the full v1.3.0 r **v1.4.0:** adds `annotate_pdf` (markup via incremental update) and the network-free `draft_governance_issue` (**19 tools**), the MCP `prompts` capability (`governance_contract`, `draft_issue_workflow`), `pageLabels[]` in `inspect_pdf`, and the explicit `math` script — via the pdfnative 1.5.0 engine. -**v1.5.0:** adds `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf` (**24 tools**), `password` support on the read-only and page-tree tools, MCP resources (`pdfnative://output/…`), and tool annotations — via the pdfnative 1.7.0 engine. +**v1.5.0:** adds `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf` (**24 tools**), `password` support on the read-only and page-tree tools, MCP resources (`pdfnative://output/…`), and tool annotations — via the pdfnative 1.7.0 engine. + +**v1.6.0:** adds `add_ltv`, `timestamp_pdf`, `update_metadata`, `inspect_layout` (**28 tools**), all 13 block kinds in `generate_basic_pdf`, layout options and build-time `encrypt` on the document tools, image watermarks, print production, charts v2, honest PDF/A (`embedFonts` / `strict` / diagnostics), PAdES timestamps on `sign_pdf`, four new recipe prompts, and the MCP 2026-07-28 spec — via the pdfnative 1.7.0 engine. ```bash npx -y pdfnative-mcp @@ -1076,16 +1078,18 @@ npx -y pdfnative-mcp | Tool | Purpose | |------|---------| -| `generate_basic_pdf` | Multi-page documents from structured blocks (headings, paragraphs, lists, spacers, charts); optional `outline`, `pageLabels`, `pdfA` | +| `generate_basic_pdf` | Multi-page documents from structured blocks — all 13 block kinds since v1.6.0 (incl. tables, images, links, TOC, barcodes, SVG, form fields); optional `outline`, `pageLabels`, `pdfA`, layout options, build-time `encrypt`, print production | | `add_table` | Smart tables (`wrap`, `repeatHeader`, `zebra`, `caption`, `minRowHeight`, `cellPadding`, `cellBorders`, `cellVAlign`) | | `add_barcode` | QR Code, Code 128, EAN-13, Data Matrix, PDF417 | -| `add_international_text` | 24 script/font codes (22 Unicode scripts + `latin` + `emoji`, plus explicit `math`) with BiDi and OpenType shaping | -| `add_form` | Interactive AcroForm PDFs (`text`, `textarea`, `checkbox`, `radio`, `dropdown`) | -| `embed_image` | Embed a JPEG or PNG image (base64) | -| `prepare_signature_placeholder` | PDF with a `/Sig` field ready to be signed (optional — `sign_pdf` auto-injects one) | -| `sign_pdf` | PAdES CMS signatures (RSA-SHA256 / ECDSA-SHA256 P-256) | +| `add_international_text` | 25 `lang` font codes (22 Unicode scripts + `latin` + `emoji` + explicit `math`) with BiDi and OpenType shaping | +| `add_form` | Interactive AcroForm PDFs (`text`, `textarea`, `checkbox`, `radio`, `dropdown`, `listbox` since v1.6.0) | +| `embed_image` | Embed a JPEG or PNG image (base64), with `align` / `alt` since v1.6.0 | +| `prepare_signature_placeholder` | PDF with a `/Sig` field ready to be signed (optional — `sign_pdf` auto-injects one); `subFilter` / `reserveTimestamp` since v1.6.0 | +| `sign_pdf` | PAdES CMS signatures (RSA-SHA256/384/512 / ECDSA-SHA256 P-256); `profile: 'pades'`, RFC 3161 `timestamp`, cert chains, multiple signatures since v1.6.0 | +| `add_ltv` | Embed `/DSS` + `/VRI` long-term-validation material — PAdES B-LT (v1.6.0) | +| `timestamp_pdf` | Append a `/DocTimeStamp` through the operator TSA — PAdES B-LTA (v1.6.0) | | `validate_pdf` | Read-only PDF/UA structural validation | -| `verify_pdf` | Verify every PAdES signature (integrity + value + optional chain trust) | +| `verify_pdf` | Verify every PAdES signature and `/DocTimeStamp` (integrity + value + optional chain trust; `ltv: true` reports the achieved PAdES level since v1.6.0) | | `add_attachment` | PDF/A-3 with embedded files (Factur-X / ZUGFeRD) | | `extract_attachments` | Extract embedded files (optionally metadata-only) | | `extract_text` | Unicode text extraction (positioned runs; `password` since v1.5.0) | @@ -1094,12 +1098,14 @@ npx -y pdfnative-mcp | `extract_pages` | Pull an arbitrary, order-preserving page subset into a new PDF | | `annotate_pdf` | Overlay markup annotations (9 types) via incremental update; not a redaction | | `draft_governance_issue` | Governance-compliant GitHub-issue draft, network-free, never submits (HITL) | -| `add_chart` | Native vector `bar` / `barH` / `line` / `pie` / `donut` charts, zero rasterisation | +| `add_chart` | Native vector charts, zero rasterisation — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log & time scales | | `read_form_fields` | List an existing AcroForm's fields (types, values, options) | | `fill_form` | Fill and optionally flatten an existing AcroForm (encrypted PDFs supported) | | `encrypt_pdf` | Re-secure with AES-128/AES-256, owner/user passwords + permissions | | `decrypt_pdf` | Remove encryption in-server (RC4 / AES-128 / AES-256 sources) | -| `inspect_pdf` | Structured report (metadata, pages, signatures, PDF/A, attachments, `encryptionInfo`, `pageLabels`) | +| `update_metadata` | Rewrite `/Info` (+ XMP) of an existing PDF via incremental update (v1.6.0) | +| `inspect_pdf` | Structured report (metadata, pages, signatures, PDF/A, attachments, `encryptionInfo`, `pageLabels`; annotation and signature inventories, `dss`, page boxes since v1.6.0) | +| `inspect_layout` | Read-only pagination dry run — page count and block geometry, no PDF produced (v1.6.0) | ### Claude Desktop configuration diff --git a/ROADMAP.md b/ROADMAP.md index 74aae2b..2c36479 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -37,7 +37,7 @@ This document outlines the planned development direction for pdfnative. Prioriti - [x] **Streaming output** — AsyncGenerator-based progressive PDF emission with configurable chunk size (64 KB default), validation for TOC/template incompatibility, concatChunks utility - [x] **PDF parser & modifier** — full PDF reader (tokenizer, object parser, xref table/stream, page tree, FlateDecode inflate) + incremental modification (non-destructive save with /Prev chain) - [x] **npm metadata enrichment** — description enumerates 16 scripts + headline features (BiDi, PDF/A, encryption, signatures, AcroForm, barcodes, SVG); keywords expanded to 27 entries for npm search discoverability (v1.0.2) -- [x] **pdfnative-mcp** — Model Context Protocol server bridging pdfnative to AI clients (Claude Desktop, Cursor, Continue, Zed, ChatGPT): **24 production tools** (`generate_basic_pdf`, `add_table`, `add_barcode`, `add_international_text`, `add_form`, `embed_image`, `prepare_signature_placeholder`, `sign_pdf`, `inspect_pdf`, `verify_pdf`, `add_attachment`, `extract_text`, `validate_pdf`, `extract_attachments`, `merge_pdfs`, `split_pdf`, `extract_pages`, `annotate_pdf`, `draft_governance_issue`, `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf`), stdio/HTTP transport (with DNS-rebinding protection), sandboxed file output, token-frugal read modes (`verbosity`/`fields`), watermark + Unicode normalization options, enriched authoring (`outline`, `pageLabels`, nested lists, `viewerPreferences`, `cellBorders`, `cellVAlign`), an MCP `prompts` capability, and a constant-time `node:crypto` signing provider. Built on pdfnative 1.6.0 as of v1.5.0. See [pdfnative-mcp on GitHub](https://github.com/Nizoka/pdfnative-mcp) +- [x] **pdfnative-mcp** — Model Context Protocol server bridging pdfnative to AI clients (Claude Desktop, Cursor, Continue, Zed, ChatGPT): **28 production tools** (`generate_basic_pdf`, `add_table`, `add_barcode`, `add_international_text`, `add_form`, `embed_image`, `prepare_signature_placeholder`, `sign_pdf`, `add_ltv`, `timestamp_pdf`, `inspect_pdf`, `inspect_layout`, `verify_pdf`, `add_attachment`, `extract_text`, `validate_pdf`, `extract_attachments`, `merge_pdfs`, `split_pdf`, `extract_pages`, `annotate_pdf`, `update_metadata`, `draft_governance_issue`, `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf`), the complete PAdES ladder B-B → B-LTA, print production, charts v2, all 13 block kinds, stdio/HTTP transport (with DNS-rebinding protection and an opt-in bearer token), sandboxed file output, token-frugal read modes (`verbosity`/`fields`), watermark + Unicode normalization options, enriched authoring (`outline`, `pageLabels`, nested lists, `viewerPreferences`, `cellBorders`, `cellVAlign`), six MCP prompts, a constant-time `node:crypto` signing provider, and the MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0 as of v1.6.0. See [pdfnative-mcp on GitHub](https://github.com/Nizoka/pdfnative-mcp) - [x] **Watermark auto-fit** (v1.1.0) — text watermarks with aggressive `fontSize` + `angle` combinations are now scaled down so the rotated bounding box fits within the page. Default `autoFit: true`; opt-out via `autoFit: false` for byte-stable v1.0.x output. ([src/core/pdf-watermark.ts](src/core/pdf-watermark.ts)) - [x] **Unicode ellipsis** (v1.1.0) — `truncate()` and TOC truncation use `…` (U+2026) instead of `..` / `...` for professional typographic output (single grapheme cluster, ~50% narrower in Latin mode, identical glyph in CIDFont mode). - [x] **Pixel-based truncation API** (v1.1.0) — new `truncateToWidth(str, maxWidthPt, sz, enc)` exported from the root for measurement-based string shortening that respects proportional font widths. diff --git a/docs/assets/ecosystem.json b/docs/assets/ecosystem.json index 360989b..95e7606 100644 --- a/docs/assets/ecosystem.json +++ b/docs/assets/ecosystem.json @@ -1,6 +1,6 @@ { "$comment": "Single source of truth for every version, count and inventory quoted anywhere in the documentation. Edit THIS file, then run `npm run verify:docs` — it reports every doc that disagrees. Never hand-edit a number in a doc without updating this manifest first.", - "verifiedOn": "2026-08-21", + "verifiedOn": "2026-08-25", "packages": { "pdfnative": { "version": "1.7.0", @@ -46,17 +46,18 @@ } }, "pdfnative-mcp": { - "version": "1.5.0", + "version": "1.6.0", "pinField": "dependencies", - "pin": "^1.6.0", + "pin": "^1.7.0", "repo": "https://github.com/Nizoka/pdfnative-mcp", - "toolCount": 24, + "toolCount": 28, "tools": [ "add_attachment", "add_barcode", "add_chart", "add_form", "add_international_text", + "add_ltv", "add_table", "annotate_pdf", "decrypt_pdf", @@ -68,18 +69,25 @@ "extract_text", "fill_form", "generate_basic_pdf", + "inspect_layout", "inspect_pdf", "merge_pdfs", "prepare_signature_placeholder", "read_form_fields", "sign_pdf", "split_pdf", + "timestamp_pdf", + "update_metadata", "validate_pdf", "verify_pdf" ], "prompts": [ "governance_contract", - "draft_issue_workflow" + "draft_issue_workflow", + "pades_ladder", + "print_ready", + "reproducible_output", + "pdfa_valid" ] }, "pdfnative-react": { @@ -148,9 +156,9 @@ }, { "id": "mcp-tool-count", - "canonical": "24 tools", + "canonical": "28 tools", "match": "\\b(\\d+)\\s+(?:[A-Za-z][\\w-]*\\s+){0,2}tools\\b", - "expect": 24, + "expect": 28, "requireIn": [ "docs/guides/mcp.md", "docs/index.html", diff --git a/docs/assets/versions.js b/docs/assets/versions.js index fd46872..f14d64b 100644 --- a/docs/assets/versions.js +++ b/docs/assets/versions.js @@ -33,9 +33,9 @@ // the whole site; these mirror the latest published releases so an offline // visitor still sees a sensible value. Bumped at every release. var FALLBACK = { - 'pdfnative': { version: '1.6.0', pin: null, pinField: null }, + 'pdfnative': { version: '1.7.0', pin: null, pinField: null }, 'pdfnative-cli': { version: '1.3.0', pin: '^1.6.0', pinField: 'dependencies' }, - 'pdfnative-mcp': { version: '1.5.0', pin: '^1.6.0', pinField: 'dependencies' }, + 'pdfnative-mcp': { version: '1.6.0', pin: '^1.7.0', pinField: 'dependencies' }, 'pdfnative-react': { version: '1.1.0', pin: '^1.6.0', pinField: 'peerDependencies' } }; diff --git a/docs/guides/agentic-workflows.md b/docs/guides/agentic-workflows.md index 65edaf1..c891d24 100644 --- a/docs/guides/agentic-workflows.md +++ b/docs/guides/agentic-workflows.md @@ -178,7 +178,7 @@ under the [AI-governance / human-in-the-loop contract](ai-governance.md). - [AI governance & human-in-the-loop](ai-governance.md) — the contract that keeps repository changes human-gated. -- [MCP integration](mcp.md) — the 24 MCP tools, including `embed_image` and +- [MCP integration](mcp.md) — the 28 MCP tools, including `embed_image` and `draft_governance_issue`. - [CLI guide](cli.md) — driving pdfnative from the shell. - [Font validation](font-validation.md) — `validateFontData()` for sanity-checking diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index 5452f73..e71872a 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -111,7 +111,7 @@ The architecture diagram above shows the **internal library modules**. External [shell / CI / Docker] │ pdfnative-cli (npm) — 17 commands │ └─────────────────────────────────────────────┘ ┌─────────────────────────────────────────────┐ - [Claude / Cursor / …] │ pdfnative-mcp (npm) — 24 AI tools │ + [Claude / Cursor / …] │ pdfnative-mcp (npm) — 28 AI tools │ └───────────────────────────────────────────┘ ┌─────────────────────────────────────────────┐ [React / Next.js app] │ pdfnative-react (npm) — declarative JSX │ @@ -142,13 +142,13 @@ Like `pdfnative-mcp` and `pdfnative-react`, the CLI lives in a separate reposito ### pdfnative-mcp -[pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is a **Model Context Protocol server** that wraps the pdfnative public API and exposes it as 24 structured tools to any MCP-compatible AI client (Claude Desktop, Cursor, Continue, Zed, ChatGPT, …). It commits to API stability via a per-tool `_meta.apiVersion` field. +[pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is a **Model Context Protocol server** that wraps the pdfnative public API and exposes it as 28 structured tools to any MCP-compatible AI client (Claude Desktop, Cursor, Continue, Zed, ChatGPT, …). It commits to API stability via a per-tool `_meta.apiVersion` field. ``` [Claude Desktop / Cursor / Continue / Zed] │ MCP stdio protocol ┌──────────────────────────┐ - │ pdfnative-mcp (npm) │ ← MCP server, 24 tools + │ pdfnative-mcp (npm) │ ← MCP server, 28 tools └──────────────────────────┘ │ import { buildDocumentPDFBytes, … } from 'pdfnative' ┌──────────────────────────┐ diff --git a/docs/guides/mcp.md b/docs/guides/mcp.md index e068c39..731830a 100644 --- a/docs/guides/mcp.md +++ b/docs/guides/mcp.md @@ -1,6 +1,6 @@ # pdfnative-mcp — AI Client Integration Guide -> **Tracks the latest published `pdfnative-mcp`** (v1.5.0, built on pdfnative 1.7.0). Full release notes: [pdfnative-mcp releases](https://github.com/Nizoka/pdfnative-mcp/releases). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). +> **Tracks the latest published `pdfnative-mcp`** (v1.6.0, built on pdfnative 1.7.0). Full release notes: [pdfnative-mcp releases](https://github.com/Nizoka/pdfnative-mcp/releases). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is an **MCP server** that exposes the full pdfnative library to any AI client supporting the [Model Context Protocol](https://modelcontextprotocol.io) — Claude Desktop, Cursor, Continue, Zed, ChatGPT, and more. @@ -120,26 +120,38 @@ In your Zed `settings.json`: | `PDFNATIVE_MCP_OUTPUT_DIR` | Absolute path to the sandbox directory. **Required to enable `outputMode: "file"`**. When unset, only `base64` output is available. (The misspelt `PDFNATIVE_MPC_OUTPUT_DIR` still works as a deprecated alias.) | | `PDFNATIVE_MCP_CACHE_DIR` | Absolute path to enable the persistent SHA-256-keyed result cache (1 h TTL, 256 MiB LRU). When unset, the cache is disabled. | | `PDFNATIVE_MCP_PORT` | When set to a valid port (1–65535), starts an HTTP server on `http://127.0.0.1:/mcp` instead of stdio. | +| `PDFNATIVE_MCP_HTTP_TOKEN` _(v1.6.0)_ | Opt-in bearer token for the HTTP transport (≥ 16 characters, no whitespace — a weaker value aborts startup). When set, `/mcp` requires `Authorization: Bearer `; otherwise requests get `401` + `WWW-Authenticate`. Without it the loopback endpoint has **no authentication**. Never logged. | +| `PDFNATIVE_MCP_MAX_INFLATE_BYTES` _(v1.6.0)_ | Operator override of the engine's 100 MiB per-stream decompression cap (integer ≥ 1024, read once at startup; an invalid value refuses to start with one clean `fatal:` line). | +| `PDFNATIVE_MCP_TSA_URL` _(v1.6.0)_ | RFC 3161 timestamp authority endpoint. Required by `sign_pdf timestamp: true` and `timestamp_pdf` (`TSA_NOT_CONFIGURED` otherwise). | +| `PDFNATIVE_MCP_TSA_AUTH` _(v1.6.0)_ | Optional `Authorization` header value sent to the TSA. Treated as a secret; never logged. | +| `PDFNATIVE_MCP_REVOCATION` _(v1.6.0)_ | Revocation sources for `add_ltv mode: 'online'` — `ocsp`, `crl`, or `ocsp,crl` (`REVOCATION_NOT_CONFIGURED` otherwise). | +| `PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS` _(v1.6.0)_ | Mandatory allow-list of hosts the OCSP/CRL fetcher may contact. A certificate-supplied URL outside the list fails with `NETWORK_HOST_NOT_ALLOWED`. | +| `PDFNATIVE_MCP_NETWORK_TIMEOUT_MS` _(v1.6.0)_ | Network timeout for TSA/OCSP/CRL requests (1000–120000 ms, default 10000). | + +> **Network charter** _(v1.6.0)_. The server still makes **no outbound request by default**. The only egress it can ever perform goes to the TSA / OCSP / CRL endpoints the **operator** configures via the variables above — URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard (allow-list, http(s) only, no credentials, no redirects, internal address literals rejected, size caps, timeouts). --- ## Tool reference -`pdfnative-mcp` exposes **24 tools**: +`pdfnative-mcp` exposes **28 tools**: | Tool | Purpose | |---|---| -| `generate_basic_pdf` | Multi-page A4 documents from structured blocks (headings, paragraphs, lists, spacers, charts, page breaks). Accepts optional `pdfA`. | +| `generate_basic_pdf` | Multi-page documents from structured blocks — all **13 block kinds** since v1.6.0 (headings, paragraphs, lists, tables, images, links, TOC, barcodes, SVG, form fields, charts, page breaks, spacers). Accepts optional `pdfA`, layout options, build-time `encrypt`, and print-production fields. | | `add_table` | Tabular PDF reports from column headers and data rows. Optional `autoFitColumns` and `clipCells`. Accepts `pdfA`. | | `add_barcode` | QR Code, Code 128, EAN-13, Data Matrix, PDF417 — embedded in a single-page PDF. Accepts `pdfA`. | -| `add_international_text` | 24 `lang` font codes — the 22 writing systems plus `latin` and `emoji` — with BiDi & OpenType shaping, plus the explicit `math` script (Noto Sans Math, on-demand). `lang` accepts `string`, `string[]`, or comma-separated. | -| `add_form` | Interactive AcroForm PDFs with text fields, checkboxes, radio buttons, and dropdowns. Accepts `pdfA`. | -| `embed_image` | Embed a JPEG or PNG image (base64-encoded) into a titled PDF document. Accepts `pdfA`. | -| `prepare_signature_placeholder` | Create a PDF with a `/Sig` AcroForm placeholder ready to be signed. Accepts `pdfA`. | -| `sign_pdf` | PAdES-style CMS digital signatures (RSA-SHA256 / ECDSA-SHA256 P-256). | -| `inspect_pdf` | Read-only inspection. Returns `version`, `pageCount`, `encryption`, `pdfA`, `signatureCount`, `info`, optional `perPage`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed`. | +| `add_international_text` | 25 `lang` font codes — the 22 writing systems plus `latin`, `emoji` and the explicit `math` script (Noto Sans Math, on-demand) — with BiDi & OpenType shaping. `lang` accepts `string`, `string[]`, or comma-separated. | +| `add_form` | Interactive AcroForm PDFs with text fields, text areas, checkboxes, radio buttons, dropdowns, and list boxes _(v1.6.0)_. Accepts `pdfA`. | +| `embed_image` | Embed a JPEG or PNG image (base64-encoded) into a titled PDF document, with `align` and `alt` _(v1.6.0)_. Accepts `pdfA`. | +| `prepare_signature_placeholder` | Create a PDF with a `/Sig` AcroForm placeholder ready to be signed; `subFilter`, `reserveTimestamp` and frozen signer metadata _(v1.6.0)_. Accepts `pdfA`. | +| `sign_pdf` | PAdES CMS digital signatures — RSA-SHA256/384/512 and ECDSA-SHA256 P-256, `profile: 'pades'`, RFC 3161 `timestamp` (B-T), certificate chains, named fields and multiple signatures _(v1.6.0)_. | +| `add_ltv` _(v1.6.0)_ | Embed long-term-validation material (`/DSS` + `/VRI`) into a signed PDF — PAdES **B-LT** — online through the operator-configured revocation provider or offline from caller-supplied material. | +| `timestamp_pdf` _(v1.6.0)_ | Append a `/DocTimeStamp` (ETSI.RFC3161) through the operator TSA — PAdES **B-LTA** — with auto-suffixed field names for periodic re-timestamping. | +| `inspect_pdf` | Read-only inspection. Returns `version`, `pageCount`, `encryption`, `pdfA`, `signatureCount`, `info`, optional `perPage`, optional `pageLabels[]`, and — _(v1.6.0)_ — an optional `signatures[]` inventory, `annotations[]`, page boxes + `userUnit`, `dss` / `docTimestampCount` / `trapped`, plus `checks` + `checksPassed`. | +| `inspect_layout` _(v1.6.0)_ | Read-only pagination **dry run** — page count, page geometry and each block's position for a prospective document, with no PDF produced. | | `validate_pdf` | Read-only PDF/UA structural validation (`valid`, `errors`, `warnings`). | -| `verify_pdf` | Real CMS/PKCS#7 signature verification — RSA & ECDSA, message digest, certificate chain. (RFC 3161 timestamp validation is a `pdfnative-cli verify` feature, not part of this server.) | +| `verify_pdf` | Real CMS/PKCS#7 signature verification — RSA & ECDSA, message digest, certificate chain. Since v1.6.0, `/DocTimeStamp` entries are verified as RFC 3161 tokens and `ltv: true` reports the achieved PAdES level (B-B → B-LTA). | | `add_attachment` | Embed files (e.g. Factur-X / ZUGFeRD e-invoice XML) into PDF/A-3b output. | | `extract_attachments` | Extract embedded files from an existing PDF (optionally metadata-only). | | `extract_text` | Extract text content from an existing PDF via the native parser. | @@ -148,13 +160,14 @@ In your Zed `settings.json`: | `extract_pages` | Pull an arbitrary, order-preserving page subset (max 5000) into a single PDF. | | `annotate_pdf` | Overlay markup annotations (text / highlight / underline / strikeout / squiggly / square / circle / line / freetext) on an existing PDF via incremental update. A visual review layer, **not** a redaction. | | `draft_governance_issue` | Assemble a governance-compliant GitHub-issue draft plus a structured `compliance` report **locally** — network-free by construction; never submits. | -| `add_chart` _(v1.5.0)_ | Render a bar, horizontal-bar, line, pie or donut chart as **native PDF vector paths** — no rasterisation and no image round-trip. Tagged as `/Figure` with alt text when `pdfA` is set. | +| `add_chart` _(v1.5.0)_ | Render a chart as **native PDF vector paths** — nine types since v1.6.0 (`bar`, `barH`, `stackedBar`, `stackedBarH`, `line`, `area`, `scatter`, `pie`, `donut`), with a secondary axis, log and time scales, and data labels. Tagged as `/Figure` with alt text when `pdfA` is set. | | `read_form_fields` _(v1.5.0)_ | List an existing AcroForm's fields with their types, current values and available options — the read half of the fill round-trip. Accepts `password`. | | `fill_form` _(v1.5.0)_ | Fill AcroForm field values and optionally `flatten` them into static page content. Works on encrypted PDFs via incremental update. | | `encrypt_pdf` _(v1.5.0)_ | Re-secure an existing PDF with AES-128 or AES-256 — owner/user passwords and an explicit permission set. | | `decrypt_pdf` _(v1.5.0)_ | Remove encryption from a password-protected PDF **in-server** — RC4, AES-128 and AES-256 sources. | +| `update_metadata` _(v1.6.0)_ | Rewrite an existing PDF's `/Info` dictionary (+ XMP) — title, author, subject, keywords, pinned `modDate` — as a non-destructive incremental update. | -Every tool now publishes an `outputSchema` advertised in `tools/list` per the [MCP 2025-06-18 spec](https://modelcontextprotocol.io/specification/2025-06-18), enabling clients to statically validate responses. +Every tool publishes an `outputSchema` advertised in `tools/list`. Since v1.6.0 the server speaks the [MCP 2026-07-28 spec](https://modelcontextprotocol.io/specification/2026-07-28) (stateless envelope, `server/discover`, `resultType`, cache hints) on SDK v2, while 2025-era clients (2025-11-25 / 2025-06-18 / 2025-03-26) keep working through the automatic legacy fallback. --- @@ -177,7 +190,17 @@ Produces a multi-page document from a list of content blocks. } ``` -**Block types supported:** `heading` (levels 1–3), `paragraph`, `list` (`bullet` / `numbered`, nested items to depth 6), `pageBreak`, `spacer` (`height` in points), and `chart` (same body as `add_chart`). +**Block types supported:** all **13 engine block kinds** since v1.6.0 — `heading` (levels 1–3), `paragraph`, `list` (`bullet` / `numbered`, nested items to depth 6), `table`, `image`, `link` (`http:` / `https:` / `mailto:` only), `toc` (a contents page with internal `/GoTo` links, pairs with `outline: 'auto'`), `barcode`, `svg` (a safe subset: paths, shapes and text — no XML parser, no external reference ever fetched), `formField`, `chart` (same body as `add_chart`), `pageBreak`, and `spacer` (`height` in points). The `table` / `image` / `barcode` / `formField` blocks share their body with the dedicated tools, so a standalone artefact and an inline block validate and render identically. + +**Layout options** _(v1.6.0)_, available on all nine document tools: `pageSize` (`A4` default, `Letter`, `Legal`, `A3`, `Tabloid`), `margins` (all four sides, 0–200 pt), running `headerTemplate` / `footerTemplate` (`left` / `center` / `right` zones with `{page}` `{pages}` `{title}` `{date}` placeholders — a `footerTemplate` replaces the default footer, so `footerText` is then ignored), `compress`, and `debug`. + +**Build-time `encrypt`** _(v1.6.0)_, on `generate_basic_pdf`, `add_table`, `add_form`, `add_international_text`, `embed_image`, `add_barcode` and `add_chart`: AES-128 (default) or AES-256 with owner/user passwords and permissions — and unlike `encrypt_pdf`, it **keeps the AcroForm**, making encrypted fillable forms reachable. Exclusive with `pdfA` (ISO 19005-1 §6.3.2); never cached. + +**Print production** _(v1.6.0)_, on the nine document tools: `print` (TrimBox / BleedBox / ArtBox / CropBox, a `bleed` shorthand, crop + registration `marks`, `/UserUnit`), `metadata` (`/Author`, `/Subject`, `/Keywords`, `/Trapped` with XMP parity), and `outputIntent` (custom RGB ICC profile). `viewerPreferences` gains `duplex`, `pickTrayByPDFSize`, `printPageRange` (1-based), `numCopies`. Boxes survive `merge_pdfs` / `split_pdf` / `extract_pages` and are reported by `inspect_pdf`. + +**Honest PDF/A** _(v1.6.0)_: text rendered through the viewer's base-14 Helvetica is not embedded, so a PDF/A claim on such a file is rejected by veraPDF. `embedFonts: true` embeds Noto Sans Latin for a valid claim; `strict: true` fails instead of producing a non-conformant file; `includeDiagnostics: true` echoes the engine's diagnostics (`PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, `PDFA_DEVICE_CMYK_IMAGE`). + +**Reproducible output** _(v1.6.0)_: `creationDate` (ISO-8601) on all nine document tools pins `/Info /CreationDate`, the XMP dates and therefore the trailer `/ID` — byte-identical output on the same host time zone. --- @@ -261,7 +284,7 @@ Creates an interactive AcroForm PDF. } ``` -**Field types:** `text`, `textarea`, `checkbox`, `radio`, `dropdown`. +**Field types:** `text`, `textarea`, `checkbox`, `radio`, `dropdown`, and `listbox` _(v1.6.0)_. Text fields accept an optional `placeholder` _(v1.6.0)_. Since v1.6.0, `textarea` maps to the engine's true multi-line field (`/Ff 4096`) — in v1.5.0 it rendered as a single-line widget. --- @@ -280,6 +303,8 @@ Creates an interactive AcroForm PDF. > **Note:** Alpha-channel PNGs (color type 6) are not supported. Pre-process such images to remove the alpha channel before embedding. +Since v1.6.0 the tool also accepts `align` (horizontal placement) and `alt` (accessibility text written as `/Figure /Alt` in tagged output). + --- ### `prepare_signature_placeholder` @@ -299,6 +324,8 @@ Creates a PDF pre-wired with an AcroForm `/Sig` field, ready to be signed by `si } ``` +Since v1.6.0 the placeholder also takes `subFilter`, `reserveTimestamp` (extra `/Contents` room for a later RFC 3161 token), `placeholderBytes`, and `signingTime` — and the signer metadata (`signerName`, `reason`, `location`, `contactInfo`, `signingTime`) is **baked into the `/Sig` dictionary at placeholder time**, fixing a pre-1.7 engine bug where those values never reached the signed document. + --- ### `sign_pdf` @@ -320,6 +347,17 @@ Signs any PDF. When the input already contains a `/Sig` placeholder (e.g. produc For ECDSA P-256: use `"algorithm": "ecdsa-sha256"` and supply either `ecPrivateScalarHex` (64 hex chars, raw scalar `d`) or `ecPrivateKeyDerBase64` (SEC1 / PKCS#8 DER, base64 — signed through the constant-time `node:crypto` path) instead of `rsaKeyPkcs1DerBase64`. The two EC inputs are mutually exclusive. +**PAdES and timestamps** _(v1.6.0)_: + +- `algorithm` also accepts `rsa-sha384` and `rsa-sha512`. +- `profile: 'pades'` produces an ETSI.CAdES.detached signature (PAdES **B-B**). +- `timestamp: true` requests an RFC 3161 timestamp from the operator-configured TSA (`PDFNATIVE_MCP_TSA_URL`) and embeds it in the CMS — PAdES **B-T**. Fails with `TSA_NOT_CONFIGURED` when no TSA is set, `TSA_REJECTED` when the authority declines. +- `certChainDerBase64` embeds intermediate certificates alongside the signer certificate. +- `fieldName` targets a specific `/Sig` field (`SIGNATURE_FIELD_NOT_FOUND` when absent, `PLACEHOLDER_AMBIGUOUS` when several placeholders exist and none is named); `allowMultiple: true` adds a signature next to existing ones instead of rejecting. +- The default placeholder reservation is now `max(16384, estimated CMS size)` — plus 8192 bytes when `timestamp: true` — instead of a flat 16384 bytes. To pin it exactly, build the placeholder with `prepare_signature_placeholder` and its `placeholderBytes` input. + +To climb the rest of the PAdES ladder, follow with [`add_ltv`](#add_ltv-v160) (B-LT) and [`timestamp_pdf`](#timestamp_pdf-v160) (B-LTA), then check the achieved level with `verify_pdf ltv: true`. The server's `pades_ladder` prompt walks through the full recipe. + --- ### `inspect_pdf` @@ -337,12 +375,14 @@ Read-only PDF inspection over `openPdf()`. Never modifies the input. **Inputs:** - `pdfBase64` — base64 PDF bytes (required). - `password` — password for an encrypted PDF *(v1.5.0)*. -- `pages` — when `true`, includes per-page `index`, `width`, `height`. -- `check` — array of CI assertions. Allowed values: `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`. The response includes `checks` (per-assertion result) and `checksPassed` (boolean AND). +- `pages` — when `true`, includes per-page `index`, `width`, `height` — and, since v1.6.0, the declared page boxes and `userUnit`. +- `signatures` *(v1.6.0)* — when `true`, a per-signature inventory: `subFilter`, `isDocTimestamp`, `isPlaceholder`, `byteRange`, `vriKey`. +- `annotations` *(v1.6.0)* — when `true`, an `annotations[]` list (0-based `page`, `subtype`, `rect`, and when present `contents`, `title`, `color`, `quadPoints`, link `url`) plus `annotationCount`. +- `check` — array of CI assertions. Allowed values: `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`, and — *(v1.6.0)* — `dss`, `docTimestamp`, `trapped`, `annotations`. The response includes `checks` (per-assertion result) and `checksPassed` (boolean AND). Since v1.6.0, `checks` contains **only the requested keys**. - `verbosity` — `'full'` (default) or `'summary'` (token-frugal scalar subset). - `fields` — optional dot-path projection of the result. -**Outputs:** `version`, `pageCount`, `encryption` (`'none'` / `'aes-128'` / `'aes-256'` / `'rc4'` / `'unknown'`), optional `encryptionInfo` (`{ algorithm, revision, authenticatedAs }`, present when the document is encrypted and opened successfully), `pdfA` (`null` or the detected claim string), `signatureCount`, `hasSignaturePlaceholder`, `attachments[]` (embedded-file summaries), `info` (decoded `/Info` entries), optional `perPage[]`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed`. +**Outputs:** `version`, `pageCount`, `encryption` (`'none'` / `'aes-128'` / `'aes-256'` / `'rc4'` / `'unknown'`), optional `encryptionInfo` (`{ algorithm, revision, authenticatedAs }`, present when the document is encrypted and opened successfully), `pdfA` (`null` or the detected claim string), `signatureCount`, `hasSignaturePlaceholder`, `attachments[]` (embedded-file summaries), `info` (decoded `/Info` entries), optional `perPage[]`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed` — plus, presence-gated since v1.6.0, `dss`, `docTimestampCount` and `trapped`. Useful in CI as a final assertion step before publishing a PDF artifact: @@ -444,7 +484,7 @@ Assembles a governance-compliant GitHub-issue draft plus a structured `complianc ### `add_chart` _(v1.5.0)_ -Renders a bar, horizontal-bar, line, pie or donut chart as **native PDF vector paths** — no rasterisation, no image round-trip. Multi-series bar/line, legends, gridlines and negative values are supported; the chart is tagged `/Figure` with `/Alt` (auto-generated when `altText` is omitted). +Renders a chart as **native PDF vector paths** — no rasterisation, no image round-trip. Multi-series bar/line, legends, gridlines and negative values are supported; the chart is tagged `/Figure` with `/Alt` (auto-generated when `altText` is omitted). ```jsonc { @@ -457,7 +497,9 @@ Renders a bar, horizontal-bar, line, pie or donut chart as **native PDF vector p } ``` -**Inputs:** `chartType` (`bar` / `barH` / `line` / `pie` / `donut`) and `series` (required; pie/donut take exactly one series), plus `title`, `legend` (`'bottom'` / `'none'`), `markers` (line series), `colors` (hex palette override), `altText`, `intro` (paragraph above the chart), `pdfA`, `outputMode` / `outputPath`. For a chart amongst headings/paragraphs/tables, use a `chart` block inside `generate_basic_pdf` — both build identical pdfnative blocks. +**Inputs:** `chartType` (`bar` / `barH` / `line` / `pie` / `donut`, plus — _(v1.6.0)_ — `stackedBar` / `stackedBarH` / `area` / `scatter`) and `series` (required; pie/donut take exactly one series), plus `title`, `legend` (`'bottom'` / `'none'`), `markers` (line series), `colors` (hex palette override), `altText`, `intro` (paragraph above the chart), `pdfA`, `outputMode` / `outputPath`. For a chart amongst headings/paragraphs/tables, use a `chart` block inside `generate_basic_pdf` — both build identical pdfnative blocks. + +**Charts v2** _(v1.6.0)_: per-series `xValues` with `xAxis.type: 'linear' | 'time'` (UTC-deterministic time axes), a secondary right axis (`axis2`), `axis.scale: 'log'`, `dataLabels`, and `labelStride` / `labelRotation` for crowded x labels. Engine cross-field rules surface as `CHART_ERROR` with a remedy. ### `read_form_fields` _(v1.5.0)_ @@ -517,22 +559,101 @@ Opens an encrypted PDF (RC4, AES-128 or AES-256) and emits an unencrypted copy * > The rebuild drops signatures and `/AcroForm`. To *read* an encrypted PDF without rebuilding it, pass `password` to `inspect_pdf` / `extract_text` / `extract_attachments` instead. Excluded from the response cache. +### `add_ltv` _(v1.6.0)_ + +Embeds long-term-validation material — a `/DSS` (Document Security Store) with `/VRI` entries — into an already-signed PDF via incremental update, taking a PAdES B-T document to **B-LT**. + +```jsonc +{ + "pdfBase64": "", + "mode": "online", + "preferOcsp": true, + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `mode` (`'online'` default — fetches OCSP responses / CRLs through the operator-configured revocation provider; `'offline'` — embeds parse-validated caller-supplied material), `preferOcsp`, `certificatesDerBase64[]` / `ocspResponsesDerBase64[]` / `crlsDerBase64[]` (offline material), `outputMode` / `outputPath`. The structured result includes a `summary` of what was embedded. + +**Errors:** `LTV_NO_SIGNATURE` (nothing to validate), `LTV_EMPTY` (no material gathered), `LTV_MATERIAL_INVALID`, `LTV_ERROR`, `REVOCATION_NOT_CONFIGURED` (online mode without `PDFNATIVE_MCP_REVOCATION`), `NETWORK_HOST_NOT_ALLOWED`, `NETWORK_ERROR`, `ENCRYPTED_SOURCE` (decrypting would destroy the signatures — sign before encrypting). + +> Online mode is the **only** network-touching path in the server besides the TSA, and it only ever contacts operator-allow-listed hosts. See *Environment variables* above. + +### `timestamp_pdf` _(v1.6.0)_ + +Appends a `/DocTimeStamp` signature field (ETSI.RFC3161) through the operator-configured TSA, taking a B-LT document to **B-LTA**. The token's status, message imprint and nonce are checked before embedding; its own CMS signature is verified by `verify_pdf`. + +```jsonc +{ + "pdfBase64": "", + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `fieldName` (auto-suffixed `DocTimeStamp1`, `DocTimeStamp2`, … when omitted — supporting periodic re-timestamping), `placeholderBytes` (4096–65536), `outputMode` / `outputPath`. + +**Errors:** `TSA_NOT_CONFIGURED`, `TSA_REJECTED`, `NETWORK_ERROR`, `ENCRYPTED_SOURCE`. + +### `update_metadata` _(v1.6.0)_ + +Rewrites an existing PDF's `/Info` dictionary — and its XMP packet when present — via a non-destructive **incremental update**, so earlier revisions stay a byte-exact prefix. + +```jsonc +{ + "pdfBase64": "", + "title": "Quarterly Report — Final", + "author": "Finance Team", + "keywords": "finance, Q1, 2026", + "modDate": "2026-08-25T09:00:00+02:00", + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `title`, `author`, `subject`, `keywords` (≤ 1000 characters), `modDate` (ISO-8601 with offset — pinned for reproducible bytes), `outputMode` / `outputPath`. + +**Errors:** `ENCRYPTED_SOURCE` (use `decrypt_pdf` → `update_metadata` → `encrypt_pdf`), `METADATA_ERROR`, `PDF_PARSE_FAILED`. + +### `inspect_layout` _(v1.6.0)_ + +A read-only pagination **dry run**: measures how a prospective document would paginate — without producing a PDF and without consuming output tokens on base64 bytes. The seventh read tool with `verbosity` / `fields`. + +```jsonc +{ + "title": "Q1 2026 Report", + "blocks": [ + { "type": "heading", "text": "Executive Summary", "level": 1 }, + { "type": "paragraph", "text": "Revenue grew 24 % year over year." } + ], + "verbosity": "summary" +} +// → { "pageWidth": 595.28, "pageHeight": 841.89, "totalPages": 1, "blockCount": 2 } +``` + +**Inputs:** `title` and `blocks` (required), plus every input that moves a block — `footerText`, `pdfA`, `normalize`, `embedFonts`, `pageSize`, `margins`, `headerTemplate`, `footerTemplate` — and `verbosity` / `fields`. + +**Outputs (full):** `pageWidth`, `pageHeight`, `margins`, `totalPages`, and `pages[].blocks[]` with each block's `type`, `page`, `x`, `top`, `width`, `height` (2-decimal points). + +> **Known engine gap:** a `toc` block is measured as 0 pt, so a document with a printed contents page may paginate one page later than previewed. + --- ## MCP prompts -Since v1.4.0 the server advertises the MCP **`prompts`** capability with two prompts, sourced from the same governance contract the tools enforce: +Since v1.4.0 the server advertises the MCP **`prompts`** capability; v1.6.0 grows it to **six prompts** — the two governance prompts plus four recipe prompts: | Prompt | Purpose | |---|---| | `governance_contract` | The full AI-governance / Human-in-the-Loop contract. | | `draft_issue_workflow` | The step-by-step recipe for producing a compliant issue draft with `draft_governance_issue`. | +| `pades_ladder` _(v1.6.0)_ | The B-B → B-T → B-LT → B-LTA recipe: `sign_pdf` → `add_ltv` → `timestamp_pdf`, verified with `verify_pdf ltv: true`. | +| `print_ready` _(v1.6.0)_ | Producing press-ready output: bleed, printer's marks, custom OutputIntent. | +| `reproducible_output` _(v1.6.0)_ | Byte-stable output via pinned `creationDate` / `signingTime` / `modDate`. | +| `pdfa_valid` _(v1.6.0)_ | Producing a PDF/A file that veraPDF actually accepts (`embedFonts`, `strict`, diagnostics). | ## Error codes | `code` | Raised by | Meaning / fix | |---|---|---| -| `ENCRYPTED_SOURCE` | `annotate_pdf` | The source PDF is encrypted; `annotate_pdf` has no `password` parameter (nor do `sign_pdf` and `validate_pdf`). Use `decrypt_pdf` first (note: that rebuild drops signatures/forms), or pass `password` to the tools that accept it. | +| `ENCRYPTED_SOURCE` | `annotate_pdf`, `update_metadata`, `add_ltv`, `timestamp_pdf` | The source PDF is encrypted and the tool has no `password` parameter. Remedies are tool-specific since v1.6.0: for `annotate_pdf` / `update_metadata`, `decrypt_pdf` → edit → `encrypt_pdf`; for `add_ltv` / `timestamp_pdf`, decrypting would destroy the signatures — sign before encrypting. | | `PASSWORD_REQUIRED` *(v1.5.0)* | password-aware tools (`inspect_pdf`, `verify_pdf`, `extract_text`, `extract_attachments`, `read_form_fields`, `fill_form`, page-tree trio, `encrypt_pdf`, `decrypt_pdf`) | The source is encrypted and no `password` was supplied. | | `PASSWORD_INVALID` *(v1.5.0)* | password-aware tools | The supplied `password` does not open the document. | | `ENCRYPTION_UNSUPPORTED` *(v1.5.0)* | password-aware tools | The document uses an encryption scheme the reader does not support. | @@ -542,6 +663,20 @@ Since v1.4.0 the server advertises the MCP **`prompts`** capability with two pro | `OUTPUT_TOO_LARGE` | PDF-emitting tools | An emitted PDF exceeds 50 MiB (for `split_pdf`, also the 200 MiB aggregate), or assembly exceeds `maxOutputSizeBytes` (default 256 MiB). | | `PDF_PARSE_FAILED` | page-tree tools | The source bytes are not a parseable PDF. | | `GOVERNANCE_VIOLATION` | `draft_governance_issue` | The draft breaks the AI-governance contract (proposes a runtime dependency, omits a reproduction, or `duplicateSearchPerformed: false`). Fix the draft and retry. | +| `TSA_NOT_CONFIGURED` *(v1.6.0)* | `sign_pdf` (`timestamp: true`), `timestamp_pdf` | No `PDFNATIVE_MCP_TSA_URL` is set. Configure the TSA in the server environment. | +| `TSA_REJECTED` *(v1.6.0)* | `sign_pdf`, `timestamp_pdf` | The timestamp authority declined the request. | +| `REVOCATION_NOT_CONFIGURED` *(v1.6.0)* | `add_ltv` (`mode: 'online'`) | No `PDFNATIVE_MCP_REVOCATION` sources are set. Configure them, or use `mode: 'offline'` with caller-supplied material. | +| `NETWORK_HOST_NOT_ALLOWED` *(v1.6.0)* | `add_ltv` | A certificate-supplied OCSP/CRL URL points outside `PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS`. | +| `NETWORK_ERROR` *(v1.6.0)* | TSA/LTV tools | The TSA/OCSP/CRL request failed (timeout, refusal, size cap). | +| `LTV_NO_SIGNATURE` / `LTV_EMPTY` / `LTV_MATERIAL_INVALID` / `LTV_ERROR` *(v1.6.0)* | `add_ltv` | No signature to validate / no material gathered / supplied DER material does not parse / embedding failed. | +| `METADATA_ERROR` *(v1.6.0)* | `update_metadata` | The incremental `/Info` + XMP rewrite failed. | +| `PRINT_ERROR` *(v1.6.0)* | document tools with `print` | The requested page boxes are inconsistent (e.g. do not fit `pageSize`). | +| `CHART_ERROR` *(v1.6.0)* | `add_chart`, `chart` blocks | An engine cross-field chart rule failed (the message carries the remedy). | +| `PLACEHOLDER_AMBIGUOUS` *(v1.6.0)* | `sign_pdf` | Several unsigned placeholders exist and no `fieldName` was given. | +| `SIGNATURE_FIELD_NOT_FOUND` *(v1.6.0)* | `sign_pdf` | The named `fieldName` does not exist. | +| `CMS_PARSE_FAILED` *(v1.6.0)* | `verify_pdf` | A CMS structure is shorter or more malformed than the parser expects. | + +> **Protocol errors** *(v1.6.0)*: calling an unknown tool or prompt name is now a JSON-RPC `-32602` error (`[UNKNOWN_TOOL]` / `[UNKNOWN_PROMPT]`), not an `isError` result. Likewise, an unknown or misspelt input key — top-level or nested — fails with `VALIDATION_ERROR` ("Unrecognized key") instead of being silently stripped. --- @@ -562,8 +697,8 @@ When set on `add_international_text`, the `latin` font auto-registers so non-Win ## Output modes -Every document-producing tool accepts an `outputMode` field. The read-only -tools (`inspect_pdf`, `validate_pdf`, `verify_pdf`, `extract_text`, +Every document-producing tool accepts an `outputMode` field. The seven read-only +tools (`inspect_pdf`, `inspect_layout`, `validate_pdf`, `verify_pdf`, `extract_text`, `extract_attachments`, `read_form_fields`) return JSON only and have no `outputMode`: @@ -619,12 +754,13 @@ This workflow uses two tools in sequence: `pdfnative-mcp` is designed to run safely inside your AI client: -- **No network access** — the server does not open outbound connections. +- **No network access by default** — the server opens no outbound connection unless the **operator** configures TSA/OCSP/CRL endpoints for the PAdES tools (see the network charter under *Environment variables*). URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard. - **Sandboxed file writes** — `file` output mode is gated by `PDFNATIVE_MCP_OUTPUT_DIR`. When unset, file writes are rejected with a `SecurityError`. - **Path traversal protection** — absolute paths, `..` sequences, NUL bytes, and non-`.pdf` extensions are all rejected. - **Output size cap** — PDF output is capped at **50 MiB** per call. `split_pdf` additionally enforces **50 MiB per part** and a **200 MiB aggregate** ceiling across its parts, returning `OUTPUT_TOO_LARGE` when exceeded. -- **HTTP transport hardening** — when bound to `PDFNATIVE_MCP_PORT`, the Streamable HTTP transport is loopback-only and rejects foreign `Host` / `Origin` headers with **403** (DNS-rebinding protection). -- **Input validation** — every tool validates inputs against strict JSON Schemas and Zod runtime checks at the boundary. +- **HTTP transport hardening** — when bound to `PDFNATIVE_MCP_PORT`, the HTTP transport is loopback-only and rejects foreign `Host` / `Origin` headers with **403** (DNS-rebinding protection). An opt-in bearer token (`PDFNATIVE_MCP_HTTP_TOKEN`) adds authentication _(v1.6.0)_ — without it, the loopback endpoint has none. +- **Input validation** — every tool validates inputs against strict JSON Schemas and Zod runtime checks at the boundary. Since v1.6.0 every schema is `.strict()` at every nesting level, so an unknown or misspelt key is a `VALIDATION_ERROR` instead of being silently stripped, and PEM armour where DER base64 is expected fails with the exact `openssl … -outform DER` remedy. +- **Decompression cap** — the engine's 100 MiB per-stream inflate cap is operator-tunable via `PDFNATIVE_MCP_MAX_INFLATE_BYTES` _(v1.6.0)_. See [SECURITY.md](https://github.com/Nizoka/pdfnative-mcp/blob/main/SECURITY.md) for responsible disclosure. @@ -651,7 +787,7 @@ Split the content across multiple tool calls or reduce image/barcode count. ## Release history -The current release is **v1.5.0** (24 tools, built on pdfnative 1.7.0 — see the header note). Per-release notes, oldest first: +The current release is **v1.6.0** (28 tools, built on pdfnative 1.7.0 — see the header note). Per-release notes, oldest first: ### What's new in v1.0.0 @@ -726,7 +862,7 @@ v1.4.0 upgrades the engine to **pdfnative 1.5.0**, brings the pdfnative **AI-gov ### What's new in v1.5.0 -v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools**, closing the read/modify loop on documents the assistant did not create. +v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. - **Five new tools:** - `add_chart` — bar, horizontal-bar, line, pie and donut charts drawn as **native PDF vector paths**. No rasterisation and no image round-trip, so the output stays sharp at any zoom and tags as `/Figure` with alt text. If you were previously asking an agent to render a chart to PNG and push it through `embed_image`, stop — this replaces that. @@ -740,6 +876,34 @@ v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **2 - **Tool annotations** (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are published in `tools/list`, letting a client reason about which calls are safe to retry. - The encryption tools are deliberately **excluded from the response cache**. +### What's new in v1.6.0 + +v1.6.0 aligns the server with the **MCP 2026-07-28** specification and grows the catalogue to **28 tools**, exposing the engine's full document model and completing the PAdES baseline ladder. No breaking changes: every v1.5.0 call keeps its inputs, error codes and response shape, and a superset gate against the published 1.5.0 catalogue proves it. + +- **Four new tools:** + - `add_ltv` — embed `/DSS` + `/VRI` long-term-validation material (PAdES **B-LT**), online through the operator-configured revocation provider or offline from caller-supplied material. + - `timestamp_pdf` — append a `/DocTimeStamp` through the operator TSA (PAdES **B-LTA**), with auto-suffixed field names for periodic re-timestamping. + - `update_metadata` — rewrite `/Info` (+ XMP) of an existing PDF as an incremental update. + - `inspect_layout` — a read-only pagination dry run: page count and block positions with no PDF produced. +- **Full engine coverage** — `generate_basic_pdf` composes all **13 block kinds** (`table`, `image`, `link`, `toc`, `barcode`, `svg`, `formField` join the six existing ones); `pageSize`, `margins`, running `headerTemplate` / `footerTemplate`, `compress` and `debug` on the nine document tools; build-time `encrypt` that **keeps the AcroForm**; image watermarks with `position`. +- **PAdES signing** — `sign_pdf` gains `profile: 'pades'`, `timestamp: true` (RFC 3161, B-T), RSA-SHA384/512, `certChainDerBase64`, `fieldName` / `allowMultiple`; `verify_pdf ltv: true` reports profile, timestamp, embedded revocation status and the achieved level; `prepare_signature_placeholder` gains `subFilter` / `reserveTimestamp` and freezes signer metadata into the `/Sig` dictionary (fixing a pre-1.7 engine bug where those values were silently dropped). +- **Network charter** — still **no outbound request by default**; the single permitted egress class is the operator-configured TSA / OCSP / CRL endpoints, never a URL from a tool argument, behind an SSRF guard. Seven new environment variables configure the HTTP bearer token, the inflate cap and the network providers. +- **Print production** — `print` page boxes, `bleed`, printer's `marks`, `/UserUnit`, `metadata` with `/Trapped`, custom `outputIntent`; boxes survive the page-tree tools and are reported by `inspect_pdf`. +- **Charts v2** — `stackedBar` / `stackedBarH` / `area` / `scatter`, per-series `xValues` with linear or time axes, secondary `axis2`, `axis.scale: 'log'`, `dataLabels`, `labelStride` / `labelRotation`. +- **Honest PDF/A** — `embedFonts: true` (embed Noto Sans Latin for a claim veraPDF accepts), `strict: true` (fail instead of emitting a non-conformant file), `includeDiagnostics: true`; two new diagnostics; an advisory veraPDF corpus runs locally and in CI. +- **MCP 2026-07-28 transport** — SDK v2 (`@modelcontextprotocol/server`), `server/discover`, `resultType`, cache hints, per-result `serverInfo`, deterministic `tools/list`; 2025-era clients keep working via the automatic legacy fallback. +- **Four new recipe prompts** — `pades_ladder`, `print_ready`, `reproducible_output`, `pdfa_valid` — taking the prompt catalogue to six. +- **Reproducible output** — `creationDate` on all nine document tools, `signingTime` on the placeholder, `modDate` on `update_metadata`. + +**Migrating from v1.5.0** — six behaviour changes, all on error paths or on inputs the published schema already declared invalid: + +1. **Stray keys fail.** An unknown or misspelt key, top-level or nested, is now `VALIDATION_ERROR` ("Unrecognized key") instead of being silently stripped — the schemas always declared `additionalProperties: false`. +2. **Page-index mistakes are `VALIDATION_ERROR`.** Out-of-range pages / ranges on `merge_pdfs` / `split_pdf` / `extract_pages` were `PDF_PARSE_FAILED`; the message now carries a 0-based hint. +3. **`validate_pdf` on unparsable bytes is an error, not a verdict.** v1.5.0 returned `{ valid: false }`; v1.6.0 returns `isError: true` `[PDF_PARSE_FAILED]` — a parse failure is not a PDF/UA verdict. +4. **Unknown tool or prompt names are protocol errors** — JSON-RPC `-32602` (`[UNKNOWN_TOOL]` / `[UNKNOWN_PROMPT]`), no `isError` result. +5. **`inspect_pdf.checks` holds only the keys you asked for** — read `checksPassed` or the requested key, never an absent one. +6. **`add_form` text areas change bytes.** `fieldType: 'textarea'` now produces a real multi-line field (`/Ff 4096`). + --- ## Further reading diff --git a/docs/guides/onboarding.md b/docs/guides/onboarding.md index 7c906ba..9332cd3 100644 --- a/docs/guides/onboarding.md +++ b/docs/guides/onboarding.md @@ -98,7 +98,7 @@ Then prompt your assistant: > *Create a PDF/A-2b invoice for ACME Inc, add a multilingual paragraph in Arabic, and sign it with my key.* -The assistant calls `generate_basic_pdf` (with `pdfA: "pdfa2b"`), then `add_international_text` (with `lang: ["ar", "emoji"]`), `add_table`, `sign_pdf`, and finally `inspect_pdf` — confirming the result. v1.4.0 ships **24 tools**, including the page-tree trio `merge_pdfs`, `split_pdf`, `extract_pages`, markup `annotate_pdf`, the network-free `draft_governance_issue`, plus `validate_pdf`, `verify_pdf`, `add_attachment`, `extract_attachments`, and `extract_text`. +The assistant calls `generate_basic_pdf` (with `pdfA: "pdfa2b"`), then `add_international_text` (with `lang: ["ar", "emoji"]`), `add_table`, `sign_pdf`, and finally `inspect_pdf` — confirming the result. v1.6.0 ships **28 tools**, including the complete PAdES ladder (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), the page-tree trio `merge_pdfs`, `split_pdf`, `extract_pages`, markup `annotate_pdf`, `update_metadata`, the read-only `inspect_layout` pagination preview, the network-free `draft_governance_issue`, plus `validate_pdf`, `verify_pdf`, `add_attachment`, `extract_attachments`, and `extract_text`. Next: [MCP guide →](mcp.html) · [MCP playground →](../playgrounds/mcp.html) diff --git a/docs/llms.txt b/docs/llms.txt index 7907dee..4713efd 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -48,7 +48,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). -- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.5.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 24 tools — native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. Built on pdfnative 1.7.0. +- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. - [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. @@ -61,5 +61,5 @@ ## Ecosystem - [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. -- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 24 AI tools (v1.5.0, built on pdfnative 1.7.0). +- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). diff --git a/llms.txt b/llms.txt index 7907dee..4713efd 100644 --- a/llms.txt +++ b/llms.txt @@ -48,7 +48,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). -- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.5.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 24 tools — native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. Built on pdfnative 1.7.0. +- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. - [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. @@ -61,5 +61,5 @@ ## Ecosystem - [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. -- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 24 AI tools (v1.5.0, built on pdfnative 1.7.0). +- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). From 76206c383b6ce5df4480a5cfec606d3093374aef Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:09:17 +0200 Subject: [PATCH 02/15] docs(site): 28-tool MCP surface on the landing page, playground, SVG and SEO The landing page #mcp section grows to 28 rows with the four v1.6.0 tools (add_ltv, timestamp_pdf, inspect_layout, update_metadata) and its JSON-LD node bumps softwareVersion to 1.6.0; the architecture SVG desc and MCP box now read "28 tools"; the guides/learn/playground index blurbs and the mcp.html guide shell follow. responsibility.html names the SDK v2 package (@modelcontextprotocol/server) that replaced @modelcontextprotocol/sdk. The MCP playground gains two runnable cards: update_metadata drives the real openPdf -> createModifier -> updateMetadata -> save incremental pipeline on a browser-built source PDF, and inspect_layout runs the real inspectDocumentLayout dry run and prints the pagination report in place (a new runJson card variant, since the tool produces no PDF by design). add_ltv and timestamp_pdf are documented as deliberately card-less: they need the operator-configured TSA / OCSP / CRL providers a browser page cannot supply. Coverage arithmetic is restated as eleven of 28 without a card. Stale card blurbs (five chart types, 24 lang codes, no listbox, pre-PAdES sign_pdf) are refreshed, and the intro no longer stops at v1.4.0. Cross-cutting guides: signatures.md documents the MCP PAdES ladder (sign_pdf -> add_ltv -> timestamp_pdf, verify_pdf ltv: true), pdfa.md adds the embedFonts / strict / includeDiagnostics honesty note with the known AcroForm /DR /Helv limitation, and ai-governance.md states the single permitted egress class mirrored from the pdfnative-mcp 1.6.0 charter. sitemap.xml lastmod moves to 2026-08-25 for the 13 touched pages, llms-full.txt is regenerated (never hand-edited), and the stale-token canary in tests/docs/verify-docs.test.ts follows the manifest canonical (28) so the perturbation test keeps testing a live value. verify:docs: 17 rules passed across 109 files; vitest docs suite: 18/18. --- docs/assets/architecture.svg | 6 +- docs/guides/ai-governance.md | 7 + docs/guides/index.html | 4 +- docs/guides/mcp.html | 6 +- docs/guides/pdfa.md | 11 ++ docs/guides/signatures.md | 6 +- docs/index.html | 38 ++--- docs/learn/08-next-steps.html | 2 +- docs/llms-full.txt | 258 ++++++++++++++++++++++++++++----- docs/playgrounds/index.html | 2 +- docs/playgrounds/mcp.html | 114 ++++++++++++--- docs/responsibility.html | 2 +- docs/sitemap.xml | 26 ++-- tests/docs/verify-docs.test.ts | 4 +- 14 files changed, 382 insertions(+), 104 deletions(-) diff --git a/docs/assets/architecture.svg b/docs/assets/architecture.svg index 62687cb..999eca6 100644 --- a/docs/assets/architecture.svg +++ b/docs/assets/architecture.svg @@ -1,6 +1,6 @@ pdfnative — module architecture - Layered dependency diagram for pdfnative 1.7.0. Top band: three ecosystem consumers install pdfnative from npm — pdfnative-cli (command-line interface, 17 commands: render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern), pdfnative-mcp (Model Context Protocol server exposing 24 tools to Claude, Cursor, Zed and any stdio MCP client), and pdfnative-react (on-device JSX-to-PDF renderer). Bottom band: the zero-dependency library. Strict unidirectional flow — types feeds core; fonts imports into core; shaping imports into fonts; worker imports into shaping. Core groups pdf-builder, pdf-document, pdf-encrypt, pdf-tags plus outline, page labels, annotations and layout inspection. fonts covers encoding, subsetting, CMap and the font-data tools sub-path. Shaping covers GSUB, GPOS, BiDi and 22 Unicode scripts. crypto is a standalone module (AES, RSA, ECDSA, SHA, CMS and a pluggable native signing provider). parser is standalone, imports the compression layer, and provides page-tree manipulation: read, decrypt, extract text, modify, annotate, merge, split and re-encrypt. A header badge notes the repository's AI-agent governance with a human-in-the-loop (HITL) contribution gate. A legend explains the four edge types: imports, consumes via npm, standalone, and imports compress. + Layered dependency diagram for pdfnative 1.7.0. Top band: three ecosystem consumers install pdfnative from npm — pdfnative-cli (command-line interface, 17 commands: render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern), pdfnative-mcp (Model Context Protocol server exposing 28 tools to Claude, Cursor, Zed and any MCP client), and pdfnative-react (on-device JSX-to-PDF renderer). Bottom band: the zero-dependency library. Strict unidirectional flow — types feeds core; fonts imports into core; shaping imports into fonts; worker imports into shaping. Core groups pdf-builder, pdf-document, pdf-encrypt, pdf-tags plus outline, page labels, annotations and layout inspection. fonts covers encoding, subsetting, CMap and the font-data tools sub-path. Shaping covers GSUB, GPOS, BiDi and 22 Unicode scripts. crypto is a standalone module (AES, RSA, ECDSA, SHA, CMS and a pluggable native signing provider). parser is standalone, imports the compression layer, and provides page-tree manipulation: read, decrypt, extract text, modify, annotate, merge, split and re-encrypt. A header badge notes the repository's AI-agent governance with a human-in-the-loop (HITL) contribution gate. A legend explains the four edge types: imports, consumes via npm, standalone, and imports compress. @@ -102,7 +102,7 @@ pdfnative-mcp - MCP server · 24 tools + MCP server · 28 tools Claude · Cursor · Zed · any client @@ -167,7 +167,7 @@ parser - read · decrypt · extract text · modify + read · decrypt · extract text · modify merge / split · re-encrypt · stream diff --git a/docs/guides/ai-governance.md b/docs/guides/ai-governance.md index 8f0d810..a26485e 100644 --- a/docs/guides/ai-governance.md +++ b/docs/guides/ai-governance.md @@ -142,6 +142,13 @@ without parsing prose. Key fields: The contract `applies_to` the whole ecosystem — `pdfnative`, `pdfnative-cli`, `pdfnative-mcp`, and `pdfnative-react`. +Since pdfnative-mcp 1.6.0, that package's mirror of the charter also states +the **single permitted egress class**: the server makes no outbound request by +default, and the only network calls it can ever perform go to the TSA / OCSP / +CRL endpoints the *operator* configures for the PAdES tools — never to a URL +taken from a tool argument, and never to GitHub. "No GitHub write path" and +"no telemetry" remain absolute. + ## What agents must never do - Add a runtime dependency. diff --git a/docs/guides/index.html b/docs/guides/index.html index 506ad17..6e6aedd 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -383,7 +383,7 @@

Documentation Guides

  • MCP Integration → - Use pdfnative from Claude Desktop, Cursor, Continue, and Zed via pdfnative-mcp — 24 tools (incl. page-tree merge_pdfs, split_pdf, extract_pages, markup annotate_pdf, the network-free draft_governance_issue, plus validate_pdf, verify_pdf, extract_attachments), pdfA flag, multi-script lang, and a signed-document workflow. + Use pdfnative from Claude Desktop, Cursor, Continue, and Zed via pdfnative-mcp — 28 tools (incl. the PAdES ladder sign_pdf / add_ltv / timestamp_pdf, update_metadata, the read-only inspect_layout preview, page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue), pdfA flag, multi-script lang, and a signed-document workflow.
  • CLI → @@ -430,7 +430,7 @@

    Interactive playgrounds

  • MCP tool explorer → - Browse the pdfnative-mcp toolset (24 tools), copy a ready-to-paste config snippet for Claude / Cursor / Continue / Zed, and generate the same PDFs an AI assistant would receive — all in your browser. + Browse the pdfnative-mcp toolset (28 tools), copy a ready-to-paste config snippet for Claude / Cursor / Continue / Zed, and generate the same PDFs an AI assistant would receive — all in your browser.
  • React renderer → diff --git a/docs/guides/mcp.html b/docs/guides/mcp.html index 394ec72..46d44b9 100644 --- a/docs/guides/mcp.html +++ b/docs/guides/mcp.html @@ -4,7 +4,7 @@ pdfnative-mcp — MCP Integration Guide - + @@ -15,13 +15,13 @@ - + - + diff --git a/docs/guides/pdfa.md b/docs/guides/pdfa.md index 87e8a40..342997e 100644 --- a/docs/guides/pdfa.md +++ b/docs/guides/pdfa.md @@ -101,6 +101,17 @@ Each `PdfDiagnostic` carries a machine-readable `code`, a `severity` The code list is a stable, additions-only union (`PdfDiagnosticCode`), so a sink written today keeps compiling as future codes are added. +> **On the MCP surface** _(pdfnative-mcp 1.6.0)_, the same honesty is exposed +> as three opt-in inputs on every document tool: `embedFonts: true` embeds +> Noto Sans Latin so a PDF/A claim on base-14 text is actually accepted by +> veraPDF, `strict: true` fails instead of producing a non-conformant file, +> and `includeDiagnostics: true` echoes the engine's diagnostics +> (`PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, +> `PDFA_DEVICE_CMYK_IMAGE`) in the tool result. Known engine limitation: +> `add_form` (or a `formField` block) under a PDF/A claim still fails +> veraPDF even with `embedFonts` — the AcroForm `/DR /Helv` font is an +> unembedded Type1 (ISO 19005-2 §6.2.11.4.1). + ## v1.1.0 status — fully validated v1.1.0 ships full PDF/A-1b / 2b / 2u / 3b conformance against the diff --git a/docs/guides/signatures.md b/docs/guides/signatures.md index 148dd03..4527d83 100644 --- a/docs/guides/signatures.md +++ b/docs/guides/signatures.md @@ -108,7 +108,9 @@ verify a signed PDF end to end (byte-range digest, CMS signature value, chain, trust, timestamps, revocation), use [`pdfnative-cli verify`](cli.html#pdfnative-verify) or the [`verify_pdf` MCP tool](mcp.html) — the CMS verification logic lives -there. +there. Since pdfnative-mcp 1.6.0, `verify_pdf` also validates +`/DocTimeStamp` fields as RFC 3161 tokens and reports the achieved +PAdES level (B-B → B-LTA) with `ltv: true`. ## Why a separate placeholder step? @@ -358,4 +360,4 @@ for the idempotency proof. - [PDF/A conformance →](pdfa.html) — how signatures interact with PDF/A-2b/3b. - [Architecture →](architecture.html) — where the crypto module sits in the dependency graph. - [CLI →](cli.html) — `pdfnative-cli sign` wraps this exact pipeline. -- [MCP integration →](mcp.html) — `pdfnative-mcp` exposes signing as an AI tool. +- [MCP integration →](mcp.html) — `pdfnative-mcp` exposes signing as an AI tool, and since v1.6.0 the complete PAdES ladder: `sign_pdf` (`profile: 'pades'`, RFC 3161 `timestamp`) → `add_ltv` (`/DSS` + `/VRI`, B-LT) → `timestamp_pdf` (`/DocTimeStamp`, B-LTA), verified with `verify_pdf ltv: true`. diff --git a/docs/index.html b/docs/index.html index f9255bc..a79ec21 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ pdfnative — Zero-dependency PDF generation for TypeScript - + @@ -103,9 +103,9 @@ "@id": "https://pdfnative.dev/#mcp", "inLanguage": "en", "name": "pdfnative-mcp", - "description": "Model Context Protocol server exposing pdfnative to Claude Desktop, Cursor, Continue, Zed, and any stdio MCP client. 24 tools (incl. native vector add_chart, fill_form / read_form_fields, encrypt_pdf / decrypt_pdf, page-tree merge/split/extract, markup annotate_pdf, and the network-free draft_governance_issue), per-tool apiVersion, PDF/A flag.", + "description": "Model Context Protocol server exposing pdfnative to Claude Desktop, Cursor, Continue, Zed, and any MCP client. 28 tools (incl. the complete PAdES ladder sign_pdf / add_ltv / timestamp_pdf, update_metadata, the read-only inspect_layout preview, native vector add_chart, fill_form / read_form_fields, encrypt_pdf / decrypt_pdf, page-tree merge/split/extract, markup annotate_pdf, and the network-free draft_governance_issue), MCP 2026-07-28 spec, per-tool apiVersion, PDF/A flag.", "url": "https://github.com/Nizoka/pdfnative-mcp", - "softwareVersion": "1.5.0", + "softwareVersion": "1.6.0", "applicationCategory": "DeveloperApplication", "operatingSystem": "Cross-platform", "isAccessibleForFree": true, @@ -282,7 +282,7 @@

    Production Ready

    AI Integration — MCP

    -

    Use pdfnative from Claude Desktop, Cursor, Continue, Zed, and any other stdio MCP client (Cline, Windsurf, Goose, Gemini CLI…) via pdfnative-mcp. 24 production tools incl. page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue, validate_pdf, verify_pdf, add_attachment / extract_attachments, extract_text, pdfA flag everywhere, and token-frugal read modes. Zero configuration beyond npx -y pdfnative-mcp.

    +

    Use pdfnative from Claude Desktop, Cursor, Continue, Zed, and any other MCP client (Cline, Windsurf, Goose, Gemini CLI…) via pdfnative-mcp. 28 production tools incl. the complete PAdES ladder sign_pdf / add_ltv / timestamp_pdf, update_metadata, the read-only inspect_layout preview, page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue, pdfA flag everywhere, and token-frugal read modes. Zero configuration beyond npx -y pdfnative-mcp.

    @@ -479,7 +479,7 @@

    CLI v1.3.0

    CLI guide →
    -

    MCP v1.5.0

    +

    MCP v1.6.0

    For Claude Desktop, Cursor, Continue, Zed, …

    {
       "mcpServers": {
    @@ -739,7 +739,7 @@ 

    Architecture

    Strict unidirectional dependency flow. No circular imports. Each module is independently testable.

    @@ -763,25 +763,28 @@

    AI Governance & Human-in-the-Loop

    Use pdfnative from Any AI Client

    -

    pdfnative-mcp is a Model Context Protocol server that exposes the full pdfnative library to Claude Desktop, Cursor, Continue, Zed, and any other MCP-compatible AI client. One npx command, no code required. Now with page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue, validate_pdf, verify_pdf, add_attachment / extract_attachments, extract_text, a pdfA flag on every document tool, multi-script lang, and token-frugal read modes.

    +

    pdfnative-mcp is a Model Context Protocol server that exposes the full pdfnative library to Claude Desktop, Cursor, Continue, Zed, and any other MCP-compatible AI client. One npx command, no code required. Now with the complete PAdES ladder (sign_pdf with RFC 3161 timestamps, add_ltv, timestamp_pdf), update_metadata, the read-only inspect_layout pagination preview, all 13 block kinds in generate_basic_pdf, print production, charts v2, page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue, a pdfA flag on every document tool, and token-frugal read modes — on the MCP 2026-07-28 spec with automatic legacy fallback.

    -

    24 Production Tools

    +

    28 Production Tools

  • - + - - - - - - + + + + + + + + + - + @@ -790,11 +793,12 @@

    24 Production Tools

    - + +
    ToolPurpose
    generate_basic_pdfMulti-page documents (headings, paragraphs, lists). Optional pdfA flag.
    generate_basic_pdfMulti-page documents — all 13 block kinds since v1.6.0 (incl. tables, images, links, TOC, barcodes, SVG, form fields, charts). Optional pdfA, layout options, build-time encrypt, print production.
    add_tableTabular reports — now with optional autoFitColumns and clipCells (pdfnative v1.2 TableBlock).
    add_barcodeQR Code, Code 128, EAN-13, Data Matrix, PDF417
    add_international_text22 scripts with BiDi & OpenType shaping. lang now accepts string, string[], or comma-separated — e.g. ["ar", "emoji"].
    add_formInteractive AcroForm fields
    embed_imageJPEG / PNG image embedding
    prepare_signature_placeholderPDF with /Sig field ready to sign
    sign_pdfCMS/PKCS#7 signatures (RSA & ECDSA)
    inspect_pdfRead-only inspection — version, page count, encryption, PDF/A claim, signatures, info dict; optional CI-style check: ('pdfa'|'signed'|'encrypted'|'placeholder'|'attachments')[].
    add_international_text25 lang font codes (22 Unicode scripts + latin + emoji + explicit math) with BiDi & OpenType shaping. lang accepts string, string[], or comma-separated — e.g. ["ar", "emoji"].
    add_formInteractive AcroForm fields — text, textarea, checkbox, radio, dropdown, plus listbox and placeholder (v1.6.0)
    embed_imageJPEG / PNG image embedding, with align / alt (v1.6.0)
    prepare_signature_placeholderPDF with /Sig field ready to sign; subFilter / reserveTimestamp (v1.6.0)
    sign_pdfPAdES CMS signatures (RSA-SHA256/384/512 & ECDSA); profile: 'pades', RFC 3161 timestamp, cert chains, multiple signatures (v1.6.0)
    add_ltvEmbed /DSS + /VRI long-term-validation material — PAdES B-LT (v1.6.0).
    timestamp_pdfAppend a /DocTimeStamp through the operator TSA — PAdES B-LTA (v1.6.0).
    inspect_pdfRead-only inspection — version, page count, encryption, PDF/A claim, signatures, info dict; signature / annotation inventories, page boxes, dss (v1.6.0); optional CI-style check assertions.
    inspect_layoutRead-only pagination dry run — page count and block geometry, no PDF produced (v1.6.0).
    validate_pdfRead-only PDF/UA structural validation (valid, errors, warnings).
    verify_pdfReal CMS/PKCS#7 signature verification — RSA & ECDSA, digest, certificate chain (v1.0.0).
    verify_pdfReal CMS/PKCS#7 signature verification — RSA & ECDSA, digest, certificate chain; /DocTimeStamp tokens and the ltv: true PAdES-level report (v1.6.0).
    add_attachmentEmbed files (Factur-X / ZUGFeRD e-invoice XML) into PDF/A-3b output (v1.0.0).
    extract_attachmentsExtract embedded files from existing PDFs (metadata-only mode available, v1.2.0).
    extract_textExtract text content from an existing PDF via the native parser (v1.0.0).
    extract_pagesPull an arbitrary, order-preserving page subset (max 5000) into a new PDF (v1.3.0).
    annotate_pdfOverlay markup annotations (highlight / underline / note / shape) on an existing PDF via incremental update. A visual review layer, not a redaction (v1.4.0).
    draft_governance_issueAssemble a governance-compliant GitHub-issue draft locally — network-free by construction; never submits (v1.4.0).
    add_chartRender bar / horizontal-bar / line / pie / donut charts as native PDF vector paths — no rasterisation, no image round-trip (v1.5.0).
    add_chartRender charts as native PDF vector paths — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log & time scales; no rasterisation (v1.5.0).
    read_form_fieldsList an existing AcroForm's fields with their types, current values and options — the read half of the fill round-trip (v1.5.0).
    fill_formFill AcroForm field values and optionally flatten them into static page content (v1.5.0).
    encrypt_pdfRe-secure an existing PDF with AES-128 or AES-256, setting owner/user passwords and permissions (v1.5.0).
    decrypt_pdfRemove encryption from a password-protected PDF, in-server — RC4, AES-128 and AES-256 sources (v1.5.0).
    update_metadataRewrite /Info (+ XMP) of an existing PDF via incremental update (v1.6.0).
    diff --git a/docs/learn/08-next-steps.html b/docs/learn/08-next-steps.html index 54a8e97..9b3d2d7 100644 --- a/docs/learn/08-next-steps.html +++ b/docs/learn/08-next-steps.html @@ -124,7 +124,7 @@

    Pick the surface that fits your work

    • The library — what you have been using. Full control, any runtime.
    • pdfnative-cli — 17 commands for shell scripts, CI and Makefiles. Render, sign, encrypt, extract text, fill forms, merge and split, all from JSON on stdin.
    • -
    • pdfnative-mcp — 24 tools that let an AI assistant (Claude, Cursor, Zed, any stdio MCP client) produce and inspect PDFs on your behalf, with no code.
    • +
    • pdfnative-mcp — 28 tools that let an AI assistant (Claude, Cursor, Zed, any MCP client) produce, sign and inspect PDFs on your behalf, with no code.
    • pdfnative-react — the same engine driven by JSX components, if your app is already React.
    diff --git a/docs/llms-full.txt b/docs/llms-full.txt index a1f88cb..31ca41b 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -48,7 +48,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). -- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.5.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 24 tools — native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. Built on pdfnative 1.7.0. +- [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. - [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. @@ -61,7 +61,7 @@ ## Ecosystem - [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. -- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 24 AI tools (v1.5.0, built on pdfnative 1.7.0). +- [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). --- @@ -379,7 +379,7 @@ under the [AI-governance / human-in-the-loop contract](ai-governance.md). - [AI governance & human-in-the-loop](ai-governance.md) — the contract that keeps repository changes human-gated. -- [MCP integration](mcp.md) — the 24 MCP tools, including `embed_image` and +- [MCP integration](mcp.md) — the 28 MCP tools, including `embed_image` and `draft_governance_issue`. - [CLI guide](cli.md) — driving pdfnative from the shell. - [Font validation](font-validation.md) — `validateFontData()` for sanity-checking @@ -531,6 +531,13 @@ without parsing prose. Key fields: The contract `applies_to` the whole ecosystem — `pdfnative`, `pdfnative-cli`, `pdfnative-mcp`, and `pdfnative-react`. +Since pdfnative-mcp 1.6.0, that package's mirror of the charter also states +the **single permitted egress class**: the server makes no outbound request by +default, and the only network calls it can ever perform go to the TSA / OCSP / +CRL endpoints the *operator* configures for the PAdES tools — never to a URL +taken from a tool argument, and never to GitHub. "No GitHub write path" and +"no telemetry" remain absolute. + ## What agents must never do - Add a runtime dependency. @@ -778,7 +785,7 @@ The architecture diagram above shows the **internal library modules**. External [shell / CI / Docker] │ pdfnative-cli (npm) — 17 commands │ └─────────────────────────────────────────────┘ ┌─────────────────────────────────────────────┐ - [Claude / Cursor / …] │ pdfnative-mcp (npm) — 24 AI tools │ + [Claude / Cursor / …] │ pdfnative-mcp (npm) — 28 AI tools │ └───────────────────────────────────────────┘ ┌─────────────────────────────────────────────┐ [React / Next.js app] │ pdfnative-react (npm) — declarative JSX │ @@ -809,13 +816,13 @@ Like `pdfnative-mcp` and `pdfnative-react`, the CLI lives in a separate reposito ### pdfnative-mcp -[pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is a **Model Context Protocol server** that wraps the pdfnative public API and exposes it as 24 structured tools to any MCP-compatible AI client (Claude Desktop, Cursor, Continue, Zed, ChatGPT, …). It commits to API stability via a per-tool `_meta.apiVersion` field. +[pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is a **Model Context Protocol server** that wraps the pdfnative public API and exposes it as 28 structured tools to any MCP-compatible AI client (Claude Desktop, Cursor, Continue, Zed, ChatGPT, …). It commits to API stability via a per-tool `_meta.apiVersion` field. ``` [Claude Desktop / Cursor / Continue / Zed] │ MCP stdio protocol ┌──────────────────────────┐ - │ pdfnative-mcp (npm) │ ← MCP server, 24 tools + │ pdfnative-mcp (npm) │ ← MCP server, 28 tools └──────────────────────────┘ │ import { buildDocumentPDFBytes, … } from 'pdfnative' ┌──────────────────────────┐ @@ -3357,7 +3364,7 @@ The repository ships a deterministic offline mock PKI (`scripts/helpers/mock-pki # pdfnative-mcp — AI Client Integration Guide -> **Tracks the latest published `pdfnative-mcp`** (v1.5.0, built on pdfnative 1.7.0). Full release notes: [pdfnative-mcp releases](https://github.com/Nizoka/pdfnative-mcp/releases). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). +> **Tracks the latest published `pdfnative-mcp`** (v1.6.0, built on pdfnative 1.7.0). Full release notes: [pdfnative-mcp releases](https://github.com/Nizoka/pdfnative-mcp/releases). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp) is an **MCP server** that exposes the full pdfnative library to any AI client supporting the [Model Context Protocol](https://modelcontextprotocol.io) — Claude Desktop, Cursor, Continue, Zed, ChatGPT, and more. @@ -3477,26 +3484,38 @@ In your Zed `settings.json`: | `PDFNATIVE_MCP_OUTPUT_DIR` | Absolute path to the sandbox directory. **Required to enable `outputMode: "file"`**. When unset, only `base64` output is available. (The misspelt `PDFNATIVE_MPC_OUTPUT_DIR` still works as a deprecated alias.) | | `PDFNATIVE_MCP_CACHE_DIR` | Absolute path to enable the persistent SHA-256-keyed result cache (1 h TTL, 256 MiB LRU). When unset, the cache is disabled. | | `PDFNATIVE_MCP_PORT` | When set to a valid port (1–65535), starts an HTTP server on `http://127.0.0.1:/mcp` instead of stdio. | +| `PDFNATIVE_MCP_HTTP_TOKEN` _(v1.6.0)_ | Opt-in bearer token for the HTTP transport (≥ 16 characters, no whitespace — a weaker value aborts startup). When set, `/mcp` requires `Authorization: Bearer `; otherwise requests get `401` + `WWW-Authenticate`. Without it the loopback endpoint has **no authentication**. Never logged. | +| `PDFNATIVE_MCP_MAX_INFLATE_BYTES` _(v1.6.0)_ | Operator override of the engine's 100 MiB per-stream decompression cap (integer ≥ 1024, read once at startup; an invalid value refuses to start with one clean `fatal:` line). | +| `PDFNATIVE_MCP_TSA_URL` _(v1.6.0)_ | RFC 3161 timestamp authority endpoint. Required by `sign_pdf timestamp: true` and `timestamp_pdf` (`TSA_NOT_CONFIGURED` otherwise). | +| `PDFNATIVE_MCP_TSA_AUTH` _(v1.6.0)_ | Optional `Authorization` header value sent to the TSA. Treated as a secret; never logged. | +| `PDFNATIVE_MCP_REVOCATION` _(v1.6.0)_ | Revocation sources for `add_ltv mode: 'online'` — `ocsp`, `crl`, or `ocsp,crl` (`REVOCATION_NOT_CONFIGURED` otherwise). | +| `PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS` _(v1.6.0)_ | Mandatory allow-list of hosts the OCSP/CRL fetcher may contact. A certificate-supplied URL outside the list fails with `NETWORK_HOST_NOT_ALLOWED`. | +| `PDFNATIVE_MCP_NETWORK_TIMEOUT_MS` _(v1.6.0)_ | Network timeout for TSA/OCSP/CRL requests (1000–120000 ms, default 10000). | + +> **Network charter** _(v1.6.0)_. The server still makes **no outbound request by default**. The only egress it can ever perform goes to the TSA / OCSP / CRL endpoints the **operator** configures via the variables above — URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard (allow-list, http(s) only, no credentials, no redirects, internal address literals rejected, size caps, timeouts). --- ## Tool reference -`pdfnative-mcp` exposes **24 tools**: +`pdfnative-mcp` exposes **28 tools**: | Tool | Purpose | |---|---| -| `generate_basic_pdf` | Multi-page A4 documents from structured blocks (headings, paragraphs, lists, spacers, charts, page breaks). Accepts optional `pdfA`. | +| `generate_basic_pdf` | Multi-page documents from structured blocks — all **13 block kinds** since v1.6.0 (headings, paragraphs, lists, tables, images, links, TOC, barcodes, SVG, form fields, charts, page breaks, spacers). Accepts optional `pdfA`, layout options, build-time `encrypt`, and print-production fields. | | `add_table` | Tabular PDF reports from column headers and data rows. Optional `autoFitColumns` and `clipCells`. Accepts `pdfA`. | | `add_barcode` | QR Code, Code 128, EAN-13, Data Matrix, PDF417 — embedded in a single-page PDF. Accepts `pdfA`. | -| `add_international_text` | 24 `lang` font codes — the 22 writing systems plus `latin` and `emoji` — with BiDi & OpenType shaping, plus the explicit `math` script (Noto Sans Math, on-demand). `lang` accepts `string`, `string[]`, or comma-separated. | -| `add_form` | Interactive AcroForm PDFs with text fields, checkboxes, radio buttons, and dropdowns. Accepts `pdfA`. | -| `embed_image` | Embed a JPEG or PNG image (base64-encoded) into a titled PDF document. Accepts `pdfA`. | -| `prepare_signature_placeholder` | Create a PDF with a `/Sig` AcroForm placeholder ready to be signed. Accepts `pdfA`. | -| `sign_pdf` | PAdES-style CMS digital signatures (RSA-SHA256 / ECDSA-SHA256 P-256). | -| `inspect_pdf` | Read-only inspection. Returns `version`, `pageCount`, `encryption`, `pdfA`, `signatureCount`, `info`, optional `perPage`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed`. | +| `add_international_text` | 25 `lang` font codes — the 22 writing systems plus `latin`, `emoji` and the explicit `math` script (Noto Sans Math, on-demand) — with BiDi & OpenType shaping. `lang` accepts `string`, `string[]`, or comma-separated. | +| `add_form` | Interactive AcroForm PDFs with text fields, text areas, checkboxes, radio buttons, dropdowns, and list boxes _(v1.6.0)_. Accepts `pdfA`. | +| `embed_image` | Embed a JPEG or PNG image (base64-encoded) into a titled PDF document, with `align` and `alt` _(v1.6.0)_. Accepts `pdfA`. | +| `prepare_signature_placeholder` | Create a PDF with a `/Sig` AcroForm placeholder ready to be signed; `subFilter`, `reserveTimestamp` and frozen signer metadata _(v1.6.0)_. Accepts `pdfA`. | +| `sign_pdf` | PAdES CMS digital signatures — RSA-SHA256/384/512 and ECDSA-SHA256 P-256, `profile: 'pades'`, RFC 3161 `timestamp` (B-T), certificate chains, named fields and multiple signatures _(v1.6.0)_. | +| `add_ltv` _(v1.6.0)_ | Embed long-term-validation material (`/DSS` + `/VRI`) into a signed PDF — PAdES **B-LT** — online through the operator-configured revocation provider or offline from caller-supplied material. | +| `timestamp_pdf` _(v1.6.0)_ | Append a `/DocTimeStamp` (ETSI.RFC3161) through the operator TSA — PAdES **B-LTA** — with auto-suffixed field names for periodic re-timestamping. | +| `inspect_pdf` | Read-only inspection. Returns `version`, `pageCount`, `encryption`, `pdfA`, `signatureCount`, `info`, optional `perPage`, optional `pageLabels[]`, and — _(v1.6.0)_ — an optional `signatures[]` inventory, `annotations[]`, page boxes + `userUnit`, `dss` / `docTimestampCount` / `trapped`, plus `checks` + `checksPassed`. | +| `inspect_layout` _(v1.6.0)_ | Read-only pagination **dry run** — page count, page geometry and each block's position for a prospective document, with no PDF produced. | | `validate_pdf` | Read-only PDF/UA structural validation (`valid`, `errors`, `warnings`). | -| `verify_pdf` | Real CMS/PKCS#7 signature verification — RSA & ECDSA, message digest, certificate chain. (RFC 3161 timestamp validation is a `pdfnative-cli verify` feature, not part of this server.) | +| `verify_pdf` | Real CMS/PKCS#7 signature verification — RSA & ECDSA, message digest, certificate chain. Since v1.6.0, `/DocTimeStamp` entries are verified as RFC 3161 tokens and `ltv: true` reports the achieved PAdES level (B-B → B-LTA). | | `add_attachment` | Embed files (e.g. Factur-X / ZUGFeRD e-invoice XML) into PDF/A-3b output. | | `extract_attachments` | Extract embedded files from an existing PDF (optionally metadata-only). | | `extract_text` | Extract text content from an existing PDF via the native parser. | @@ -3505,13 +3524,14 @@ In your Zed `settings.json`: | `extract_pages` | Pull an arbitrary, order-preserving page subset (max 5000) into a single PDF. | | `annotate_pdf` | Overlay markup annotations (text / highlight / underline / strikeout / squiggly / square / circle / line / freetext) on an existing PDF via incremental update. A visual review layer, **not** a redaction. | | `draft_governance_issue` | Assemble a governance-compliant GitHub-issue draft plus a structured `compliance` report **locally** — network-free by construction; never submits. | -| `add_chart` _(v1.5.0)_ | Render a bar, horizontal-bar, line, pie or donut chart as **native PDF vector paths** — no rasterisation and no image round-trip. Tagged as `/Figure` with alt text when `pdfA` is set. | +| `add_chart` _(v1.5.0)_ | Render a chart as **native PDF vector paths** — nine types since v1.6.0 (`bar`, `barH`, `stackedBar`, `stackedBarH`, `line`, `area`, `scatter`, `pie`, `donut`), with a secondary axis, log and time scales, and data labels. Tagged as `/Figure` with alt text when `pdfA` is set. | | `read_form_fields` _(v1.5.0)_ | List an existing AcroForm's fields with their types, current values and available options — the read half of the fill round-trip. Accepts `password`. | | `fill_form` _(v1.5.0)_ | Fill AcroForm field values and optionally `flatten` them into static page content. Works on encrypted PDFs via incremental update. | | `encrypt_pdf` _(v1.5.0)_ | Re-secure an existing PDF with AES-128 or AES-256 — owner/user passwords and an explicit permission set. | | `decrypt_pdf` _(v1.5.0)_ | Remove encryption from a password-protected PDF **in-server** — RC4, AES-128 and AES-256 sources. | +| `update_metadata` _(v1.6.0)_ | Rewrite an existing PDF's `/Info` dictionary (+ XMP) — title, author, subject, keywords, pinned `modDate` — as a non-destructive incremental update. | -Every tool now publishes an `outputSchema` advertised in `tools/list` per the [MCP 2025-06-18 spec](https://modelcontextprotocol.io/specification/2025-06-18), enabling clients to statically validate responses. +Every tool publishes an `outputSchema` advertised in `tools/list`. Since v1.6.0 the server speaks the [MCP 2026-07-28 spec](https://modelcontextprotocol.io/specification/2026-07-28) (stateless envelope, `server/discover`, `resultType`, cache hints) on SDK v2, while 2025-era clients (2025-11-25 / 2025-06-18 / 2025-03-26) keep working through the automatic legacy fallback. --- @@ -3534,7 +3554,17 @@ Produces a multi-page document from a list of content blocks. } ``` -**Block types supported:** `heading` (levels 1–3), `paragraph`, `list` (`bullet` / `numbered`, nested items to depth 6), `pageBreak`, `spacer` (`height` in points), and `chart` (same body as `add_chart`). +**Block types supported:** all **13 engine block kinds** since v1.6.0 — `heading` (levels 1–3), `paragraph`, `list` (`bullet` / `numbered`, nested items to depth 6), `table`, `image`, `link` (`http:` / `https:` / `mailto:` only), `toc` (a contents page with internal `/GoTo` links, pairs with `outline: 'auto'`), `barcode`, `svg` (a safe subset: paths, shapes and text — no XML parser, no external reference ever fetched), `formField`, `chart` (same body as `add_chart`), `pageBreak`, and `spacer` (`height` in points). The `table` / `image` / `barcode` / `formField` blocks share their body with the dedicated tools, so a standalone artefact and an inline block validate and render identically. + +**Layout options** _(v1.6.0)_, available on all nine document tools: `pageSize` (`A4` default, `Letter`, `Legal`, `A3`, `Tabloid`), `margins` (all four sides, 0–200 pt), running `headerTemplate` / `footerTemplate` (`left` / `center` / `right` zones with `{page}` `{pages}` `{title}` `{date}` placeholders — a `footerTemplate` replaces the default footer, so `footerText` is then ignored), `compress`, and `debug`. + +**Build-time `encrypt`** _(v1.6.0)_, on `generate_basic_pdf`, `add_table`, `add_form`, `add_international_text`, `embed_image`, `add_barcode` and `add_chart`: AES-128 (default) or AES-256 with owner/user passwords and permissions — and unlike `encrypt_pdf`, it **keeps the AcroForm**, making encrypted fillable forms reachable. Exclusive with `pdfA` (ISO 19005-1 §6.3.2); never cached. + +**Print production** _(v1.6.0)_, on the nine document tools: `print` (TrimBox / BleedBox / ArtBox / CropBox, a `bleed` shorthand, crop + registration `marks`, `/UserUnit`), `metadata` (`/Author`, `/Subject`, `/Keywords`, `/Trapped` with XMP parity), and `outputIntent` (custom RGB ICC profile). `viewerPreferences` gains `duplex`, `pickTrayByPDFSize`, `printPageRange` (1-based), `numCopies`. Boxes survive `merge_pdfs` / `split_pdf` / `extract_pages` and are reported by `inspect_pdf`. + +**Honest PDF/A** _(v1.6.0)_: text rendered through the viewer's base-14 Helvetica is not embedded, so a PDF/A claim on such a file is rejected by veraPDF. `embedFonts: true` embeds Noto Sans Latin for a valid claim; `strict: true` fails instead of producing a non-conformant file; `includeDiagnostics: true` echoes the engine's diagnostics (`PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, `PDFA_DEVICE_CMYK_IMAGE`). + +**Reproducible output** _(v1.6.0)_: `creationDate` (ISO-8601) on all nine document tools pins `/Info /CreationDate`, the XMP dates and therefore the trailer `/ID` — byte-identical output on the same host time zone. --- @@ -3618,7 +3648,7 @@ Creates an interactive AcroForm PDF. } ``` -**Field types:** `text`, `textarea`, `checkbox`, `radio`, `dropdown`. +**Field types:** `text`, `textarea`, `checkbox`, `radio`, `dropdown`, and `listbox` _(v1.6.0)_. Text fields accept an optional `placeholder` _(v1.6.0)_. Since v1.6.0, `textarea` maps to the engine's true multi-line field (`/Ff 4096`) — in v1.5.0 it rendered as a single-line widget. --- @@ -3637,6 +3667,8 @@ Creates an interactive AcroForm PDF. > **Note:** Alpha-channel PNGs (color type 6) are not supported. Pre-process such images to remove the alpha channel before embedding. +Since v1.6.0 the tool also accepts `align` (horizontal placement) and `alt` (accessibility text written as `/Figure /Alt` in tagged output). + --- ### `prepare_signature_placeholder` @@ -3656,6 +3688,8 @@ Creates a PDF pre-wired with an AcroForm `/Sig` field, ready to be signed by `si } ``` +Since v1.6.0 the placeholder also takes `subFilter`, `reserveTimestamp` (extra `/Contents` room for a later RFC 3161 token), `placeholderBytes`, and `signingTime` — and the signer metadata (`signerName`, `reason`, `location`, `contactInfo`, `signingTime`) is **baked into the `/Sig` dictionary at placeholder time**, fixing a pre-1.7 engine bug where those values never reached the signed document. + --- ### `sign_pdf` @@ -3677,6 +3711,17 @@ Signs any PDF. When the input already contains a `/Sig` placeholder (e.g. produc For ECDSA P-256: use `"algorithm": "ecdsa-sha256"` and supply either `ecPrivateScalarHex` (64 hex chars, raw scalar `d`) or `ecPrivateKeyDerBase64` (SEC1 / PKCS#8 DER, base64 — signed through the constant-time `node:crypto` path) instead of `rsaKeyPkcs1DerBase64`. The two EC inputs are mutually exclusive. +**PAdES and timestamps** _(v1.6.0)_: + +- `algorithm` also accepts `rsa-sha384` and `rsa-sha512`. +- `profile: 'pades'` produces an ETSI.CAdES.detached signature (PAdES **B-B**). +- `timestamp: true` requests an RFC 3161 timestamp from the operator-configured TSA (`PDFNATIVE_MCP_TSA_URL`) and embeds it in the CMS — PAdES **B-T**. Fails with `TSA_NOT_CONFIGURED` when no TSA is set, `TSA_REJECTED` when the authority declines. +- `certChainDerBase64` embeds intermediate certificates alongside the signer certificate. +- `fieldName` targets a specific `/Sig` field (`SIGNATURE_FIELD_NOT_FOUND` when absent, `PLACEHOLDER_AMBIGUOUS` when several placeholders exist and none is named); `allowMultiple: true` adds a signature next to existing ones instead of rejecting. +- The default placeholder reservation is now `max(16384, estimated CMS size)` — plus 8192 bytes when `timestamp: true` — instead of a flat 16384 bytes. To pin it exactly, build the placeholder with `prepare_signature_placeholder` and its `placeholderBytes` input. + +To climb the rest of the PAdES ladder, follow with [`add_ltv`](#add_ltv-v160) (B-LT) and [`timestamp_pdf`](#timestamp_pdf-v160) (B-LTA), then check the achieved level with `verify_pdf ltv: true`. The server's `pades_ladder` prompt walks through the full recipe. + --- ### `inspect_pdf` @@ -3694,12 +3739,14 @@ Read-only PDF inspection over `openPdf()`. Never modifies the input. **Inputs:** - `pdfBase64` — base64 PDF bytes (required). - `password` — password for an encrypted PDF *(v1.5.0)*. -- `pages` — when `true`, includes per-page `index`, `width`, `height`. -- `check` — array of CI assertions. Allowed values: `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`. The response includes `checks` (per-assertion result) and `checksPassed` (boolean AND). +- `pages` — when `true`, includes per-page `index`, `width`, `height` — and, since v1.6.0, the declared page boxes and `userUnit`. +- `signatures` *(v1.6.0)* — when `true`, a per-signature inventory: `subFilter`, `isDocTimestamp`, `isPlaceholder`, `byteRange`, `vriKey`. +- `annotations` *(v1.6.0)* — when `true`, an `annotations[]` list (0-based `page`, `subtype`, `rect`, and when present `contents`, `title`, `color`, `quadPoints`, link `url`) plus `annotationCount`. +- `check` — array of CI assertions. Allowed values: `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`, and — *(v1.6.0)* — `dss`, `docTimestamp`, `trapped`, `annotations`. The response includes `checks` (per-assertion result) and `checksPassed` (boolean AND). Since v1.6.0, `checks` contains **only the requested keys**. - `verbosity` — `'full'` (default) or `'summary'` (token-frugal scalar subset). - `fields` — optional dot-path projection of the result. -**Outputs:** `version`, `pageCount`, `encryption` (`'none'` / `'aes-128'` / `'aes-256'` / `'rc4'` / `'unknown'`), optional `encryptionInfo` (`{ algorithm, revision, authenticatedAs }`, present when the document is encrypted and opened successfully), `pdfA` (`null` or the detected claim string), `signatureCount`, `hasSignaturePlaceholder`, `attachments[]` (embedded-file summaries), `info` (decoded `/Info` entries), optional `perPage[]`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed`. +**Outputs:** `version`, `pageCount`, `encryption` (`'none'` / `'aes-128'` / `'aes-256'` / `'rc4'` / `'unknown'`), optional `encryptionInfo` (`{ algorithm, revision, authenticatedAs }`, present when the document is encrypted and opened successfully), `pdfA` (`null` or the detected claim string), `signatureCount`, `hasSignaturePlaceholder`, `attachments[]` (embedded-file summaries), `info` (decoded `/Info` entries), optional `perPage[]`, optional `pageLabels[]` (when `/PageLabels` is declared), optional `checks` + `checksPassed` — plus, presence-gated since v1.6.0, `dss`, `docTimestampCount` and `trapped`. Useful in CI as a final assertion step before publishing a PDF artifact: @@ -3801,7 +3848,7 @@ Assembles a governance-compliant GitHub-issue draft plus a structured `complianc ### `add_chart` _(v1.5.0)_ -Renders a bar, horizontal-bar, line, pie or donut chart as **native PDF vector paths** — no rasterisation, no image round-trip. Multi-series bar/line, legends, gridlines and negative values are supported; the chart is tagged `/Figure` with `/Alt` (auto-generated when `altText` is omitted). +Renders a chart as **native PDF vector paths** — no rasterisation, no image round-trip. Multi-series bar/line, legends, gridlines and negative values are supported; the chart is tagged `/Figure` with `/Alt` (auto-generated when `altText` is omitted). ```jsonc { @@ -3814,7 +3861,9 @@ Renders a bar, horizontal-bar, line, pie or donut chart as **native PDF vector p } ``` -**Inputs:** `chartType` (`bar` / `barH` / `line` / `pie` / `donut`) and `series` (required; pie/donut take exactly one series), plus `title`, `legend` (`'bottom'` / `'none'`), `markers` (line series), `colors` (hex palette override), `altText`, `intro` (paragraph above the chart), `pdfA`, `outputMode` / `outputPath`. For a chart amongst headings/paragraphs/tables, use a `chart` block inside `generate_basic_pdf` — both build identical pdfnative blocks. +**Inputs:** `chartType` (`bar` / `barH` / `line` / `pie` / `donut`, plus — _(v1.6.0)_ — `stackedBar` / `stackedBarH` / `area` / `scatter`) and `series` (required; pie/donut take exactly one series), plus `title`, `legend` (`'bottom'` / `'none'`), `markers` (line series), `colors` (hex palette override), `altText`, `intro` (paragraph above the chart), `pdfA`, `outputMode` / `outputPath`. For a chart amongst headings/paragraphs/tables, use a `chart` block inside `generate_basic_pdf` — both build identical pdfnative blocks. + +**Charts v2** _(v1.6.0)_: per-series `xValues` with `xAxis.type: 'linear' | 'time'` (UTC-deterministic time axes), a secondary right axis (`axis2`), `axis.scale: 'log'`, `dataLabels`, and `labelStride` / `labelRotation` for crowded x labels. Engine cross-field rules surface as `CHART_ERROR` with a remedy. ### `read_form_fields` _(v1.5.0)_ @@ -3874,22 +3923,101 @@ Opens an encrypted PDF (RC4, AES-128 or AES-256) and emits an unencrypted copy * > The rebuild drops signatures and `/AcroForm`. To *read* an encrypted PDF without rebuilding it, pass `password` to `inspect_pdf` / `extract_text` / `extract_attachments` instead. Excluded from the response cache. +### `add_ltv` _(v1.6.0)_ + +Embeds long-term-validation material — a `/DSS` (Document Security Store) with `/VRI` entries — into an already-signed PDF via incremental update, taking a PAdES B-T document to **B-LT**. + +```jsonc +{ + "pdfBase64": "", + "mode": "online", + "preferOcsp": true, + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `mode` (`'online'` default — fetches OCSP responses / CRLs through the operator-configured revocation provider; `'offline'` — embeds parse-validated caller-supplied material), `preferOcsp`, `certificatesDerBase64[]` / `ocspResponsesDerBase64[]` / `crlsDerBase64[]` (offline material), `outputMode` / `outputPath`. The structured result includes a `summary` of what was embedded. + +**Errors:** `LTV_NO_SIGNATURE` (nothing to validate), `LTV_EMPTY` (no material gathered), `LTV_MATERIAL_INVALID`, `LTV_ERROR`, `REVOCATION_NOT_CONFIGURED` (online mode without `PDFNATIVE_MCP_REVOCATION`), `NETWORK_HOST_NOT_ALLOWED`, `NETWORK_ERROR`, `ENCRYPTED_SOURCE` (decrypting would destroy the signatures — sign before encrypting). + +> Online mode is the **only** network-touching path in the server besides the TSA, and it only ever contacts operator-allow-listed hosts. See *Environment variables* above. + +### `timestamp_pdf` _(v1.6.0)_ + +Appends a `/DocTimeStamp` signature field (ETSI.RFC3161) through the operator-configured TSA, taking a B-LT document to **B-LTA**. The token's status, message imprint and nonce are checked before embedding; its own CMS signature is verified by `verify_pdf`. + +```jsonc +{ + "pdfBase64": "", + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `fieldName` (auto-suffixed `DocTimeStamp1`, `DocTimeStamp2`, … when omitted — supporting periodic re-timestamping), `placeholderBytes` (4096–65536), `outputMode` / `outputPath`. + +**Errors:** `TSA_NOT_CONFIGURED`, `TSA_REJECTED`, `NETWORK_ERROR`, `ENCRYPTED_SOURCE`. + +### `update_metadata` _(v1.6.0)_ + +Rewrites an existing PDF's `/Info` dictionary — and its XMP packet when present — via a non-destructive **incremental update**, so earlier revisions stay a byte-exact prefix. + +```jsonc +{ + "pdfBase64": "", + "title": "Quarterly Report — Final", + "author": "Finance Team", + "keywords": "finance, Q1, 2026", + "modDate": "2026-08-25T09:00:00+02:00", + "outputMode": "base64" +} +``` + +**Inputs:** `pdfBase64` (required), `title`, `author`, `subject`, `keywords` (≤ 1000 characters), `modDate` (ISO-8601 with offset — pinned for reproducible bytes), `outputMode` / `outputPath`. + +**Errors:** `ENCRYPTED_SOURCE` (use `decrypt_pdf` → `update_metadata` → `encrypt_pdf`), `METADATA_ERROR`, `PDF_PARSE_FAILED`. + +### `inspect_layout` _(v1.6.0)_ + +A read-only pagination **dry run**: measures how a prospective document would paginate — without producing a PDF and without consuming output tokens on base64 bytes. The seventh read tool with `verbosity` / `fields`. + +```jsonc +{ + "title": "Q1 2026 Report", + "blocks": [ + { "type": "heading", "text": "Executive Summary", "level": 1 }, + { "type": "paragraph", "text": "Revenue grew 24 % year over year." } + ], + "verbosity": "summary" +} +// → { "pageWidth": 595.28, "pageHeight": 841.89, "totalPages": 1, "blockCount": 2 } +``` + +**Inputs:** `title` and `blocks` (required), plus every input that moves a block — `footerText`, `pdfA`, `normalize`, `embedFonts`, `pageSize`, `margins`, `headerTemplate`, `footerTemplate` — and `verbosity` / `fields`. + +**Outputs (full):** `pageWidth`, `pageHeight`, `margins`, `totalPages`, and `pages[].blocks[]` with each block's `type`, `page`, `x`, `top`, `width`, `height` (2-decimal points). + +> **Known engine gap:** a `toc` block is measured as 0 pt, so a document with a printed contents page may paginate one page later than previewed. + --- ## MCP prompts -Since v1.4.0 the server advertises the MCP **`prompts`** capability with two prompts, sourced from the same governance contract the tools enforce: +Since v1.4.0 the server advertises the MCP **`prompts`** capability; v1.6.0 grows it to **six prompts** — the two governance prompts plus four recipe prompts: | Prompt | Purpose | |---|---| | `governance_contract` | The full AI-governance / Human-in-the-Loop contract. | | `draft_issue_workflow` | The step-by-step recipe for producing a compliant issue draft with `draft_governance_issue`. | +| `pades_ladder` _(v1.6.0)_ | The B-B → B-T → B-LT → B-LTA recipe: `sign_pdf` → `add_ltv` → `timestamp_pdf`, verified with `verify_pdf ltv: true`. | +| `print_ready` _(v1.6.0)_ | Producing press-ready output: bleed, printer's marks, custom OutputIntent. | +| `reproducible_output` _(v1.6.0)_ | Byte-stable output via pinned `creationDate` / `signingTime` / `modDate`. | +| `pdfa_valid` _(v1.6.0)_ | Producing a PDF/A file that veraPDF actually accepts (`embedFonts`, `strict`, diagnostics). | ## Error codes | `code` | Raised by | Meaning / fix | |---|---|---| -| `ENCRYPTED_SOURCE` | `annotate_pdf` | The source PDF is encrypted; `annotate_pdf` has no `password` parameter (nor do `sign_pdf` and `validate_pdf`). Use `decrypt_pdf` first (note: that rebuild drops signatures/forms), or pass `password` to the tools that accept it. | +| `ENCRYPTED_SOURCE` | `annotate_pdf`, `update_metadata`, `add_ltv`, `timestamp_pdf` | The source PDF is encrypted and the tool has no `password` parameter. Remedies are tool-specific since v1.6.0: for `annotate_pdf` / `update_metadata`, `decrypt_pdf` → edit → `encrypt_pdf`; for `add_ltv` / `timestamp_pdf`, decrypting would destroy the signatures — sign before encrypting. | | `PASSWORD_REQUIRED` *(v1.5.0)* | password-aware tools (`inspect_pdf`, `verify_pdf`, `extract_text`, `extract_attachments`, `read_form_fields`, `fill_form`, page-tree trio, `encrypt_pdf`, `decrypt_pdf`) | The source is encrypted and no `password` was supplied. | | `PASSWORD_INVALID` *(v1.5.0)* | password-aware tools | The supplied `password` does not open the document. | | `ENCRYPTION_UNSUPPORTED` *(v1.5.0)* | password-aware tools | The document uses an encryption scheme the reader does not support. | @@ -3899,6 +4027,20 @@ Since v1.4.0 the server advertises the MCP **`prompts`** capability with two pro | `OUTPUT_TOO_LARGE` | PDF-emitting tools | An emitted PDF exceeds 50 MiB (for `split_pdf`, also the 200 MiB aggregate), or assembly exceeds `maxOutputSizeBytes` (default 256 MiB). | | `PDF_PARSE_FAILED` | page-tree tools | The source bytes are not a parseable PDF. | | `GOVERNANCE_VIOLATION` | `draft_governance_issue` | The draft breaks the AI-governance contract (proposes a runtime dependency, omits a reproduction, or `duplicateSearchPerformed: false`). Fix the draft and retry. | +| `TSA_NOT_CONFIGURED` *(v1.6.0)* | `sign_pdf` (`timestamp: true`), `timestamp_pdf` | No `PDFNATIVE_MCP_TSA_URL` is set. Configure the TSA in the server environment. | +| `TSA_REJECTED` *(v1.6.0)* | `sign_pdf`, `timestamp_pdf` | The timestamp authority declined the request. | +| `REVOCATION_NOT_CONFIGURED` *(v1.6.0)* | `add_ltv` (`mode: 'online'`) | No `PDFNATIVE_MCP_REVOCATION` sources are set. Configure them, or use `mode: 'offline'` with caller-supplied material. | +| `NETWORK_HOST_NOT_ALLOWED` *(v1.6.0)* | `add_ltv` | A certificate-supplied OCSP/CRL URL points outside `PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS`. | +| `NETWORK_ERROR` *(v1.6.0)* | TSA/LTV tools | The TSA/OCSP/CRL request failed (timeout, refusal, size cap). | +| `LTV_NO_SIGNATURE` / `LTV_EMPTY` / `LTV_MATERIAL_INVALID` / `LTV_ERROR` *(v1.6.0)* | `add_ltv` | No signature to validate / no material gathered / supplied DER material does not parse / embedding failed. | +| `METADATA_ERROR` *(v1.6.0)* | `update_metadata` | The incremental `/Info` + XMP rewrite failed. | +| `PRINT_ERROR` *(v1.6.0)* | document tools with `print` | The requested page boxes are inconsistent (e.g. do not fit `pageSize`). | +| `CHART_ERROR` *(v1.6.0)* | `add_chart`, `chart` blocks | An engine cross-field chart rule failed (the message carries the remedy). | +| `PLACEHOLDER_AMBIGUOUS` *(v1.6.0)* | `sign_pdf` | Several unsigned placeholders exist and no `fieldName` was given. | +| `SIGNATURE_FIELD_NOT_FOUND` *(v1.6.0)* | `sign_pdf` | The named `fieldName` does not exist. | +| `CMS_PARSE_FAILED` *(v1.6.0)* | `verify_pdf` | A CMS structure is shorter or more malformed than the parser expects. | + +> **Protocol errors** *(v1.6.0)*: calling an unknown tool or prompt name is now a JSON-RPC `-32602` error (`[UNKNOWN_TOOL]` / `[UNKNOWN_PROMPT]`), not an `isError` result. Likewise, an unknown or misspelt input key — top-level or nested — fails with `VALIDATION_ERROR` ("Unrecognized key") instead of being silently stripped. --- @@ -3919,8 +4061,8 @@ When set on `add_international_text`, the `latin` font auto-registers so non-Win ## Output modes -Every document-producing tool accepts an `outputMode` field. The read-only -tools (`inspect_pdf`, `validate_pdf`, `verify_pdf`, `extract_text`, +Every document-producing tool accepts an `outputMode` field. The seven read-only +tools (`inspect_pdf`, `inspect_layout`, `validate_pdf`, `verify_pdf`, `extract_text`, `extract_attachments`, `read_form_fields`) return JSON only and have no `outputMode`: @@ -3976,12 +4118,13 @@ This workflow uses two tools in sequence: `pdfnative-mcp` is designed to run safely inside your AI client: -- **No network access** — the server does not open outbound connections. +- **No network access by default** — the server opens no outbound connection unless the **operator** configures TSA/OCSP/CRL endpoints for the PAdES tools (see the network charter under *Environment variables*). URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard. - **Sandboxed file writes** — `file` output mode is gated by `PDFNATIVE_MCP_OUTPUT_DIR`. When unset, file writes are rejected with a `SecurityError`. - **Path traversal protection** — absolute paths, `..` sequences, NUL bytes, and non-`.pdf` extensions are all rejected. - **Output size cap** — PDF output is capped at **50 MiB** per call. `split_pdf` additionally enforces **50 MiB per part** and a **200 MiB aggregate** ceiling across its parts, returning `OUTPUT_TOO_LARGE` when exceeded. -- **HTTP transport hardening** — when bound to `PDFNATIVE_MCP_PORT`, the Streamable HTTP transport is loopback-only and rejects foreign `Host` / `Origin` headers with **403** (DNS-rebinding protection). -- **Input validation** — every tool validates inputs against strict JSON Schemas and Zod runtime checks at the boundary. +- **HTTP transport hardening** — when bound to `PDFNATIVE_MCP_PORT`, the HTTP transport is loopback-only and rejects foreign `Host` / `Origin` headers with **403** (DNS-rebinding protection). An opt-in bearer token (`PDFNATIVE_MCP_HTTP_TOKEN`) adds authentication _(v1.6.0)_ — without it, the loopback endpoint has none. +- **Input validation** — every tool validates inputs against strict JSON Schemas and Zod runtime checks at the boundary. Since v1.6.0 every schema is `.strict()` at every nesting level, so an unknown or misspelt key is a `VALIDATION_ERROR` instead of being silently stripped, and PEM armour where DER base64 is expected fails with the exact `openssl … -outform DER` remedy. +- **Decompression cap** — the engine's 100 MiB per-stream inflate cap is operator-tunable via `PDFNATIVE_MCP_MAX_INFLATE_BYTES` _(v1.6.0)_. See [SECURITY.md](https://github.com/Nizoka/pdfnative-mcp/blob/main/SECURITY.md) for responsible disclosure. @@ -4008,7 +4151,7 @@ Split the content across multiple tool calls or reduce image/barcode count. ## Release history -The current release is **v1.5.0** (24 tools, built on pdfnative 1.7.0 — see the header note). Per-release notes, oldest first: +The current release is **v1.6.0** (28 tools, built on pdfnative 1.7.0 — see the header note). Per-release notes, oldest first: ### What's new in v1.0.0 @@ -4083,7 +4226,7 @@ v1.4.0 upgrades the engine to **pdfnative 1.5.0**, brings the pdfnative **AI-gov ### What's new in v1.5.0 -v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools**, closing the read/modify loop on documents the assistant did not create. +v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. - **Five new tools:** - `add_chart` — bar, horizontal-bar, line, pie and donut charts drawn as **native PDF vector paths**. No rasterisation and no image round-trip, so the output stays sharp at any zoom and tags as `/Figure` with alt text. If you were previously asking an agent to render a chart to PNG and push it through `embed_image`, stop — this replaces that. @@ -4097,6 +4240,34 @@ v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **2 - **Tool annotations** (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are published in `tools/list`, letting a client reason about which calls are safe to retry. - The encryption tools are deliberately **excluded from the response cache**. +### What's new in v1.6.0 + +v1.6.0 aligns the server with the **MCP 2026-07-28** specification and grows the catalogue to **28 tools**, exposing the engine's full document model and completing the PAdES baseline ladder. No breaking changes: every v1.5.0 call keeps its inputs, error codes and response shape, and a superset gate against the published 1.5.0 catalogue proves it. + +- **Four new tools:** + - `add_ltv` — embed `/DSS` + `/VRI` long-term-validation material (PAdES **B-LT**), online through the operator-configured revocation provider or offline from caller-supplied material. + - `timestamp_pdf` — append a `/DocTimeStamp` through the operator TSA (PAdES **B-LTA**), with auto-suffixed field names for periodic re-timestamping. + - `update_metadata` — rewrite `/Info` (+ XMP) of an existing PDF as an incremental update. + - `inspect_layout` — a read-only pagination dry run: page count and block positions with no PDF produced. +- **Full engine coverage** — `generate_basic_pdf` composes all **13 block kinds** (`table`, `image`, `link`, `toc`, `barcode`, `svg`, `formField` join the six existing ones); `pageSize`, `margins`, running `headerTemplate` / `footerTemplate`, `compress` and `debug` on the nine document tools; build-time `encrypt` that **keeps the AcroForm**; image watermarks with `position`. +- **PAdES signing** — `sign_pdf` gains `profile: 'pades'`, `timestamp: true` (RFC 3161, B-T), RSA-SHA384/512, `certChainDerBase64`, `fieldName` / `allowMultiple`; `verify_pdf ltv: true` reports profile, timestamp, embedded revocation status and the achieved level; `prepare_signature_placeholder` gains `subFilter` / `reserveTimestamp` and freezes signer metadata into the `/Sig` dictionary (fixing a pre-1.7 engine bug where those values were silently dropped). +- **Network charter** — still **no outbound request by default**; the single permitted egress class is the operator-configured TSA / OCSP / CRL endpoints, never a URL from a tool argument, behind an SSRF guard. Seven new environment variables configure the HTTP bearer token, the inflate cap and the network providers. +- **Print production** — `print` page boxes, `bleed`, printer's `marks`, `/UserUnit`, `metadata` with `/Trapped`, custom `outputIntent`; boxes survive the page-tree tools and are reported by `inspect_pdf`. +- **Charts v2** — `stackedBar` / `stackedBarH` / `area` / `scatter`, per-series `xValues` with linear or time axes, secondary `axis2`, `axis.scale: 'log'`, `dataLabels`, `labelStride` / `labelRotation`. +- **Honest PDF/A** — `embedFonts: true` (embed Noto Sans Latin for a claim veraPDF accepts), `strict: true` (fail instead of emitting a non-conformant file), `includeDiagnostics: true`; two new diagnostics; an advisory veraPDF corpus runs locally and in CI. +- **MCP 2026-07-28 transport** — SDK v2 (`@modelcontextprotocol/server`), `server/discover`, `resultType`, cache hints, per-result `serverInfo`, deterministic `tools/list`; 2025-era clients keep working via the automatic legacy fallback. +- **Four new recipe prompts** — `pades_ladder`, `print_ready`, `reproducible_output`, `pdfa_valid` — taking the prompt catalogue to six. +- **Reproducible output** — `creationDate` on all nine document tools, `signingTime` on the placeholder, `modDate` on `update_metadata`. + +**Migrating from v1.5.0** — six behaviour changes, all on error paths or on inputs the published schema already declared invalid: + +1. **Stray keys fail.** An unknown or misspelt key, top-level or nested, is now `VALIDATION_ERROR` ("Unrecognized key") instead of being silently stripped — the schemas always declared `additionalProperties: false`. +2. **Page-index mistakes are `VALIDATION_ERROR`.** Out-of-range pages / ranges on `merge_pdfs` / `split_pdf` / `extract_pages` were `PDF_PARSE_FAILED`; the message now carries a 0-based hint. +3. **`validate_pdf` on unparsable bytes is an error, not a verdict.** v1.5.0 returned `{ valid: false }`; v1.6.0 returns `isError: true` `[PDF_PARSE_FAILED]` — a parse failure is not a PDF/UA verdict. +4. **Unknown tool or prompt names are protocol errors** — JSON-RPC `-32602` (`[UNKNOWN_TOOL]` / `[UNKNOWN_PROMPT]`), no `isError` result. +5. **`inspect_pdf.checks` holds only the keys you asked for** — read `checksPassed` or the requested key, never an absent one. +6. **`add_form` text areas change bytes.** `fieldType: 'textarea'` now produces a real multi-line field (`/Ff 4096`). + --- ## Further reading @@ -4209,7 +4380,7 @@ Then prompt your assistant: > *Create a PDF/A-2b invoice for ACME Inc, add a multilingual paragraph in Arabic, and sign it with my key.* -The assistant calls `generate_basic_pdf` (with `pdfA: "pdfa2b"`), then `add_international_text` (with `lang: ["ar", "emoji"]`), `add_table`, `sign_pdf`, and finally `inspect_pdf` — confirming the result. v1.4.0 ships **24 tools**, including the page-tree trio `merge_pdfs`, `split_pdf`, `extract_pages`, markup `annotate_pdf`, the network-free `draft_governance_issue`, plus `validate_pdf`, `verify_pdf`, `add_attachment`, `extract_attachments`, and `extract_text`. +The assistant calls `generate_basic_pdf` (with `pdfA: "pdfa2b"`), then `add_international_text` (with `lang: ["ar", "emoji"]`), `add_table`, `sign_pdf`, and finally `inspect_pdf` — confirming the result. v1.6.0 ships **28 tools**, including the complete PAdES ladder (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), the page-tree trio `merge_pdfs`, `split_pdf`, `extract_pages`, markup `annotate_pdf`, `update_metadata`, the read-only `inspect_layout` pagination preview, the network-free `draft_governance_issue`, plus `validate_pdf`, `verify_pdf`, `add_attachment`, `extract_attachments`, and `extract_text`. Next: [MCP guide →](mcp.html) · [MCP playground →](../playgrounds/mcp.html) @@ -4807,6 +4978,17 @@ Each `PdfDiagnostic` carries a machine-readable `code`, a `severity` The code list is a stable, additions-only union (`PdfDiagnosticCode`), so a sink written today keeps compiling as future codes are added. +> **On the MCP surface** _(pdfnative-mcp 1.6.0)_, the same honesty is exposed +> as three opt-in inputs on every document tool: `embedFonts: true` embeds +> Noto Sans Latin so a PDF/A claim on base-14 text is actually accepted by +> veraPDF, `strict: true` fails instead of producing a non-conformant file, +> and `includeDiagnostics: true` echoes the engine's diagnostics +> (`PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, +> `PDFA_DEVICE_CMYK_IMAGE`) in the tool result. Known engine limitation: +> `add_form` (or a `formField` block) under a PDF/A claim still fails +> veraPDF even with `embedFonts` — the AcroForm `/DR /Helv` font is an +> unembedded Type1 (ISO 19005-2 §6.2.11.4.1). + ## v1.1.0 status — fully validated v1.1.0 ships full PDF/A-1b / 2b / 2u / 3b conformance against the @@ -5854,7 +6036,9 @@ verify a signed PDF end to end (byte-range digest, CMS signature value, chain, trust, timestamps, revocation), use [`pdfnative-cli verify`](cli.html#pdfnative-verify) or the [`verify_pdf` MCP tool](mcp.html) — the CMS verification logic lives -there. +there. Since pdfnative-mcp 1.6.0, `verify_pdf` also validates +`/DocTimeStamp` fields as RFC 3161 tokens and reports the achieved +PAdES level (B-B → B-LTA) with `ltv: true`. ## Why a separate placeholder step? @@ -6104,7 +6288,7 @@ for the idempotency proof. - [PDF/A conformance →](pdfa.html) — how signatures interact with PDF/A-2b/3b. - [Architecture →](architecture.html) — where the crypto module sits in the dependency graph. - [CLI →](cli.html) — `pdfnative-cli sign` wraps this exact pipeline. -- [MCP integration →](mcp.html) — `pdfnative-mcp` exposes signing as an AI tool. +- [MCP integration →](mcp.html) — `pdfnative-mcp` exposes signing as an AI tool, and since v1.6.0 the complete PAdES ladder: `sign_pdf` (`profile: 'pades'`, RFC 3161 `timestamp`) → `add_ltv` (`/DSS` + `/VRI`, B-LT) → `timestamp_pdf` (`/DocTimeStamp`, B-LTA), verified with `verify_pdf ltv: true`. --- diff --git a/docs/playgrounds/index.html b/docs/playgrounds/index.html index 7798c0c..c5c7c6a 100644 --- a/docs/playgrounds/index.html +++ b/docs/playgrounds/index.html @@ -247,7 +247,7 @@

    CLI builder CLI

    MCP tool explorer MCP

    -

    Browse the pdfnative-mcp v1.5.0 toolset (24 tools: incl. page-tree merge_pdfs, split_pdf, extract_pages, markup annotate_pdf, the network-free draft_governance_issue, plus validate_pdf, verify_pdf, add_attachment, extract_attachments), copy a ready-to-paste config snippet for Claude Desktop, Cursor, Continue, Zed (or any stdio MCP client), and generate the same PDFs an AI assistant would receive — directly in your browser.

    +

    Browse the pdfnative-mcp v1.6.0 toolset (28 tools: incl. the PAdES ladder sign_pdf / add_ltv / timestamp_pdf, update_metadata, the read-only inspect_layout preview, page-tree merge_pdfs / split_pdf / extract_pages, markup annotate_pdf, the network-free draft_governance_issue), copy a ready-to-paste config snippet for Claude Desktop, Cursor, Continue, Zed (or any MCP client), and generate the same PDFs an AI assistant would receive — directly in your browser.

    stdio toolsClaudeCursorZed+ any stdio client
    diff --git a/docs/playgrounds/mcp.html b/docs/playgrounds/mcp.html index cb068ef..2d22d88 100644 --- a/docs/playgrounds/mcp.html +++ b/docs/playgrounds/mcp.html @@ -4,8 +4,8 @@ MCP Tool Explorer — pdfnative - - + + @@ -14,14 +14,14 @@ - - + + - - + + @@ -66,14 +66,14 @@ "applicationCategory": "DeveloperApplication", "operatingSystem": "Any (browser-based)", "url": "https://pdfnative.dev/playgrounds/mcp.html", - "description": "Interactive explorer for the 24 pdfnative-mcp tools. Browse JSON schemas, see example prompts, and generate the exact PDFs an AI assistant receives — directly in the browser.", + "description": "Interactive explorer for the 28 pdfnative-mcp tools. Browse JSON schemas, see example prompts, and generate the exact PDFs an AI assistant receives — directly in the browser.", "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, "isPartOf": { "@type": "WebSite", "name": "pdfnative", "url": "https://pdfnative.dev" }, "about": { "@type": "SoftwareApplication", "inLanguage": "en", "name": "pdfnative-mcp", - "softwareVersion": "1.5.0", + "softwareVersion": "1.6.0", "applicationCategory": "DeveloperApplication", "operatingSystem": "Node.js", "url": "https://github.com/Nizoka/pdfnative-mcp" @@ -127,7 +127,7 @@

    pdfnative-mcp tool explorer

    -

    The tools an AI assistant gets when you wire pdfnative-mcp into Claude Desktop, Cursor, Continue, Zed, or any other stdio MCP client (Cline, Windsurf, Goose, Gemini CLI…). The server exposes 24 tools — including the v1.4.0 markup annotate_pdf and network-free draft_governance_issue, the v1.3.0 page-tree trio merge_pdfs / split_pdf / extract_pages, plus validate_pdf, inspect_pdf, verify_pdf, add_attachment / extract_attachments (Factur-X / ZUGFeRD PDF/A-3), and extract_text — a pdfA flag on every document tool, multi-script lang on add_international_text, token-frugal read modes (verbosity / fields), and a per-tool _meta.apiVersion stability contract. Each card below shows a real example prompt, the pdfnative library payload this browser demo executes (the MCP tool schemas differ — see the MCP guide for the exact schemas), and a Generate PDF button that produces the very PDF the AI assistant would receive — running the underlying pdfnative library directly in your browser. No API keys, no install, no LLM cost.

    +

    The tools an AI assistant gets when you wire pdfnative-mcp into Claude Desktop, Cursor, Continue, Zed, or any other MCP client (Cline, Windsurf, Goose, Gemini CLI…). The server exposes 28 tools — including the v1.6.0 PAdES ladder add_ltv / timestamp_pdf (with RFC 3161 timestamps on sign_pdf), update_metadata, the read-only inspect_layout pagination preview, all 13 block kinds in generate_basic_pdf, print production and charts v2, the v1.5.0 add_chart / form / encryption tools, the v1.4.0 markup annotate_pdf and network-free draft_governance_issue, and the v1.3.0 page-tree trio merge_pdfs / split_pdf / extract_pages — a pdfA flag on every document tool, multi-script lang on add_international_text, token-frugal read modes (verbosity / fields), and a per-tool _meta.apiVersion stability contract. Each card below shows a real example prompt, the pdfnative library payload this browser demo executes (the MCP tool schemas differ — see the MCP guide for the exact schemas), and a Generate PDF button that produces the very PDF the AI assistant would receive — running the underlying pdfnative library directly in your browser. No API keys, no install, no LLM cost.

    For the full integration story (config, security model, signed-document workflow), see the MCP guide.

    @@ -146,7 +146,7 @@

    1. Configure your client

    2. Try the tools

    Loading pdfnative from esm.sh… this happens once per page load.

    -

    The interactive cards below cover the core document-authoring tools, the v1.3.0 page-tree trio (merge_pdfs, split_pdf, extract_pages), and the v1.4.0 annotate_pdf — all of which build their own source PDFs in the browser and run the real pdfnative 1.7.0 API. draft_governance_issue produces no PDF (it validates a report locally, network-free) — use its Copy args button. Nine of the 24 tools have no card here, because they need a real PDF as input and run best from your own MCP client: validate_pdf, verify_pdf, add_attachment, extract_attachments, extract_text, read_form_fields, fill_form, encrypt_pdf and decrypt_pdf. See the MCP guide for their full schemas.

    +

    The interactive cards below cover the core document-authoring tools, the v1.3.0 page-tree trio (merge_pdfs, split_pdf, extract_pages), the v1.4.0 annotate_pdf, and the v1.6.0 update_metadata — all of which build their own source PDFs in the browser and run the real pdfnative 1.7.0 API. Two cards produce no PDF by design: inspect_layout returns a pagination report (run it to see the JSON), and draft_governance_issue validates a report locally, network-free — use its Copy args button. Eleven of the 28 tools have no card here: nine need a real PDF as input and run best from your own MCP client (validate_pdf, verify_pdf, add_attachment, extract_attachments, extract_text, read_form_fields, fill_form, encrypt_pdf, decrypt_pdf), and two need the operator-configured network providers a browser page cannot supply (add_ltv, timestamp_pdf — TSA / OCSP / CRL endpoints set via environment variables). See the MCP guide for their full schemas.

    @@ -305,7 +305,7 @@

    Resources

    }, { id: 'add_chart', - purpose: 'Render a bar, horizontal-bar, line, pie or donut chart as native PDF vector paths — no rasterisation, no image round-trip.', + purpose: 'Render a chart as native PDF vector paths — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log and time scales; no rasterisation, no image round-trip.', prompt: 'Chart our Q1-Q4 revenue against cost as a bar chart, and tag it for accessibility.', args: { title: 'FY2026 performance', @@ -326,7 +326,7 @@

    Resources

    }, { id: 'add_international_text', - purpose: 'Render text in any of 24 lang codes — 22 scripts plus latin and emoji, with explicit math on top — with BiDi & OpenType shaping. You can pass several in one call.', + purpose: 'Render text in any of 25 lang codes — 22 scripts plus latin, emoji and the explicit math font — with BiDi & OpenType shaping. You can pass several in one call.', prompt: 'Create a multilingual greeting card with English, Thai, Arabic, and a smiling-face emoji.', args: { title: 'Multilingual greeting', @@ -360,7 +360,7 @@

    Resources

    }, { id: 'add_form', - purpose: 'Build an interactive AcroForm PDF (text, checkbox, radio, dropdown).', + purpose: 'Build an interactive AcroForm PDF (text, textarea, checkbox, radio, dropdown — plus listbox and placeholder since v1.6.0).', prompt: 'Create a contact form PDF with a name field, an email field, and a “subscribe to newsletter” checkbox.', args: { title: 'Contact us', @@ -407,7 +407,7 @@

    Resources

    }, { id: 'sign_pdf', - purpose: 'Apply a CMS/PKCS#7 signature (RSA or ECDSA) to an existing PDF.', + purpose: 'Apply a PAdES CMS signature (RSA-SHA256/384/512 or ECDSA) to an existing PDF — with profile: "pades", an RFC 3161 timestamp, certificate chains and multiple signatures since v1.6.0.', prompt: 'Sign the prepared contract using our corporate certificate.', args: { algorithm: 'rsa-sha256', @@ -453,6 +453,30 @@

    Resources

    }); }, }, + { + id: 'inspect_layout', + purpose: 'Dry-run pagination: page count, page geometry and the position of every block — with no PDF produced (new in v1.6.0).', + prompt: 'Before rendering, tell me how many pages this report will take and where the table lands.', + args: { + title: 'Layout preview', + blocks: [ + { type: 'heading', text: 'Quarterly report', level: 1 }, + { type: 'paragraph', text: 'A short summary paragraph that flows above the figures table.' }, + { type: 'table', + headers: ['Quarter', 'Revenue'], + rows: [ + { cells: ['Q1', '$1.2M'] }, + { cells: ['Q2', '$1.5M'] }, + ], + }, + ], + }, + noteHtml: 'The MCP tool wraps the same read-only inspectDocumentLayout() this button runs — token-frugal by design, because no base64 PDF bytes are produced or transferred. The button prints the real pagination report (page size, margins, and each block’s page / x / top / width / height) below the card.', + runJson: (p, m) => { + requireFn(m, 'inspectDocumentLayout'); + return m.inspectDocumentLayout(p); + }, + }, { id: 'merge_pdfs', purpose: 'Concatenate 2–50 PDFs into one document (page-tree API, new in v1.3.0).', @@ -564,6 +588,40 @@

    Resources

    return mod.save(); }, }, + { + id: 'update_metadata', + purpose: 'Rewrite the /Info dictionary (+ XMP) of an existing PDF — title, author, subject, keywords, pinned modDate — via a non-destructive incremental update (new in v1.6.0).', + prompt: 'Set the title of this PDF to "Quarterly Report — Final" and the author to "Finance Team", without touching anything else.', + args: { + pdfBase64: '', + title: 'Quarterly Report — Final', + author: 'Finance Team', + keywords: 'finance, quarterly, 2026', + modDate: '2026-08-25T09:00:00+02:00', + outputMode: 'base64', + }, + noteHtml: 'In a real MCP call the AI assistant supplies the source as base64. This demo builds a small source PDF in the browser, then runs the real pdfnative 1.7.0 openPdf()createModifier()updateMetadata()save() pipeline the tool calls — an incremental update, so the original bytes stay a byte-exact prefix. Pinning modDate keeps the output reproducible. Encrypted sources are rejected (ENCRYPTED_SOURCE), matching the tool contract.', + build: (p, m) => { + requireFn(m, 'openPdf'); + requireFn(m, 'createModifier'); + const src = m.buildDocumentPDFBytes({ title: 'Quarterly report (draft)', blocks: [ + { type: 'heading', text: 'Quarterly report', level: 1 }, + { type: 'paragraph', text: 'Source document handed to update_metadata — its /Info dictionary is rewritten as an appended revision.' }, + ]}); + const reader = m.openPdf(src); + const mod = m.createModifier(reader); + if (typeof mod.updateMetadata !== 'function') { + throw new Error('updateMetadata() requires pdfnative ≥ 1.7.0 — the CDN served an older build. Try reloading.'); + } + mod.updateMetadata({ + title: 'Quarterly Report — Final', + author: 'Finance Team', + keywords: 'finance, quarterly, 2026', + modDate: new Date('2026-08-25T09:00:00+02:00'), + }); + return mod.save(); + }, + }, { id: 'draft_governance_issue', purpose: 'Generate a policy-compliant GitHub issue report locally — fully network-free (new in v1.4.0).', @@ -612,16 +670,18 @@

    ${t.id}

    ${t.prompt}
    ${t.noteHtml ? `

    ${t.noteHtml}

    ` : ''}
    - ${t.build ? 'Payload (pdfnative library form)' : 'MCP tool-call payload (schema example)'} + ${(t.build || t.runJson) ? 'Payload (pdfnative library form)' : 'MCP tool-call payload (schema example)'}
    - ${t.build ? '

    This is the pdfnative library payload the browser demo executes; the MCP tool schema differs — see the MCP guide.

    ' : ''} + ${(t.build || t.runJson) ? '

    This is the pdfnative library payload the browser demo executes; the MCP tool schema differs — see the MCP guide.

    ' : ''}
    ${escapeHtml(JSON.stringify(t.args, null, 2))}
    ${t.build ? `` : ''} + ${t.runJson ? `` : ''}
    + ${t.runJson ? '' : ''}
    `; cards.appendChild(card); @@ -677,14 +737,24 @@

    ${t.id}

    if (btn.dataset.action === 'run') { if (!pdfnativeMod) return; btn.disabled = true; - status.textContent = 'Generating…'; status.className = 'status'; + status.textContent = tool.runJson ? 'Measuring…' : 'Generating…'; status.className = 'status'; try { const t0 = performance.now(); - const bytes = await tool.build(tool.args, pdfnativeMod); - const elapsed = (performance.now() - t0).toFixed(0); - downloadPdf(bytes, tool.id + '.pdf'); - status.textContent = `Generated ${(bytes.length / 1024).toFixed(1)} KB in ${elapsed} ms.`; - status.className = 'status success'; + if (tool.runJson) { + const report = await tool.runJson(tool.args, pdfnativeMod); + const elapsed = (performance.now() - t0).toFixed(0); + const out = card.querySelector('[data-result]'); + out.textContent = JSON.stringify(report, null, 2); + out.hidden = false; + status.textContent = `Previewed ${report.totalPages} page(s) in ${elapsed} ms — no PDF produced.`; + status.className = 'status success'; + } else { + const bytes = await tool.build(tool.args, pdfnativeMod); + const elapsed = (performance.now() - t0).toFixed(0); + downloadPdf(bytes, tool.id + '.pdf'); + status.textContent = `Generated ${(bytes.length / 1024).toFixed(1)} KB in ${elapsed} ms.`; + status.className = 'status success'; + } } catch (e) { status.textContent = 'Error: ' + (e && e.message ? e.message : e); status.className = 'status error'; diff --git a/docs/responsibility.html b/docs/responsibility.html index f1069c5..08eb2cb 100644 --- a/docs/responsibility.html +++ b/docs/responsibility.html @@ -98,7 +98,7 @@

    Efficiency by architecture

    The engine has no runtime dependencies. pdfnative ships with no dependencies field at all: nothing transitive to install, audit, or patch.

    -

    Scoped honestly, because the ecosystem is not uniform: pdfnative-cli depends only on pdfnative itself. pdfnative-react adds one real runtime dependency, react-reconciler, with React and pdfnative as peers. pdfnative-mcp adds two, @modelcontextprotocol/sdk and zod. And this documentation site itself loads Prism, marked and DOMPurify from a CDN — pinned, with subresource-integrity hashes.

    +

    Scoped honestly, because the ecosystem is not uniform: pdfnative-cli depends only on pdfnative itself. pdfnative-react adds one real runtime dependency, react-reconciler, with React and pdfnative as peers. pdfnative-mcp adds two, @modelcontextprotocol/server and zod. And this documentation site itself loads Prism, marked and DOMPurify from a CDN — pinned, with subresource-integrity hashes.

    Verify it: package.json (no dependencies key) · pdfnative-react/package.json · pdfnative-mcp/package.json

    diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 837d73a..5228199 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -11,7 +11,7 @@ https://pdfnative.dev/ - 2026-08-01 + 2026-08-25 weekly 1.0 @@ -21,7 +21,7 @@ https://pdfnative.dev/guides/ - 2026-08-01 + 2026-08-25 weekly 0.9 @@ -29,7 +29,7 @@ https://pdfnative.dev/guides/onboarding.html - 2026-08-01 + 2026-08-25 monthly 0.8 @@ -45,7 +45,7 @@ https://pdfnative.dev/guides/architecture.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -53,7 +53,7 @@ https://pdfnative.dev/guides/ai-governance.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -61,7 +61,7 @@ https://pdfnative.dev/guides/agentic-workflows.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -77,7 +77,7 @@ https://pdfnative.dev/guides/pdfa.html - 2026-08-21 + 2026-08-25 monthly 0.7 @@ -93,7 +93,7 @@ https://pdfnative.dev/guides/signatures.html - 2026-08-20 + 2026-08-25 monthly 0.7 @@ -221,7 +221,7 @@ https://pdfnative.dev/guides/mcp.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -255,7 +255,7 @@ https://pdfnative.dev/playgrounds/ - 2026-08-01 + 2026-08-25 monthly 0.8 @@ -367,7 +367,7 @@ https://pdfnative.dev/learn/08-next-steps.html - 2026-08-01 + 2026-08-25 monthly 0.8 @@ -375,7 +375,7 @@ https://pdfnative.dev/responsibility.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -399,7 +399,7 @@ https://pdfnative.dev/playgrounds/mcp.html - 2026-08-01 + 2026-08-25 monthly 0.6 diff --git a/tests/docs/verify-docs.test.ts b/tests/docs/verify-docs.test.ts index 87b200c..c32d447 100644 --- a/tests/docs/verify-docs.test.ts +++ b/tests/docs/verify-docs.test.ts @@ -91,10 +91,10 @@ describe('verify-docs', () => { it('stale-token compares counts for equality, not against a blocklist', () => { withSandbox((dir) => { // A value that no blocklist would have enumerated. - patch(dir, 'docs/guides/mcp.md', '**24 tools**', '**25 tools**'); + patch(dir, 'docs/guides/mcp.md', '**28 tools**', '**29 tools**'); const run = runVerifier(dir); expect(run.output).toContain('stale-token'); - expect(run.output).toContain('the manifest says 24'); + expect(run.output).toContain('the manifest says 28'); expect(run.status).toBe(1); }); }, 120_000); From 94e95c48aa16a0f1a28432186c9dc4317528cc82 Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:44:53 +0200 Subject: [PATCH 03/15] docs(site): regenerate the Open Graph card for 1.7.0 and fix the test-count canonical The served og-image.png had never been re-rasterised since the v1.4.0 docs audit: its git history stops at 52cbbdd while og-image.svg was edited later in f88c3b0, so every social card still advertised the pre-1.6.0 feature line. The SVG subtitle now names the 1.7.0 headliners (LTV Signatures, Print Production) alongside the engine constants, and the PNG was re-captured per the procedure in the SVG header comment: chrome --headless --disable-gpu --hide-scrollbars \ --window-size=1200,630 --screenshot=og-image.png og-image.svg Verified 1200x630 with the new line rendered. No conversion tooling is committed - the SVG header comment remains the documented procedure. charts.html was the only page referencing og-image without og:image:width/height; the pair is added, and its og:description now enumerates all nine chart kinds instead of seven labels. ecosystem.json: the test-count-homepage canonical read "665+ tests across 122 files" - a dropped leading 2 that made the substring assertion unable to catch the very drift it exists for. Corrected to "2665+"; verify:docs stays green (docs/index.html carries the full string). --- docs/assets/ecosystem.json | 2 +- docs/assets/og-image.png | Bin 344498 -> 345658 bytes docs/assets/og-image.svg | 2 +- docs/playgrounds/charts.html | 4 +++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/assets/ecosystem.json b/docs/assets/ecosystem.json index 95e7606..08f98ac 100644 --- a/docs/assets/ecosystem.json +++ b/docs/assets/ecosystem.json @@ -190,7 +190,7 @@ { "id": "test-count-homepage", "$comment": "The homepage quoted a stale test count for a full release train; no assertion covered it. canonical-present pins the current figure where it is displayed.", - "canonical": "665+ tests across 122 files", + "canonical": "2665+ tests across 122 files", "requireIn": [ "docs/index.html" ] diff --git a/docs/assets/og-image.png b/docs/assets/og-image.png index 41b6653ea23dda58b6c2954345b308b4f7628b41..1c2c650ae128e09315960d8a62f5a0aec5627289 100644 GIT binary patch delta 109244 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

    BLIoX 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 XT{Gjn#o+>eazW#f*27X&i`V}H+Uvjc diff --git a/docs/assets/og-image.svg b/docs/assets/og-image.svg index e2a4194..4d040fa 100644 --- a/docs/assets/og-image.svg +++ b/docs/assets/og-image.svg @@ -58,7 +58,7 @@ Zero dependencies · ISO 32000-1 · PDF/A · TypeScript-first - Charts · Forms · Merge / Split · Encryption · Signatures · 22 Unicode scripts + Charts · Forms · Merge / Split · LTV Signatures · Print Production · 22 Unicode scripts diff --git a/docs/playgrounds/charts.html b/docs/playgrounds/charts.html index 549a598..0d110d6 100644 --- a/docs/playgrounds/charts.html +++ b/docs/playgrounds/charts.html @@ -13,8 +13,10 @@ - + + + From f6ff12fcace5e073fd896dcaae4c4220777c1637 Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:50:05 +0200 Subject: [PATCH 04/15] docs: correct every remaining factual claim against pdfnative 1.7.0 ground truth A cross-verified conformity audit of the whole documentation surface found the errors that the 17 verify-docs rules do not cover - counters, versions and API claims living outside the rule corpus, plus engine-version prose. Counters and versions: AGENTS.md said 2664+ tests and ~241 sample PDFs (manifest: 2665 / 242); testing.instructions.md was frozen at "2396+ tests across 105 files" with 44 generator modules (2665+ / 122 / 48) and its test tree missed tests/docs, tests/tools and tests/visual; quality-gate.prompt.md expected 2396+; SECURITY.md still supported 1.6.x/1.5.x (now 1.7.x/1.6.x). Signature claims: SECURITY.md, ROADMAP.md and pdf-core.instructions.md described two selectable algorithms - v1.7.0 has four (rsa-sha256/384/512, ecdsa-sha256, src/crypto/cms.ts); compliance-audit.prompt.md audited only adbe.pkcs7.detached where the engine now also emits ETSI.CAdES.detached and ETSI.RFC3161. MIRROR_MAP no longer exists in the code - the shaping instructions now name BIDI_MIRRORING_PAIRS and its full 428-pair UCD table, plus the v1.7.0 I1/I2 conformance work. Engine-version over-propagation: cli.md, react.md, mcp.md's v1.5.0 history entry, README's CLI v1.3.0 / MCP v1.5.0 history lines and llms.txt claimed "built on pdfnative 1.7.0" for packages whose published manifests pin ^1.6.0 (CLI 1.3.0, React 1.1.0) or shipped on the 1.6 engine (MCP 1.5.0, per its own release notes). All now state 1.6.0 with the pin/semver-accepts nuance, matching versions.js and the manifest. Site: the no-JS Library badge fallback said v1.6.0; the charts cards, metas and playground badge were frozen at five kinds/v1.6.0 while the engine has nine since 1.7.0; two "nine types" enumerations listed seven labels (barH and stackedBarH restored, also in README and llms.txt); learn pages said 26 reference guides (28); ltv.md and responsibility.html mischaracterised pdfnative-cli verify (full cryptographic verification, OCSP/CRL only behind --revocation online, no TSA contact) - and the published grep proof on responsibility.html is rephrased now that the v1.7.0 provider interfaces carry @example JSDoc showing user-land fetch. llms.txt (root + docs copy, byte-identical) also gains the v1.6/v1.7 roadmap detail its "v1.0 -> v1.7" line promised. --- .github/instructions/pdf-core.instructions.md | 4 ++-- .github/instructions/testing.instructions.md | 7 +++++-- .github/instructions/text-shaping.instructions.md | 4 ++-- .github/prompts/compliance-audit.prompt.md | 2 +- .github/prompts/quality-gate.prompt.md | 2 +- AGENTS.md | 6 +++--- README.md | 6 +++--- ROADMAP.md | 2 +- SECURITY.md | 8 ++++---- docs/guides/charts.html | 4 ++-- docs/guides/cli.md | 6 +++--- docs/guides/index.html | 2 +- docs/guides/ltv.md | 2 +- docs/guides/mcp.md | 2 +- docs/guides/react.md | 2 +- docs/index.html | 4 ++-- docs/learn/08-next-steps.html | 2 +- docs/learn/index.html | 2 +- docs/llms.txt | 8 ++++---- docs/playgrounds/charts.html | 4 ++-- docs/playgrounds/index.html | 8 ++++---- docs/playgrounds/mcp.html | 2 +- docs/responsibility.html | 4 ++-- llms.txt | 8 ++++---- 24 files changed, 52 insertions(+), 49 deletions(-) diff --git a/.github/instructions/pdf-core.instructions.md b/.github/instructions/pdf-core.instructions.md index 8f1c706..ccdead5 100644 --- a/.github/instructions/pdf-core.instructions.md +++ b/.github/instructions/pdf-core.instructions.md @@ -127,7 +127,7 @@ applyTo: "src/core/**" - LTR runs: standard encoding path (no BiDi processing) - Arabic shaping (`shapeArabicText()`) returns glyphs in logical order — must reverse for RTL visual rendering - Hebrew text detected by `containsHebrew()` — uses RTL ordering without shaping -- Glyph mirroring for brackets/parentheses in RTL context via `MIRROR_MAP` +- Glyph mirroring for brackets/parentheses in RTL context via `BIDI_MIRRORING_PAIRS` (`bidi-mirroring-data.ts`, full 428-pair UCD table since v1.7.0) - CRITICAL: never call `shapeArabicText()` on already-reversed text — always un-reverse to logical first ## PDF Encryption (ISO 32000-1 §7.6) @@ -263,7 +263,7 @@ applyTo: "src/core/**" ## Digital Signature Model (pdf-signature.ts — ISO 32000-1 §12.8) - `buildSigDict(options, contentsSize?)`: builds the `/Sig` dictionary with `/ByteRange` placeholder (`SigDictMetadata` options: signingTime, name, reason, location, contact) - Signature includes `/Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached` -- `signPdfBytes(pdfBytes, options)`: round-trip sign → inject CMS into `/Contents`; `PdfSignOptions = { signerCert: X509Certificate, rsaKey? | ecKey?, certChain?, algorithm?: 'rsa-sha256' | 'ecdsa-sha256', provider?, ...SigDictMetadata }` +- `signPdfBytes(pdfBytes, options)`: round-trip sign → inject CMS into `/Contents`; `PdfSignOptions = { signerCert: X509Certificate, rsaKey? | ecKey?, certChain?, algorithm?: 'rsa-sha256' | 'rsa-sha384' | 'rsa-sha512' | 'ecdsa-sha256', provider?, ...SigDictMetadata }` - `/ByteRange [0 before after end]`: specifies which bytes are signed (excludes `/Contents` hex) - CMS SignedData via `crypto/cms.ts`: signed attributes, certificate embedding, digest diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 97bf442..7197014 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -25,15 +25,18 @@ tests/ ├── worker/ # worker-api ├── integration/ # full PDF generation end-to-end, pdf-compression ├── fuzzing/ # edge-case & adversarial input tests +├── docs/ # documentation verifier mirror test (verify-docs) +├── tools/ # font-data tooling tests +├── visual/ # dual-mode visual-regression suite └── fixtures/ # test data, sample fonts, expected outputs scripts/ ├── generate-samples.ts # Orchestrator for modular PDF sample generation -├── generators/ # Per-category sample generators (44 modules) +├── generators/ # Per-category sample generators (48 modules) └── helpers/ # Shared utilities (fonts, images, I/O) ``` ## Current State (maintain these thresholds) -- **2396+ tests** across 105 test files + 1 benchmark file +- **2665+ tests** across 122 test files + 1 benchmark file - Statements: ~95% (threshold: 88%) - Branches: ~88% (threshold: 80%) - Functions: ~98% (threshold: 85%) diff --git a/.github/instructions/text-shaping.instructions.md b/.github/instructions/text-shaping.instructions.md index 7050a70..c93abdf 100644 --- a/.github/instructions/text-shaping.instructions.md +++ b/.github/instructions/text-shaping.instructions.md @@ -51,13 +51,13 @@ applyTo: "src/shaping/**" - Critical for Thai, Devanagari, Bengali, Tamil, and Vietnamese where combining marks get spatially repositioned ## BiDi Resolution (UAX #9) -- Simplified UBA: paragraph level detection (P2-P3), weak type resolution (W1-W7), neutral resolution (N1-N2) +- Simplified UBA: paragraph level detection (P2-P3), weak type resolution (W1-W7), neutral resolution (N1-N2), and — since v1.7.0 — implicit even-level embedding (I1/I2) so digit runs (EN/AN, incl. Extended Arabic-Indic) keep logical order inside RTL text - `BidiType` classification: L (Latin), R (Hebrew), AL (Arabic), EN, AN, ES, ET, CS, WS, ON, NSM, BN - Character classification order matters: check NSM/BN/AN/EN specific ranges BEFORE broad Arabic block (0x0600-06FF) - General Punctuation (U+2010–U+2027, U+2030–U+205E) classified as ON — covers dashes, quotes, ellipsis, primes - `resolveBidiRuns(text)`: main API — returns `BidiRun[]` in visual order (L2 reordering: runs reversed for RTL paragraphs so LTR text renders first at leftmost position) - `containsRTL(text)`: fast O(n) check for Arabic/Hebrew content -- Glyph mirroring via `MIRROR_MAP`: ~40 pairs (parentheses, brackets, guillemets, math symbols) +- Glyph mirroring via `BIDI_MIRRORING_PAIRS` (`bidi-mirroring-data.ts`): the complete 428-pair UCD `BidiMirroring.txt` table under rule L4 (since v1.7.0 — replaces the former ~40-pair `MIRROR_MAP`) - `reverseString()`: surrogate-pair safe reversal for RTL run reordering - Levels: 0 = LTR, 1 = RTL, 2 = LTR embedded in RTL diff --git a/.github/prompts/compliance-audit.prompt.md b/.github/prompts/compliance-audit.prompt.md index 8dead6d..2de2fee 100644 --- a/.github/prompts/compliance-audit.prompt.md +++ b/.github/prompts/compliance-audit.prompt.md @@ -65,7 +65,7 @@ Perform a comprehensive compliance audit of pdfnative. ### 10. Digital Signature Compliance (ISO 32000-1 §12.8) - `/ByteRange` covers entire PDF minus `/Contents` hex - CMS SignedData structure embeds X.509 certificate chain -- `/Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached` +- `/Filter /Adobe.PPKLite` with `/SubFilter /adbe.pkcs7.detached`, `/ETSI.CAdES.detached` (PAdES profile, v1.7.0), or `/ETSI.RFC3161` (document timestamps, v1.7.0) - RSA and ECDSA signatures produce valid ASN.1 DER output ### 11. Streaming Output Compliance diff --git a/.github/prompts/quality-gate.prompt.md b/.github/prompts/quality-gate.prompt.md index 5b08a8a..a2db685 100644 --- a/.github/prompts/quality-gate.prompt.md +++ b/.github/prompts/quality-gate.prompt.md @@ -10,7 +10,7 @@ Run the full pdfnative quality gate and report results. 1. `npm run typecheck:all` — verify zero TypeScript errors across src/, tests/, and scripts/ 2. `npm run lint` — verify zero ESLint warnings/errors -3. `npm run test` — run all unit tests (2396+ expected) +3. `npm run test` — run all unit tests (2665+ expected) 4. `npm run test:coverage` — verify coverage thresholds (88% statements / 80% branches / 85% functions / 90% lines) 5. `npm run build` — verify clean build (ESM + CJS + .d.ts) 6. Verify `dist/` output contains: `index.js`, `index.cjs`, `index.d.ts`, `worker/index.js`, `tools/index.js`, `tools/build-emoji-font.js` (the last two are required by package.json `exports`/`bin`) diff --git a/AGENTS.md b/AGENTS.md index 55ddf5a..fc66c08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Guidance for AI coding agents (Cursor, Aider, Claude Code, Continue, Zed, Cline, pdfnative is a **zero-runtime-dependency** TypeScript library that generates ISO 32000-1 (PDF 1.7) and ISO 19005 (PDF/A) compliant PDFs. Pure native — no Cairo, no PDFKit, no node-forge, no fontkit, no anything. -Quality bar: GAFAM-grade. 2664+ tests, 95%+ statement coverage measured at v1.6.0 (CI enforces ≥88%), blocking veraPDF validation in CI, SLSA provenance on npm. +Quality bar: GAFAM-grade. 2665+ tests, 95%+ statement coverage measured at v1.6.0 (CI enforces ≥88%), blocking veraPDF validation in CI, SLSA provenance on npm. Since v1.6.0 the parser side also **decrypts** (Standard Security Handler, RC4 + AES-128/256), **streams** page-tree merge/split/extract, **fills & flattens** AcroForms — including encrypted sources via encrypted incremental update — **extracts text** with positions (`extractText`), renders native **vector charts**, and **re-encrypts** output (`MergeOptions.encrypt`). All exported from `src/index.ts` like everything else. @@ -16,10 +16,10 @@ Since v1.6.0 the parser side also **decrypts** (Standard Security Handler, RC4 + ```bash npm run build # tsup → dist/ (ESM + CJS + .d.ts) -npm run test # vitest run (2664+ tests) +npm run test # vitest run (2665+ tests) npm run typecheck:all # src/ + tests/ + scripts/ npm run lint # eslint -npm run test:generate # produce ~241 sample PDFs → test-output/ +npm run test:generate # produce ~242 sample PDFs → test-output/ npm run validate:pdfa # local veraPDF run ``` diff --git a/README.md b/README.md index d46cdea..655f668 100644 --- a/README.md +++ b/README.md @@ -1026,7 +1026,7 @@ pdfnative ships as a library, but three official companion packages cover the mo **New in v1.2.0:** five new commands — **`merge`**, **`split`**, **`extract`** (page-tree manipulation via pdfnative 1.5.0), **`annotate`** (markup annotations via incremental save, so existing signatures stay intact), and **`govern`** (the AI-governance / Human-in-the-Loop contract: `govern rules` / `govern policy` / `govern verify-issue`, with a stable `E_POLICY` error code). Plus PDF bookmarks (`--outline auto` or an explicit tree), the bundled math font (`--font math`), layout introspection (`--inspect-layout` / `--debug-layout`), and native constant-time crypto by default in `sign` (opt out with `--pure-crypto`). -**New in v1.3.0:** five more commands on the pdfnative 1.7.0 engine — **`fill`** (fill, flatten, and export existing AcroForms via incremental save, encrypted PDFs included), **`encrypt`** / **`decrypt`** (AES-128/256 re-securing and password removal; RC4 never emitted), **`extract-text`** (reading-order Unicode text as text/JSON/NDJSON, `--runs`, `--password`), and **`doctor`** (offline environment preflight). Also native vector charts in `render`, `--password` + `--encrypt` re-encryption on merge/split/extract, an agent capability manifest (`schema manifest` + `llms.txt`), and PowerShell completion. +**New in v1.3.0:** five more commands on the pdfnative 1.6.0 engine — **`fill`** (fill, flatten, and export existing AcroForms via incremental save, encrypted PDFs included), **`encrypt`** / **`decrypt`** (AES-128/256 re-securing and password removal; RC4 never emitted), **`extract-text`** (reading-order Unicode text as text/JSON/NDJSON, `--runs`, `--password`), and **`doctor`** (offline environment preflight). Also native vector charts in `render`, `--password` + `--encrypt` re-encryption on merge/split/extract, an agent capability manifest (`schema manifest` + `llms.txt`), and PowerShell completion. ```bash # render with full layout coverage (encryption + watermark + PDF/A-2b) @@ -1066,7 +1066,7 @@ See the [CLI Guide](https://pdfnative.dev/guides/cli.html) for the full v1.3.0 r **v1.4.0:** adds `annotate_pdf` (markup via incremental update) and the network-free `draft_governance_issue` (**19 tools**), the MCP `prompts` capability (`governance_contract`, `draft_issue_workflow`), `pageLabels[]` in `inspect_pdf`, and the explicit `math` script — via the pdfnative 1.5.0 engine. -**v1.5.0:** adds `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf` (**24 tools**), `password` support on the read-only and page-tree tools, MCP resources (`pdfnative://output/…`), and tool annotations — via the pdfnative 1.7.0 engine. +**v1.5.0:** adds `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf` (**24 tools**), `password` support on the read-only and page-tree tools, MCP resources (`pdfnative://output/…`), and tool annotations — via the pdfnative 1.6.0 engine. **v1.6.0:** adds `add_ltv`, `timestamp_pdf`, `update_metadata`, `inspect_layout` (**28 tools**), all 13 block kinds in `generate_basic_pdf`, layout options and build-time `encrypt` on the document tools, image watermarks, print production, charts v2, honest PDF/A (`embedFonts` / `strict` / diagnostics), PAdES timestamps on `sign_pdf`, four new recipe prompts, and the MCP 2026-07-28 spec — via the pdfnative 1.7.0 engine. @@ -1098,7 +1098,7 @@ npx -y pdfnative-mcp | `extract_pages` | Pull an arbitrary, order-preserving page subset into a new PDF | | `annotate_pdf` | Overlay markup annotations (9 types) via incremental update; not a redaction | | `draft_governance_issue` | Governance-compliant GitHub-issue draft, network-free, never submits (HITL) | -| `add_chart` | Native vector charts, zero rasterisation — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log & time scales | +| `add_chart` | Native vector charts, zero rasterisation — nine types since v1.6.0 (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut), dual axis, log & time scales | | `read_form_fields` | List an existing AcroForm's fields (types, values, options) | | `fill_form` | Fill and optionally flatten an existing AcroForm (encrypted PDFs supported) | | `encrypt_pdf` | Re-secure with AES-128/AES-256, owner/user passwords + permissions | diff --git a/ROADMAP.md b/ROADMAP.md index 2c36479..cdd2826 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,7 +33,7 @@ This document outlines the planned development direction for pdfnative. Prioriti - [x] **Barcode & QR code generation** — Code 128 (ISO 15417), EAN-13 (ISO 15420), QR Code (ISO 18004), Data Matrix ECC 200 (ISO 16022), PDF417 (ISO 15438) — pure PDF path operators (no image dependency) - [x] **SVG path rendering** — 7 SVG element types (path, rect, circle, ellipse, line, polyline, polygon) rendered as native PDF path operators - [x] **Form fields** — AcroForm interactive fields (ISO 32000-1 §12.7): text, multiline, checkbox, radio, dropdown, listbox with appearance streams -- [x] **Digital signatures** — CMS/PKCS#7 detached signatures (ISO 32000-1 §12.8): RSA PKCS#1 v1.5 + ECDSA P-256 (`rsa-sha256` / `ecdsa-sha256`, the two selectable signature algorithms; SHA-384/512 are standalone hash exports), X.509 certificate parsing, zero-dependency crypto +- [x] **Digital signatures** — CMS/PKCS#7 detached signatures (ISO 32000-1 §12.8): RSA PKCS#1 v1.5 + ECDSA P-256 (`rsa-sha256` / `rsa-sha384` / `rsa-sha512` / `ecdsa-sha256`, the four selectable signature algorithms since v1.7.0), X.509 certificate parsing, zero-dependency crypto - [x] **Streaming output** — AsyncGenerator-based progressive PDF emission with configurable chunk size (64 KB default), validation for TOC/template incompatibility, concatChunks utility - [x] **PDF parser & modifier** — full PDF reader (tokenizer, object parser, xref table/stream, page tree, FlateDecode inflate) + incremental modification (non-destructive save with /Prev chain) - [x] **npm metadata enrichment** — description enumerates 16 scripts + headline features (BiDi, PDF/A, encryption, signatures, AcroForm, barcodes, SVG); keywords expanded to 27 entries for npm search discoverability (v1.0.2) diff --git a/SECURITY.md b/SECURITY.md index 66fd1d8..8b1a901 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,9 +14,9 @@ We will acknowledge receipt within 48 hours and target a fix within 7 days for C | Version | Supported | |---------|-----------| -| 1.6.x | ✅ | -| 1.5.x | ✅ (security fixes) | -| < 1.5 | ❌ | +| 1.7.x | ✅ | +| 1.6.x | ✅ (security fixes) | +| < 1.6 | ❌ | ## Security Model @@ -40,7 +40,7 @@ pdfnative is a pure TypeScript library with **zero runtime dependencies**. This ### Digital Signatures - CMS/PKCS#7 detached signatures (ISO 32000-1 §12.8) -- RSA PKCS#1 v1.5 (SHA-256) and ECDSA P-256 — pure TypeScript, zero external dependencies +- RSA PKCS#1 v1.5 (SHA-256, and SHA-384/512 since v1.7.0) and ECDSA P-256 (SHA-256) — pure TypeScript, zero external dependencies - X.509 DER certificate parsing for certificate chain embedding - `/ByteRange` ensures only the signature `/Contents` is excluded from the signed digest diff --git a/docs/guides/charts.html b/docs/guides/charts.html index edd6dd6..9462d0c 100644 --- a/docs/guides/charts.html +++ b/docs/guides/charts.html @@ -4,7 +4,7 @@ Charts (native vector) — pdfnative - + @@ -14,7 +14,7 @@ - + diff --git a/docs/guides/cli.md b/docs/guides/cli.md index e9708b2..e034074 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -1,6 +1,6 @@ # pdfnative-cli — Command-Line Interface Guide -> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on pdfnative 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). +> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) is the **official command-line interface** for the [`pdfnative`](https://github.com/Nizoka/pdfnative) library. It exposes 17 commands in five groups — create & edit (`render`, `fill`, `annotate`), page tree (`merge`, `split`, `extract`), security (`sign`, `verify`, `encrypt`, `decrypt`), read & extract (`inspect`, `extract-text`), and automation & meta (`batch`, `doctor`, `schema`, `completion`, `govern`) — that together cover the full document lifecycle from JSON to a signed, verified, archive-grade PDF, plus page-tree editing, markup annotations, and an AI-governance gate, with an agent-native automation contract for autonomous AI and CI pipelines. @@ -873,12 +873,12 @@ node samples/run-all.js ### What's new in v1.3.0 -v1.3.0 is built on **pdfnative 1.7.0** and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. +v1.3.0 is built on **pdfnative 1.6.0** and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. | Area | v1.2.0 | v1.3.0 | |---|---|---| | Commands | 12 commands | adds **`fill`** (fill / flatten / **export** AcroForms via an incremental save), **`encrypt`**, **`decrypt`**, **`extract-text`** (reading-order Unicode text: `text` \| `json` \| `ndjson`), and **`doctor`** (offline environment / capability preflight) | -| Charts | — | the pdfnative 1.7.0 `chart` document block (bar, barH, line, pie, donut) renders as pure vector path operators through `render` | +| Charts | — | the pdfnative 1.6.0 `chart` document block (bar, barH, line, pie, donut) renders as pure vector path operators through `render` | | Encryption vocabulary | `--encrypt-*` flags on `render` only | **unified vocabulary** — `--encrypt [aes-128\|aes-256]`, `--owner-password`, `--user-password`, `--permissions print,copy,modify,extract` — shared by `render`, `merge`, `split` and `extract` (legacy `--encrypt-*` flags kept as aliases) | | Page-tree commands | plaintext sources only | **`--password`** reads encrypted sources; **`--encrypt`** re-encrypts the output; **`--stream`** (+ `--chunk-size`) streams the output with constant memory | | Inspection | metadata, PDF/UA, annotations | adds `inspect --form-fields`, `--encryption` and `--password` | diff --git a/docs/guides/index.html b/docs/guides/index.html index 6e6aedd..ea0b899 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -359,7 +359,7 @@

    Documentation Guides

  • Charts (native vector) → - v1.6.0 bar, horizontal-bar, line, pie, and donut chart blocks rendered as pure PDF path operators — zero dependencies, multi-series, "nice" axis ticks, negative values, tagged /Figure + alt text, PDF/A-safe. + Nine chart kinds rendered as pure PDF path operators — bar, horizontal-bar, line, pie, donut (v1.6.0), plus stacked bars, area, and scatter with a secondary axis, log & time scales, and data labels (charts v2, v1.7.0). Zero dependencies, multi-series, negative values, tagged /Figure + alt text, PDF/A-safe.
  • Text extraction → diff --git a/docs/guides/ltv.md b/docs/guides/ltv.md index 1aff024..aff3a21 100644 --- a/docs/guides/ltv.md +++ b/docs/guides/ltv.md @@ -143,7 +143,7 @@ The repository ships a deterministic offline mock PKI (`scripts/helpers/mock-pki - `listSignatures(bytes)` — every entry, its SubFilter, ByteRange, placeholder/timestamp status. - Adobe Reader: *Signature Panel → Certificate Details* — trust the mock/company root, then look for **"LTV enabled"** on B-LT documents and a valid document timestamp on B-LTA. - `openssl ts -reply -in token.der -text` inspects extracted timestamp tokens. -- pdfnative itself ships **no signature verifier** — validation belongs to dedicated tooling (veraPDF for PDF/A, Adobe/DSS for signatures, `pdfnative-cli verify` for structural checks). +- pdfnative itself ships **no signature verifier** — validation belongs to dedicated tooling (veraPDF for PDF/A, Adobe/DSS for signatures, `pdfnative-cli verify` for full cryptographic verification: CMS signature values, certificate chain and trust, RFC 3161 timestamps, OCSP/CRL revocation). ## Limits & scope (v1.7.0) diff --git a/docs/guides/mcp.md b/docs/guides/mcp.md index 731830a..a1d2fb8 100644 --- a/docs/guides/mcp.md +++ b/docs/guides/mcp.md @@ -862,7 +862,7 @@ v1.4.0 upgrades the engine to **pdfnative 1.5.0**, brings the pdfnative **AI-gov ### What's new in v1.5.0 -v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. +v1.5.0 upgrades the engine to **pdfnative 1.6.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. - **Five new tools:** - `add_chart` — bar, horizontal-bar, line, pie and donut charts drawn as **native PDF vector paths**. No rasterisation and no image round-trip, so the output stays sharp at any zoom and tags as `/Figure` with alt text. If you were previously asking an agent to render a chart to PNG and push it through `embed_image`, stop — this replaces that. diff --git a/docs/guides/react.md b/docs/guides/react.md index 51708fb..fd1977c 100644 --- a/docs/guides/react.md +++ b/docs/guides/react.md @@ -1,6 +1,6 @@ # pdfnative-react — Declarative JSX Renderer Guide -> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on pdfnative 1.7.0), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). +> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on pdfnative 1.6.0), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies (semver-accepts 1.7.0). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). [`pdfnative-react`](https://github.com/Nizoka/pdfnative-react) turns declarative **JSX** into real, on-device PDFs powered by the zero-dependency [`pdfnative`](https://github.com/Nizoka/pdfnative) engine — no DOM, no headless browser, no SaaS round-trips. Your documents never leave the process. diff --git a/docs/index.html b/docs/index.html index a79ec21..2b5a9e3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -456,7 +456,7 @@

    30-Second Start

    Four adoption paths — pick the one that fits your stack. All four are zero-config and zero-build.

    -

    Library v1.6.0

    +

    Library v1.7.0

    For TypeScript / JavaScript apps.

    npm install pdfnative
    import { buildDocumentPDFBytes, extractText } from 'pdfnative';
    @@ -793,7 +793,7 @@ 

    28 Production Tools

    extract_pagesPull an arbitrary, order-preserving page subset (max 5000) into a new PDF (v1.3.0). annotate_pdfOverlay markup annotations (highlight / underline / note / shape) on an existing PDF via incremental update. A visual review layer, not a redaction (v1.4.0). draft_governance_issueAssemble a governance-compliant GitHub-issue draft locally — network-free by construction; never submits (v1.4.0). - add_chartRender charts as native PDF vector paths — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log & time scales; no rasterisation (v1.5.0). + add_chartRender charts as native PDF vector paths — nine types since v1.6.0 (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut), dual axis, log & time scales; no rasterisation (v1.5.0). read_form_fieldsList an existing AcroForm's fields with their types, current values and options — the read half of the fill round-trip (v1.5.0). fill_formFill AcroForm field values and optionally flatten them into static page content (v1.5.0). encrypt_pdfRe-secure an existing PDF with AES-128 or AES-256, setting owner/user passwords and permissions (v1.5.0). diff --git a/docs/learn/08-next-steps.html b/docs/learn/08-next-steps.html index 9b3d2d7..24fdb9b 100644 --- a/docs/learn/08-next-steps.html +++ b/docs/learn/08-next-steps.html @@ -141,7 +141,7 @@

    Common next requirements

    Try things without installing anything

    Nine playgrounds run in your browser: 22 writing systems in one document, native charts, the full toolkit, a CLI command builder, an MCP tool explorer, the React renderer, and 100,000 pages.

    -

    All 26 reference guides are one click away. If something in them does not hold up, say so — a claim we cannot defend is a bug.

    +

    All 28 reference guides are one click away. If something in them does not hold up, say so — a claim we cannot defend is a bug.

    You are done. You know the model, the delivery paths and where the reference material lives. diff --git a/docs/learn/index.html b/docs/learn/index.html index 606a97e..23c2db1 100644 --- a/docs/learn/index.html +++ b/docs/learn/index.html @@ -162,7 +162,7 @@

    Learn pdfnative

    Eight short steps, in order, from installing the package to shipping a hundred-thousand-page document. Each one introduces exactly one idea and says what you should be able to do at the end of it.

    About thirty minutes, start to finish. You need Node.js 22 or newer and nothing else — no account, no key, no service.

    -

    Already know what you want? The 26 reference guides are the shelf; this is the path.

    +

    Already know what you want? The 28 reference guides are the shelf; this is the path.

    1. 1. Install pdfnative and make one PDF

      Ten lines, one file on disk. Nothing else.

    2. diff --git a/docs/llms.txt b/docs/llms.txt index 4713efd..b9a4368 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -5,7 +5,7 @@ ## Docs - [README](https://github.com/Nizoka/pdfnative/blob/main/README.md): Full feature list, install, quick start, API surface, examples. -- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). +- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.7: PAdES LTV B-B → B-LTA, charts v2, colour-emoji flag/ZWJ sequences, UAX #9 conformance, print production, PDF/A declaration guards, `updateMetadata`; v1.6: parser-side decryptor, streaming merge/split/extract, fill & flatten AcroForms, native vector charts, `extractText`, re-encrypt output; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). - [CHANGELOG](https://github.com/Nizoka/pdfnative/blob/main/CHANGELOG.md): Per-version changes. - [Release notes v1.7.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.7.0.md): PAdES LTV signing B-B → B-LTA (`signPdfBytesWithTimestamp` RFC 3161 timestamps, `addValidationInfo` `/DSS` + `/VRI` OCSP/CRL material, `addDocumentTimestamp`, `listSignatures`, multi-signature via `addSignaturePlaceholder({ allowMultiple })` — network transport injected through `setTimestampProvider`/`setRevocationProvider`, the engine never opens a socket), charts v2 (stackedBar/stackedBarH/area/scatter, secondary right axis, log + UTC time scales, data labels, x-label collision handling), colour-emoji flag & ZWJ sequences (51 flags + 22 ZWJ as COLR ligatures), UAX #9 conformance fixes (RTL digit order, glyph mirroring, ALEF joining, Persian presentation forms), PDF/A declaration guards (`strict`/`onDiagnostic`), incremental writer/xref hardening, `PdfModifier.updateMetadata()`. - [Release notes v1.6.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.6.0.md): Standard Security Handler reader/decryptor (`openPdf(bytes, { password })`, RC4/AES-128/AES-256; `PdfSourceInput`/`MergeOptions.password` for encrypted merge/split sources), streaming page-tree manipulation (`streamMergedPdfs`/`streamSplitPdf`/`streamExtractPages`), fill & flatten existing AcroForm PDFs (`readFormFields`/`fillForm`/`flattenForm`) — encrypted documents included (encrypted incremental update), native vector charts (`chart` block: bar/barH/line/pie/donut), text extraction (`extractText`: per-page reading-order text + positioned runs, encrypted PDFs supported), re-encrypt merge/split output (`MergeOptions.encrypt`, AES-128/256), expanded colour-emoji subset (221 → 1167 incl. complete Transport & Map). Fixes: spec-compliant R6 hash + all-strings encryption on the writer; incremental trailer carries `/Encrypt` forward. @@ -25,7 +25,7 @@ - [FAQ](https://pdfnative.dev/guides/faq.html): Custom fonts, encryption, BiDi, workers, vs other libs. - [Troubleshooting](https://pdfnative.dev/guides/troubleshooting.html): Missing glyphs, RTL appearing backwards, oversized PDFs, parser errors. - [Colour-emoji CLI](https://pdfnative.dev/guides/colour-emoji-cli.html): `npx pdfnative-build-emoji-font` — generate a colour-emoji data module covering any glyph subset up to the full ~3,600-glyph Noto Color Emoji set, plus flag & ZWJ sequences via `--sequences` / `--sequence-list`; pinned download + SHA-256 verification. -- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bars, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. +- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. - [Text extraction](https://pdfnative.dev/guides/text-extraction.html): `extractText()` — reading-order Unicode via `/ToUnicode`, optional positioned runs, works on encrypted PDFs with a password. The RAG / agent-ingestion entry point. - [Form filling](https://pdfnative.dev/guides/form-filling.html): `readFormFields()` / `fillForm()` / `flattenForm()` on existing AcroForms, including encrypted PDFs via incremental update. - [PDF manipulation](https://pdfnative.dev/guides/pdf-manipulation.html): `openPdf()` (with `{ password }`), merge / split / extract pages, re-encrypt on output, and their streaming variants. @@ -49,7 +49,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). - [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. -- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. +- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.6.0 (pins `^1.6.0`, semver-accepts 1.7.0). - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. ## Source @@ -60,6 +60,6 @@ ## Ecosystem -- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. +- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). diff --git a/docs/playgrounds/charts.html b/docs/playgrounds/charts.html index 0d110d6..9e6e4fd 100644 --- a/docs/playgrounds/charts.html +++ b/docs/playgrounds/charts.html @@ -107,7 +107,7 @@
      -

      Home  ›  Playgrounds  ›  Charts (v1.6.0)

      +

      Home  ›  Playgrounds  ›  Charts (v1.6.0 → v1.7.0)

      -

      Native vector charts — new in 1.6.0

      +

      Native vector charts — since 1.6.0, charts v2 in 1.7.0

      pdfnative's chart block renders bar, horizontal-bar, line, pie, and donut charts — and, since 1.7.0, stacked bars, area, and scatter — as pure PDF path operators: rectangles, line segments, and cubic-Bézier arcs. Zero dependencies, no rasterisation, crisp at any zoom, and PDF/A-safe. Pick a preset, generate, and open the downloaded PDF in any viewer.

      diff --git a/docs/playgrounds/index.html b/docs/playgrounds/index.html index c5c7c6a..af182f4 100644 --- a/docs/playgrounds/index.html +++ b/docs/playgrounds/index.html @@ -13,7 +13,7 @@ - + @@ -206,10 +206,10 @@

      PDF Toolkit Library engin -

      Charts Library 1.6.0

      -

      New in 1.6.0: native vector charts — bar, horizontal-bar, line (with markers), pie, and donut — rendered as pure PDF path operators. Multi-series, legends, "nice" axis ticks, negative values, and tagged /Figure alt text. Each preset is a one-click downloadable PDF.

      +

      Charts Library 1.6.0 → 1.7.0

      +

      Native vector charts — nine kinds: bar, horizontal-bar, line (with markers), pie, donut (v1.6.0), plus stacked bars, area, and scatter with a secondary axis, log & time scales, and data labels (charts v2, v1.7.0) — rendered as pure PDF path operators. Multi-series, legends, "nice" axis ticks, negative values, and tagged /Figure alt text. Each preset is a one-click downloadable PDF.

      - BarLinePie/DonutZero-dep + BarLinePie/DonutStacked/Area/ScatterZero-dep
      Open Charts playground →
      diff --git a/docs/playgrounds/mcp.html b/docs/playgrounds/mcp.html index 2d22d88..b869111 100644 --- a/docs/playgrounds/mcp.html +++ b/docs/playgrounds/mcp.html @@ -305,7 +305,7 @@

      Resources

      }, { id: 'add_chart', - purpose: 'Render a chart as native PDF vector paths — nine types since v1.6.0 (bar, stacked, line, area, scatter, pie, donut), dual axis, log and time scales; no rasterisation, no image round-trip.', + purpose: 'Render a chart as native PDF vector paths — nine types since v1.6.0 (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut), dual axis, log and time scales; no rasterisation, no image round-trip.', prompt: 'Chart our Q1-Q4 revenue against cost as a bar chart, and tag it for accessibility.', args: { title: 'FY2026 performance', diff --git a/docs/responsibility.html b/docs/responsibility.html index 08eb2cb..d0e82dc 100644 --- a/docs/responsibility.html +++ b/docs/responsibility.html @@ -103,7 +103,7 @@

      Efficiency by architecture

    -

    The library never opens a socket. No telemetry endpoint, no auto-update channel, no remote font fetch. Searching the engine source for any of the ways JavaScript can reach the network — fetch, XMLHttpRequest, WebSocket, node:http, sendBeacon — returns no matches at all.

    +

    The library never opens a socket. No telemetry endpoint, no auto-update channel, no remote font fetch. Searching the engine source for any of the ways JavaScript can reach the network — fetch, XMLHttpRequest, WebSocket, node:http, sendBeacon — finds no call sites in executable code. The only matches are documentation comments: the @example blocks of the injectable TimestampProvider / RevocationProvider interfaces (v1.7.0), which show how your code can supply network transport for LTV — the engine itself never performs the request.

    One scoped exception, stated because it is real: pdfnative-cli verify can fetch OCSP and CRL responses when checking whether a signing certificate has been revoked. It does not do so by default — the default reads only the revocation data already embedded in the document, and network checks require --revocation online. RFC 3161 timestamps are validated from the embedded token; the CLI does not contact a timestamp authority, and sign --timestamp reports that it is not yet implemented.

    Verify it: grep -rE "fetch\(|XMLHttpRequest|WebSocket|node:http" src/ · SECURITY.md

    @@ -179,7 +179,7 @@

    What we deliberately do not claim

  • "Lighter than library X" as an environmental argument. The feature comparison compares capabilities. There is no committed benchmark comparing runtime or bundle size against other libraries, so there is no basis for an efficiency claim relative to them.
  • A single headline bundle size. The built artefacts in dist/ are unminified and ship with sourcemaps, so quoting their raw byte count would overstate the cost as badly as quoting a gzipped figure would understate it. Measure it for your own bundle.
  • "Zero dependencies" across the ecosystem. True for the engine. Not true for pdfnative-react or pdfnative-mcp, as stated above.
  • -
  • "Never touches the network" across the ecosystem. True for the engine. The CLI's verify command makes OCSP, CRL and timestamp requests by design.
  • +
  • "Never touches the network" across the ecosystem. True for the engine. The CLI's verify command can make OCSP and CRL requests when --revocation online is passed, and pdfnative-mcp's PAdES tools reach only the operator-configured TSA / OCSP / CRL endpoints — by design in both cases.
  • Certification of any kind. PDF/A output is validated by the veraPDF reference validator in CI, which is meaningful evidence — but pdfnative is not an accredited conformance-tested product, and there is no ISO 14001, SOC 2, HIPAA or GDPR certification behind it. Software architecture is not a compliance certificate.
  • WCAG conformance of this website. The site has a skip link, visible focus indicators, reduced-motion support and contrast ratios checked in CI. It has not been through a formal accessibility audit, so we describe the measures rather than claim the outcome.
  • diff --git a/llms.txt b/llms.txt index 4713efd..b9a4368 100644 --- a/llms.txt +++ b/llms.txt @@ -5,7 +5,7 @@ ## Docs - [README](https://github.com/Nizoka/pdfnative/blob/main/README.md): Full feature list, install, quick start, API surface, examples. -- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). +- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.7: PAdES LTV B-B → B-LTA, charts v2, colour-emoji flag/ZWJ sequences, UAX #9 conformance, print production, PDF/A declaration guards, `updateMetadata`; v1.6: parser-side decryptor, streaming merge/split/extract, fill & flatten AcroForms, native vector charts, `extractText`, re-encrypt output; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). - [CHANGELOG](https://github.com/Nizoka/pdfnative/blob/main/CHANGELOG.md): Per-version changes. - [Release notes v1.7.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.7.0.md): PAdES LTV signing B-B → B-LTA (`signPdfBytesWithTimestamp` RFC 3161 timestamps, `addValidationInfo` `/DSS` + `/VRI` OCSP/CRL material, `addDocumentTimestamp`, `listSignatures`, multi-signature via `addSignaturePlaceholder({ allowMultiple })` — network transport injected through `setTimestampProvider`/`setRevocationProvider`, the engine never opens a socket), charts v2 (stackedBar/stackedBarH/area/scatter, secondary right axis, log + UTC time scales, data labels, x-label collision handling), colour-emoji flag & ZWJ sequences (51 flags + 22 ZWJ as COLR ligatures), UAX #9 conformance fixes (RTL digit order, glyph mirroring, ALEF joining, Persian presentation forms), PDF/A declaration guards (`strict`/`onDiagnostic`), incremental writer/xref hardening, `PdfModifier.updateMetadata()`. - [Release notes v1.6.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.6.0.md): Standard Security Handler reader/decryptor (`openPdf(bytes, { password })`, RC4/AES-128/AES-256; `PdfSourceInput`/`MergeOptions.password` for encrypted merge/split sources), streaming page-tree manipulation (`streamMergedPdfs`/`streamSplitPdf`/`streamExtractPages`), fill & flatten existing AcroForm PDFs (`readFormFields`/`fillForm`/`flattenForm`) — encrypted documents included (encrypted incremental update), native vector charts (`chart` block: bar/barH/line/pie/donut), text extraction (`extractText`: per-page reading-order text + positioned runs, encrypted PDFs supported), re-encrypt merge/split output (`MergeOptions.encrypt`, AES-128/256), expanded colour-emoji subset (221 → 1167 incl. complete Transport & Map). Fixes: spec-compliant R6 hash + all-strings encryption on the writer; incremental trailer carries `/Encrypt` forward. @@ -25,7 +25,7 @@ - [FAQ](https://pdfnative.dev/guides/faq.html): Custom fonts, encryption, BiDi, workers, vs other libs. - [Troubleshooting](https://pdfnative.dev/guides/troubleshooting.html): Missing glyphs, RTL appearing backwards, oversized PDFs, parser errors. - [Colour-emoji CLI](https://pdfnative.dev/guides/colour-emoji-cli.html): `npx pdfnative-build-emoji-font` — generate a colour-emoji data module covering any glyph subset up to the full ~3,600-glyph Noto Color Emoji set, plus flag & ZWJ sequences via `--sequences` / `--sequence-list`; pinned download + SHA-256 verification. -- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bars, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. +- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. - [Text extraction](https://pdfnative.dev/guides/text-extraction.html): `extractText()` — reading-order Unicode via `/ToUnicode`, optional positioned runs, works on encrypted PDFs with a password. The RAG / agent-ingestion entry point. - [Form filling](https://pdfnative.dev/guides/form-filling.html): `readFormFields()` / `fillForm()` / `flattenForm()` on existing AcroForms, including encrypted PDFs via incremental update. - [PDF manipulation](https://pdfnative.dev/guides/pdf-manipulation.html): `openPdf()` (with `{ password }`), merge / split / extract pages, re-encrypt on output, and their streaming variants. @@ -49,7 +49,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). - [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. -- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. +- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.6.0 (pins `^1.6.0`, semver-accepts 1.7.0). - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. ## Source @@ -60,6 +60,6 @@ ## Ecosystem -- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. +- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). From 103802d308d5827947d6bff1048db4a09bbda2ec Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:19:47 +0200 Subject: [PATCH 05/15] docs: surface the 1.7.0 features the guide vitrines and agent files omitted The audit''s second class of findings: pages whose facts were not wrong but whose framing predated 1.7.0. The guide index cards and HTML shell metas for signatures (PAdES profile, RSA SHA-384/512, multiple signatures, listSignatures), colour emoji (sweep gradients, the 73 flag/ZWJ sequences, --sequences / --sequence-list), PDF/A (declaration guards, dropping the "v1.1.0" framing) and the CLI (all 17 commands, verify with RFC 3161 + OCSP/CRL) now describe the current release. The landing page gains the print-production sentence in the Rich Content card, print in the #library JSON-LD description, the emoji-sequences line in the scripts card, and print.html / ltv.html in the footer guide list. Four guides gain the 1.7.0 note their subject now requires: form-filling (AcroForm /Helv carries /ToUnicode in every mode - searchable form text, byte change on every form document), text-extraction (complete base-14 and form-field coverage), pdf-manipulation (page boxes and /UserUnit survive merge/split/extract), accessibility (PDF/A declaration guards). Agent instruction files catch up with the 1.7.0 API surface: api-design.instructions.md gains the LTV/PAdES, print-production and PDF/A-diagnostics export categories; pdfa-conformance.instructions.md gains the "Declaration guards (v1.7.0)" section AGENTS.md routes to; copilot-instructions.md names all nine playgrounds. The "built on pdfnative 1.6.0" phrasings introduced by the audit fixes are restated as "the pdfnative 1.6 engine (pins ^1.6.0, semver-accepts 1.7.0)" so the version-token rule keeps guarding real drift instead of being suppressed - the pin carries the exact floor. llms.txt (root + docs, byte-identical) follows, llms-full.txt is regenerated, and sitemap lastmod moves to 2026-08-25 for the newly touched pages. verify:docs 17 rules green; vitest docs suite 18/18. --- .github/copilot-instructions.md | 2 +- .../instructions/api-design.instructions.md | 5 +- .../pdfa-conformance.instructions.md | 16 +++++++ README.md | 2 +- docs/guides/accessibility.md | 9 ++++ docs/guides/cli.html | 4 +- docs/guides/cli.md | 6 +-- docs/guides/colour-emoji.html | 6 +-- docs/guides/form-filling.md | 8 ++++ docs/guides/index.html | 8 ++-- docs/guides/pdf-manipulation.md | 5 ++ docs/guides/pdfa.html | 6 +-- docs/guides/react.md | 2 +- docs/guides/signatures.html | 6 +-- docs/guides/text-extraction.md | 5 ++ docs/index.html | 8 ++-- docs/llms-full.txt | 47 +++++++++++++++---- docs/llms.txt | 4 +- docs/sitemap.xml | 22 ++++----- llms.txt | 4 +- 20 files changed, 125 insertions(+), 50 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 94d0b7b..82c849c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -80,7 +80,7 @@ test-output/extreme/ # Visual regression baselines for extreme scripts (extreme tests/ # 2665+ tests (122 files: unit/integration/fuzz/parser/visual) mirroring src/ structure bench/ # Performance benchmarks (vitest bench) docs/ # GitHub Pages landing site (pdfnative.dev) — pure HTML/CSS/JS, zero build deps - └── playgrounds/ # Interactive browser playgrounds (extreme-scripts.html, scale.html) + └── playgrounds/ # 9 interactive browser playgrounds (extreme-scripts, all-scripts, toolkit, charts, authoring-plus, scale, cli, mcp, react) ``` - **Single entry point**: `src/index.ts` re-exports everything. All public API surfaces live there. diff --git a/.github/instructions/api-design.instructions.md b/.github/instructions/api-design.instructions.md index cd1023a..6c9775f 100644 --- a/.github/instructions/api-design.instructions.md +++ b/.github/instructions/api-design.instructions.md @@ -48,7 +48,10 @@ applyTo: "src/index.ts" 17. Core — Digital Signatures (`buildSigDict`, `signPdfBytes`, `estimateContentsSize`, `addSignaturePlaceholder`, `PdfSignOptions`, `SigDictMetadata`) 18. Core — Streaming (`buildPDFStream`, `buildDocumentPDFStream`, `buildPDFStreamTrue`, `buildDocumentPDFStreamTrue`, `buildPDFStreamPageByPage`, `buildDocumentPDFStreamPageByPage`, `streamToFile`, `StreamOptions`) 19. Crypto — Primitives (`sha384`, `sha512`, `hmacSha256`, `rsaSign`, `ecdsaSign`, `parseCertificate`, `buildCmsSignedData`, `setCryptoProvider`) -20. Parser — Read & Modify (`openPdf`/`PdfReader`, `createModifier`/`PdfModifier`, `createTokenizer`/`PdfTokenizer`, `parseValue`, `parseIndirectObject`, `parseXrefTable`, `PdfValue`, `ParsedDict`, `PdfRef`, `extractText`, `readFormFields`-family lives in core, `mergePdfs`/`splitPdf`/`extractPages` + streaming variants, `validatePdfUA`, decode filters) +20. Parser — Read & Modify (`openPdf`/`PdfReader`, `createModifier`/`PdfModifier`, `createTokenizer`/`PdfTokenizer`, `parseValue`, `parseIndirectObject`, `parseXrefTable`, `PdfValue`, `ParsedDict`, `PdfRef`, `extractText`, `readFormFields`-family lives in core, `mergePdfs`/`splitPdf`/`extractPages` + streaming variants, `validatePdfUA`, decode filters, `PdfModifier.updateMetadata` (v1.7.0)) +21. Core — LTV / PAdES (v1.7.0: `signPdfBytesWithTimestamp`, `collectValidationInfo`, `embedValidationInfo`, `addValidationInfo`, `addDocumentTimestamp`, `listSignatures`, `vriKeyForContents`, `buildDocTimeStampDict`; injected transports: `setTimestampProvider`/`getTimestampProvider`, `setRevocationProvider`/`getRevocationProvider`, `TimestampProvider`, `RevocationProvider` — the engine itself never opens a socket) +22. Core — Print Production (v1.7.0: `layout.print` page boxes + `bleed` shorthand + `marks`, `/Trapped` metadata with XMP parity, custom OutputIntent ICC, `/UserUnit`, print-dialog `viewerPreferences`: `duplex`, `pickTrayByPDFSize`, `printPageRange`, `numCopies`) +23. Core — PDF/A Diagnostics (v1.7.0: `strict`, `onDiagnostic`, `PdfDiagnostic`, `PdfDiagnosticCode` — codes `PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, `PDFA_DEVICE_CMYK_IMAGE`) ## Parser Option-Type Precedent - Parser-module option/result types live in the module itself (`MergeOptions`, diff --git a/.github/instructions/pdfa-conformance.instructions.md b/.github/instructions/pdfa-conformance.instructions.md index cba5e4f..79bcce0 100644 --- a/.github/instructions/pdfa-conformance.instructions.md +++ b/.github/instructions/pdfa-conformance.instructions.md @@ -74,6 +74,22 @@ applyTo: 'src/core/pdf-tags.ts,src/core/pdf-builder.ts,src/core/pdf-document.ts, embedded fonts. Do not reintroduce unembedded base-14 references in any PDF/A code path. +## Declaration guards (v1.7.0) + +- Configurations that would break the declared PDF/A level surface a + diagnostic through `src/core/pdf-diagnostics.ts`: `console.warn` by default + (deduplicated once per code), a caller-supplied `onDiagnostic` sink, or a + thrown error under `strict: true` (`onDiagnostic` is ignored when `strict` + is set — diagnostics throw instead). +- Current codes (stable, additions-only union `PdfDiagnosticCode`): + `PDFA_NO_FONT_ENTRIES` (base-14 text without embedded fonts under a claim), + `PDFA_UNEMBEDDED_FORM_FONT` (any form field under a claim — the AcroForm + `/DR /Helv` is an unembedded Type 1), `PDFA_DEVICE_CMYK_IMAGE` (CMYK JPEG + against the sRGB OutputIntent). +- When adding a new guard: extend the union (never remove or rename a code), + include the remedy in the message, and cover both the warn path and the + `strict` throw path in tests. + ## Validator workflow 1. `npm run test:generate` — regenerate `test-output/`. diff --git a/README.md b/README.md index 655f668..df5eecd 100644 --- a/README.md +++ b/README.md @@ -1026,7 +1026,7 @@ pdfnative ships as a library, but three official companion packages cover the mo **New in v1.2.0:** five new commands — **`merge`**, **`split`**, **`extract`** (page-tree manipulation via pdfnative 1.5.0), **`annotate`** (markup annotations via incremental save, so existing signatures stay intact), and **`govern`** (the AI-governance / Human-in-the-Loop contract: `govern rules` / `govern policy` / `govern verify-issue`, with a stable `E_POLICY` error code). Plus PDF bookmarks (`--outline auto` or an explicit tree), the bundled math font (`--font math`), layout introspection (`--inspect-layout` / `--debug-layout`), and native constant-time crypto by default in `sign` (opt out with `--pure-crypto`). -**New in v1.3.0:** five more commands on the pdfnative 1.6.0 engine — **`fill`** (fill, flatten, and export existing AcroForms via incremental save, encrypted PDFs included), **`encrypt`** / **`decrypt`** (AES-128/256 re-securing and password removal; RC4 never emitted), **`extract-text`** (reading-order Unicode text as text/JSON/NDJSON, `--runs`, `--password`), and **`doctor`** (offline environment preflight). Also native vector charts in `render`, `--password` + `--encrypt` re-encryption on merge/split/extract, an agent capability manifest (`schema manifest` + `llms.txt`), and PowerShell completion. +**New in v1.3.0:** five more commands on the pdfnative 1.6 engine — **`fill`** (fill, flatten, and export existing AcroForms via incremental save, encrypted PDFs included), **`encrypt`** / **`decrypt`** (AES-128/256 re-securing and password removal; RC4 never emitted), **`extract-text`** (reading-order Unicode text as text/JSON/NDJSON, `--runs`, `--password`), and **`doctor`** (offline environment preflight). Also native vector charts in `render`, `--password` + `--encrypt` re-encryption on merge/split/extract, an agent capability manifest (`schema manifest` + `llms.txt`), and PowerShell completion. ```bash # render with full layout coverage (encryption + watermark + PDF/A-2b) diff --git a/docs/guides/accessibility.md b/docs/guides/accessibility.md index 6de79fd..2589246 100644 --- a/docs/guides/accessibility.md +++ b/docs/guides/accessibility.md @@ -110,6 +110,15 @@ For a real-world test, open your PDF with **NVDA** (Windows, free), **VoiceOver* Contributions to address these are welcome — see the [Roadmap](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md). +> **PDF/A declaration guards** _(v1.7.0)_. Tagged documents that also claim +> PDF/A are now checked at build time: configurations that would break the +> declared level (base-14 text without embedded fonts, form fields under a +> claim, DeviceCMYK images against the sRGB OutputIntent) surface a diagnostic +> — `console.warn` by default, a custom `onDiagnostic` sink, or a thrown error +> under `strict: true`. Base-14 dictionaries under tagged mode also gain full +> `/ToUnicode` coverage, which changes output bytes versus 1.6.0. Details in +> the [PDF/A guide](pdfa.html). + ## Testing your PDFs are accessible A pragmatic checklist for solo developers: diff --git a/docs/guides/cli.html b/docs/guides/cli.html index 5898770..f26a94b 100644 --- a/docs/guides/cli.html +++ b/docs/guides/cli.html @@ -4,7 +4,7 @@ pdfnative-cli — Command-Line Interface Guide - + @@ -15,7 +15,7 @@ - + diff --git a/docs/guides/cli.md b/docs/guides/cli.md index e034074..c2effa0 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -1,6 +1,6 @@ # pdfnative-cli — Command-Line Interface Guide -> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). +> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) is the **official command-line interface** for the [`pdfnative`](https://github.com/Nizoka/pdfnative) library. It exposes 17 commands in five groups — create & edit (`render`, `fill`, `annotate`), page tree (`merge`, `split`, `extract`), security (`sign`, `verify`, `encrypt`, `decrypt`), read & extract (`inspect`, `extract-text`), and automation & meta (`batch`, `doctor`, `schema`, `completion`, `govern`) — that together cover the full document lifecycle from JSON to a signed, verified, archive-grade PDF, plus page-tree editing, markup annotations, and an AI-governance gate, with an agent-native automation contract for autonomous AI and CI pipelines. @@ -873,12 +873,12 @@ node samples/run-all.js ### What's new in v1.3.0 -v1.3.0 is built on **pdfnative 1.6.0** and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. +v1.3.0 is built on the **pdfnative 1.6 engine** (pin `^1.6.0`) and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. | Area | v1.2.0 | v1.3.0 | |---|---|---| | Commands | 12 commands | adds **`fill`** (fill / flatten / **export** AcroForms via an incremental save), **`encrypt`**, **`decrypt`**, **`extract-text`** (reading-order Unicode text: `text` \| `json` \| `ndjson`), and **`doctor`** (offline environment / capability preflight) | -| Charts | — | the pdfnative 1.6.0 `chart` document block (bar, barH, line, pie, donut) renders as pure vector path operators through `render` | +| Charts | — | the engine's `chart` document block (bar, barH, line, pie, donut — the pdfnative 1.6 set) renders as pure vector path operators through `render` | | Encryption vocabulary | `--encrypt-*` flags on `render` only | **unified vocabulary** — `--encrypt [aes-128\|aes-256]`, `--owner-password`, `--user-password`, `--permissions print,copy,modify,extract` — shared by `render`, `merge`, `split` and `extract` (legacy `--encrypt-*` flags kept as aliases) | | Page-tree commands | plaintext sources only | **`--password`** reads encrypted sources; **`--encrypt`** re-encrypts the output; **`--stream`** (+ `--chunk-size`) streams the output with constant memory | | Inspection | metadata, PDF/UA, annotations | adds `inspect --form-fields`, `--encryption` and `--password` | diff --git a/docs/guides/colour-emoji.html b/docs/guides/colour-emoji.html index 69be57e..8270fd4 100644 --- a/docs/guides/colour-emoji.html +++ b/docs/guides/colour-emoji.html @@ -4,7 +4,7 @@ Colour emoji (COLRv1) — pdfnative - + @@ -14,13 +14,13 @@ - + - + diff --git a/docs/guides/form-filling.md b/docs/guides/form-filling.md index abc28ce..53ad48b 100644 --- a/docs/guides/form-filling.md +++ b/docs/guides/form-filling.md @@ -135,6 +135,14 @@ Both operations use the incremental modifier rewritten — the update is appended after the existing body with a new xref and a `/Prev` chain. That is why signatures on earlier revisions stay valid. +> **Searchable form text** _(v1.7.0)_. The AcroForm `/Helv` font dictionary now +> carries a `/ToUnicode` CMap in every mode, so text typed into form fields is +> searchable and extractable (see the [text extraction guide](text-extraction.html)). +> Because of that CMap, **every form-carrying document changes bytes** compared +> to v1.6.0 output (about 20 bytes) — a deliberate correctness fix, recorded in +> the v1.7.0 release notes. Under a PDF/A claim, a form field also raises the +> `PDFA_UNEMBEDDED_FORM_FONT` diagnostic (see the [PDF/A guide](pdfa.html)). + ## See also - [Signatures](signatures.html) — CMS/PKCS#7 signing diff --git a/docs/guides/index.html b/docs/guides/index.html index ea0b899..147fc84 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -315,7 +315,7 @@

    Documentation Guides

  • PDF/A conformance → - PDF/A-1b, 2b, 2u, 3b — validated against the veraPDF reference validator in CI on every commit. + PDF/A-1b, 2b, 2u, 3b — validated against the veraPDF reference validator in CI on every commit. v1.7.0 adds declaration guards: strict, onDiagnostic, and the PDFA_* diagnostic codes for configurations that would break the claimed level.
  • Print production → @@ -323,7 +323,7 @@

    Documentation Guides

  • Digital signatures → - CMS/PKCS#7 detached signatures — RSA + ECDSA P-256, one-call addSignaturePlaceholder() workflow, Adobe Reader / openssl-cms validation. + CMS/PKCS#7 detached signatures — RSA SHA-256/384/512 + ECDSA P-256, one-call addSignaturePlaceholder() workflow, Adobe Reader / openssl-cms validation. v1.7.0 adds the PAdES baseline profile (profile: 'pades'), multiple signatures (allowMultiple + fieldName), listSignatures(), and the LTV ladder to B-LTA.
  • Long-term validation (LTV) → @@ -335,11 +335,11 @@

    Documentation Guides

  • Colour emoji → - v1.3.0 COLRv1 colour emoji — opt-in Noto Color Emoji subset, solid + linear + radial gradient layers rendered as native PDF Form XObjects. Monochrome fallback when not registered. + v1.3.0 COLRv1 colour emoji — opt-in Noto Color Emoji subset, solid + linear + radial gradient layers (plus v1.4.0 sweep gradients and blend modes) rendered as native PDF Form XObjects. v1.7.0 adds 73 flag & ZWJ sequences (51 flags + 22 ZWJ) resolved as single colour ligature glyphs. Monochrome fallback when not registered.
  • Colour-emoji font CLI → - v1.4.0 pdfnative-build-emoji-font — bundled CLI that generates a colour-emoji data module with exactly the glyphs you need, from a few codepoints to the full ~3,600-glyph set. --download + checksum, --ttf, --all, --ranges. + v1.4.0 pdfnative-build-emoji-font — bundled CLI that generates a colour-emoji data module with exactly the glyphs you need, from a few codepoints to the full ~3,600-glyph set. --download + checksum, --ttf, --all, --ranges, and v1.7.0 --sequences flags|zwj|all|none / --sequence-list for flag & ZWJ ligatures.
  • Streaming output → diff --git a/docs/guides/pdf-manipulation.md b/docs/guides/pdf-manipulation.md index 644a764..104663d 100644 --- a/docs/guides/pdf-manipulation.md +++ b/docs/guides/pdf-manipulation.md @@ -268,6 +268,11 @@ claim are preserved, so Info↔XMP parity holds for PDF/A documents. to it.) - **Full rebuild, not in-place surgery.** The clean-graph approach trades a little speed for correctness and is safe to run on third-party PDFs. +- **Page boxes survive** _(v1.7.0)_. Declared `/TrimBox`, `/BleedBox`, + `/ArtBox`, `/CropBox` and `/UserUnit` entries are preserved through + `mergePdfs` / `splitPdf` / `extractPages` (they were dropped by the rebuild + before 1.7.0) — so print-ready sources keep their geometry. See the + [print production guide](print.html). ## How it works diff --git a/docs/guides/pdfa.html b/docs/guides/pdfa.html index 84d3951..ae94a14 100644 --- a/docs/guides/pdfa.html +++ b/docs/guides/pdfa.html @@ -4,7 +4,7 @@ PDF/A conformance — pdfnative - + @@ -14,13 +14,13 @@ - + - + diff --git a/docs/guides/react.md b/docs/guides/react.md index fd1977c..95dfa1a 100644 --- a/docs/guides/react.md +++ b/docs/guides/react.md @@ -1,6 +1,6 @@ # pdfnative-react — Declarative JSX Renderer Guide -> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on pdfnative 1.6.0), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies (semver-accepts 1.7.0). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). +> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on the pdfnative 1.6 engine), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies (semver-accepts 1.7.0). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). [`pdfnative-react`](https://github.com/Nizoka/pdfnative-react) turns declarative **JSX** into real, on-device PDFs powered by the zero-dependency [`pdfnative`](https://github.com/Nizoka/pdfnative) engine — no DOM, no headless browser, no SaaS round-trips. Your documents never leave the process. diff --git a/docs/guides/signatures.html b/docs/guides/signatures.html index ac3818e..e774475 100644 --- a/docs/guides/signatures.html +++ b/docs/guides/signatures.html @@ -4,7 +4,7 @@ Digital signatures — pdfnative - + @@ -14,13 +14,13 @@ - + - + diff --git a/docs/guides/text-extraction.md b/docs/guides/text-extraction.md index f6b2065..bda5568 100644 --- a/docs/guides/text-extraction.md +++ b/docs/guides/text-extraction.md @@ -72,6 +72,11 @@ mapped through their `/ToUnicode` CMap — which pdfnative's own CIDFont output always embeds, so **pdfnative-generated documents round-trip losslessly**, including CJK, Arabic, and emoji. +Since pdfnative **1.7.0** the `/ToUnicode` coverage is complete across both +builders: base-14 font dictionaries are reached under tagged mode and the +AcroForm `/Helv` dictionary carries a CMap in every mode — so base-14 text in +tagged documents and **text typed into form fields** now extract cleanly too. + ## Reading order and runs `page.text` is assembled geometrically: runs are grouped into lines by diff --git a/docs/index.html b/docs/index.html index 2b5a9e3..b878148 100644 --- a/docs/index.html +++ b/docs/index.html @@ -72,7 +72,7 @@ "@id": "https://pdfnative.dev/#library", "inLanguage": "en", "name": "pdfnative", - "description": "Pure native PDF library — no runtime dependencies in the engine, producing documents that conform to ISO 32000-1. Generate, parse, merge/split, extract text with positions, fill & flatten AcroForms, decrypt and re-encrypt (AES-128/256), PAdES digital signatures with long-term validation (LTV: RFC 3161 timestamps, /DSS, document timestamps), native vector charts (stacked, area, scatter, log & time axes).", + "description": "Pure native PDF library — no runtime dependencies in the engine, producing documents that conform to ISO 32000-1. Generate, parse, merge/split, extract text with positions, fill & flatten AcroForms, decrypt and re-encrypt (AES-128/256), PAdES digital signatures with long-term validation (LTV: RFC 3161 timestamps, /DSS, document timestamps), native vector charts (stacked, area, scatter, log & time axes), print production (page boxes, printer's marks, /Trapped, /UserUnit, custom OutputIntent).", "url": "https://pdfnative.dev", "codeRepository": "https://github.com/Nizoka/pdfnative", "programmingLanguage": "TypeScript", @@ -252,7 +252,7 @@

    Zero Dependencies

    22 Unicode Scripts

    -

    Thai, Arabic, Hebrew, Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, Myanmar, Amharic, CJK, Cyrillic, Greek, Devanagari, and more. Full UAX #9 BiDi — isolates + explicit embeddings (LRE/RLE/LRO/RLO/PDF). OpenType GSUB/GPOS shaping for Thai, Arabic, Devanagari, Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, and Myanmar.

    +

    Thai, Arabic, Hebrew, Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, Myanmar, Amharic, CJK, Cyrillic, Greek, Devanagari, and more. Full UAX #9 BiDi — isolates + explicit embeddings (LRE/RLE/LRO/RLO/PDF). OpenType GSUB/GPOS shaping for Thai, Arabic, Devanagari, Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, and Myanmar. Plus COLRv1 colour emoji — with 51 flag and 22 ZWJ sequences as single colour ligatures since v1.7.0. Emoji guide →

    @@ -270,7 +270,7 @@

    Security Built-in

    Rich Content

    -

    13 block types: tables, images, barcodes (5 ISO formats), native vector charts, SVG, AcroForm fields, TOC, watermarks, hyperlinks. Pure PDF vector ops — no rasterization. Smart tables: multi-page slicing with repeated headers, auto-wrap, zebra striping, captions, and per-cell borders. Document tools: bookmarks (/Outlines), page labels, viewer preferences, nested lists, and a merge / split / extract page-tree API. Tables guide → · Manipulation guide →

    +

    13 block types: tables, images, barcodes (5 ISO formats), native vector charts, SVG, AcroForm fields, TOC, watermarks, hyperlinks. Pure PDF vector ops — no rasterization. Smart tables: multi-page slicing with repeated headers, auto-wrap, zebra striping, captions, and per-cell borders. Document tools: bookmarks (/Outlines), page labels, viewer preferences, nested lists, and a merge / split / extract page-tree API. Print production (v1.7.0): bleed/trim/art/crop page boxes, crop & registration marks, /Trapped, large-format /UserUnit, custom OutputIntent ICC. Tables guide → · Manipulation guide → · Print guide →

    @@ -971,6 +971,8 @@

    Guides

  • Annotations
  • Outlines & page labels
  • Signatures
  • +
  • Long-term validation
  • +
  • Print production
  • Colour emoji
  • FAQ
  • Troubleshooting
  • diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 31ca41b..15fc4e0 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -5,7 +5,7 @@ ## Docs - [README](https://github.com/Nizoka/pdfnative/blob/main/README.md): Full feature list, install, quick start, API surface, examples. -- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). +- [ROADMAP](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md): Shipped milestones (v1.0 → v1.7; v1.7: PAdES LTV B-B → B-LTA, charts v2, colour-emoji flag/ZWJ sequences, UAX #9 conformance, print production, PDF/A declaration guards, `updateMetadata`; v1.6: parser-side decryptor, streaming merge/split/extract, fill & flatten AcroForms, native vector charts, `extractText`, re-encrypt output; v1.5: math/technical-symbols font, `pdfnative/tools` font compiler/parser, SVG ``, page-labels reader, layout debug overlay + `inspectDocumentLayout`, markup annotation read/write, AI-agent governance; v1.4: document outline/bookmarks, page labels, page-tree manipulation (merge/split/extract), streamToFile, COLRv1 advanced compositing; v1.3: COLRv1 colour emoji, USE-lite shaper integration, true constant-memory streaming, UAX #9 X4–X5 overrides, Telugu + Amharic/Sinhala/Tibetan/Khmer/Myanmar scripts, validatePdfUA) + long-term (WASM, full USE). - [CHANGELOG](https://github.com/Nizoka/pdfnative/blob/main/CHANGELOG.md): Per-version changes. - [Release notes v1.7.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.7.0.md): PAdES LTV signing B-B → B-LTA (`signPdfBytesWithTimestamp` RFC 3161 timestamps, `addValidationInfo` `/DSS` + `/VRI` OCSP/CRL material, `addDocumentTimestamp`, `listSignatures`, multi-signature via `addSignaturePlaceholder({ allowMultiple })` — network transport injected through `setTimestampProvider`/`setRevocationProvider`, the engine never opens a socket), charts v2 (stackedBar/stackedBarH/area/scatter, secondary right axis, log + UTC time scales, data labels, x-label collision handling), colour-emoji flag & ZWJ sequences (51 flags + 22 ZWJ as COLR ligatures), UAX #9 conformance fixes (RTL digit order, glyph mirroring, ALEF joining, Persian presentation forms), PDF/A declaration guards (`strict`/`onDiagnostic`), incremental writer/xref hardening, `PdfModifier.updateMetadata()`. - [Release notes v1.6.0](https://github.com/Nizoka/pdfnative/blob/main/release-notes/v1.6.0.md): Standard Security Handler reader/decryptor (`openPdf(bytes, { password })`, RC4/AES-128/AES-256; `PdfSourceInput`/`MergeOptions.password` for encrypted merge/split sources), streaming page-tree manipulation (`streamMergedPdfs`/`streamSplitPdf`/`streamExtractPages`), fill & flatten existing AcroForm PDFs (`readFormFields`/`fillForm`/`flattenForm`) — encrypted documents included (encrypted incremental update), native vector charts (`chart` block: bar/barH/line/pie/donut), text extraction (`extractText`: per-page reading-order text + positioned runs, encrypted PDFs supported), re-encrypt merge/split output (`MergeOptions.encrypt`, AES-128/256), expanded colour-emoji subset (221 → 1167 incl. complete Transport & Map). Fixes: spec-compliant R6 hash + all-strings encryption on the writer; incremental trailer carries `/Encrypt` forward. @@ -25,7 +25,7 @@ - [FAQ](https://pdfnative.dev/guides/faq.html): Custom fonts, encryption, BiDi, workers, vs other libs. - [Troubleshooting](https://pdfnative.dev/guides/troubleshooting.html): Missing glyphs, RTL appearing backwards, oversized PDFs, parser errors. - [Colour-emoji CLI](https://pdfnative.dev/guides/colour-emoji-cli.html): `npx pdfnative-build-emoji-font` — generate a colour-emoji data module covering any glyph subset up to the full ~3,600-glyph Noto Color Emoji set, plus flag & ZWJ sequences via `--sequences` / `--sequence-list`; pinned download + SHA-256 verification. -- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bars, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. +- [Charts](https://pdfnative.dev/guides/charts.html): Native vector `chart` blocks — 9 kinds (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut) with secondary right axis, log and time scales, data labels, x-label collision handling. Path operators, no rasterisation; `/Figure` tagging with alt text. - [Text extraction](https://pdfnative.dev/guides/text-extraction.html): `extractText()` — reading-order Unicode via `/ToUnicode`, optional positioned runs, works on encrypted PDFs with a password. The RAG / agent-ingestion entry point. - [Form filling](https://pdfnative.dev/guides/form-filling.html): `readFormFields()` / `fillForm()` / `flattenForm()` on existing AcroForms, including encrypted PDFs via incremental update. - [PDF manipulation](https://pdfnative.dev/guides/pdf-manipulation.html): `openPdf()` (with `{ password }`), merge / split / extract pages, re-encrypt on output, and their streaming variants. @@ -49,7 +49,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). - [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. -- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.7.0. +- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on the pdfnative 1.6 engine (pins `^1.6.0`, semver-accepts 1.7.0). - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. ## Source @@ -60,7 +60,7 @@ ## Ecosystem -- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. +- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). @@ -178,6 +178,15 @@ For a real-world test, open your PDF with **NVDA** (Windows, free), **VoiceOver* Contributions to address these are welcome — see the [Roadmap](https://github.com/Nizoka/pdfnative/blob/main/ROADMAP.md). +> **PDF/A declaration guards** _(v1.7.0)_. Tagged documents that also claim +> PDF/A are now checked at build time: configurations that would break the +> declared level (base-14 text without embedded fonts, form fields under a +> claim, DeviceCMYK images against the sRGB OutputIntent) surface a diagnostic +> — `console.warn` by default, a custom `onDiagnostic` sink, or a thrown error +> under `strict: true`. Base-14 dictionaries under tagged mode also gain full +> `/ToUnicode` coverage, which changes output bytes versus 1.6.0. Details in +> the [PDF/A guide](pdfa.html). + ## Testing your PDFs are accessible A pragmatic checklist for solo developers: @@ -1121,7 +1130,7 @@ labels shape correctly when the matching font is registered — no extra work. # pdfnative-cli — Command-Line Interface Guide -> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on pdfnative 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). +> **Tracks the latest published `pdfnative-cli`** (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-cli releases](https://github.com/Nizoka/pdfnative-cli/releases). [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) is the **official command-line interface** for the [`pdfnative`](https://github.com/Nizoka/pdfnative) library. It exposes 17 commands in five groups — create & edit (`render`, `fill`, `annotate`), page tree (`merge`, `split`, `extract`), security (`sign`, `verify`, `encrypt`, `decrypt`), read & extract (`inspect`, `extract-text`), and automation & meta (`batch`, `doctor`, `schema`, `completion`, `govern`) — that together cover the full document lifecycle from JSON to a signed, verified, archive-grade PDF, plus page-tree editing, markup annotations, and an AI-governance gate, with an agent-native automation contract for autonomous AI and CI pipelines. @@ -1994,12 +2003,12 @@ node samples/run-all.js ### What's new in v1.3.0 -v1.3.0 is built on **pdfnative 1.7.0** and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. +v1.3.0 is built on the **pdfnative 1.6 engine** (pin `^1.6.0`) and surfaces its engine additions as five new commands (`extract-text`, `fill`, `encrypt`, `decrypt`, `doctor`), native vector charts in `render`, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent `render --encrypt` no-op. **100 % backward-compatible** with v1.2.0. | Area | v1.2.0 | v1.3.0 | |---|---|---| | Commands | 12 commands | adds **`fill`** (fill / flatten / **export** AcroForms via an incremental save), **`encrypt`**, **`decrypt`**, **`extract-text`** (reading-order Unicode text: `text` \| `json` \| `ndjson`), and **`doctor`** (offline environment / capability preflight) | -| Charts | — | the pdfnative 1.7.0 `chart` document block (bar, barH, line, pie, donut) renders as pure vector path operators through `render` | +| Charts | — | the engine's `chart` document block (bar, barH, line, pie, donut — the pdfnative 1.6 set) renders as pure vector path operators through `render` | | Encryption vocabulary | `--encrypt-*` flags on `render` only | **unified vocabulary** — `--encrypt [aes-128\|aes-256]`, `--owner-password`, `--user-password`, `--permissions print,copy,modify,extract` — shared by `render`, `merge`, `split` and `extract` (legacy `--encrypt-*` flags kept as aliases) | | Page-tree commands | plaintext sources only | **`--password`** reads encrypted sources; **`--encrypt`** re-encrypts the output; **`--stream`** (+ `--chunk-size`) streams the output with constant memory | | Inspection | metadata, PDF/UA, annotations | adds `inspect --form-fields`, `--encryption` and `--password` | @@ -3196,6 +3205,14 @@ Both operations use the incremental modifier rewritten — the update is appended after the existing body with a new xref and a `/Prev` chain. That is why signatures on earlier revisions stay valid. +> **Searchable form text** _(v1.7.0)_. The AcroForm `/Helv` font dictionary now +> carries a `/ToUnicode` CMap in every mode, so text typed into form fields is +> searchable and extractable (see the [text extraction guide](text-extraction.html)). +> Because of that CMap, **every form-carrying document changes bytes** compared +> to v1.6.0 output (about 20 bytes) — a deliberate correctness fix, recorded in +> the v1.7.0 release notes. Under a PDF/A claim, a form field also raises the +> `PDFA_UNEMBEDDED_FORM_FONT` diagnostic (see the [PDF/A guide](pdfa.html)). + ## See also - [Signatures](signatures.html) — CMS/PKCS#7 signing @@ -3350,7 +3367,7 @@ The repository ships a deterministic offline mock PKI (`scripts/helpers/mock-pki - `listSignatures(bytes)` — every entry, its SubFilter, ByteRange, placeholder/timestamp status. - Adobe Reader: *Signature Panel → Certificate Details* — trust the mock/company root, then look for **"LTV enabled"** on B-LT documents and a valid document timestamp on B-LTA. - `openssl ts -reply -in token.der -text` inspects extracted timestamp tokens. -- pdfnative itself ships **no signature verifier** — validation belongs to dedicated tooling (veraPDF for PDF/A, Adobe/DSS for signatures, `pdfnative-cli verify` for structural checks). +- pdfnative itself ships **no signature verifier** — validation belongs to dedicated tooling (veraPDF for PDF/A, Adobe/DSS for signatures, `pdfnative-cli verify` for full cryptographic verification: CMS signature values, certificate chain and trust, RFC 3161 timestamps, OCSP/CRL revocation). ## Limits & scope (v1.7.0) @@ -4226,7 +4243,7 @@ v1.4.0 upgrades the engine to **pdfnative 1.5.0**, brings the pdfnative **AI-gov ### What's new in v1.5.0 -v1.5.0 upgrades the engine to **pdfnative 1.7.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. +v1.5.0 upgrades the engine to **pdfnative 1.6.0** and takes the catalogue to **24 tools** at that release, closing the read/modify loop on documents the assistant did not create. - **Five new tools:** - `add_chart` — bar, horizontal-bar, line, pie and donut charts drawn as **native PDF vector paths**. No rasterisation and no image round-trip, so the output stays sharp at any zoom and tags as `/Figure` with alt text. If you were previously asking an agent to render a chart to PNG and push it through `embed_image`, stop — this replaces that. @@ -4855,6 +4872,11 @@ claim are preserved, so Info↔XMP parity holds for PDF/A documents. to it.) - **Full rebuild, not in-place surgery.** The clean-graph approach trades a little speed for correctness and is safe to run on third-party PDFs. +- **Page boxes survive** _(v1.7.0)_. Declared `/TrimBox`, `/BleedBox`, + `/ArtBox`, `/CropBox` and `/UserUnit` entries are preserved through + `mergePdfs` / `splitPdf` / `extractPages` (they were dropped by the rebuild + before 1.7.0) — so print-ready sources keep their geometry. See the + [print production guide](print.html). ## How it works @@ -5581,7 +5603,7 @@ See the dedicated [CLI guide](cli.html) for the full command reference, security # pdfnative-react — Declarative JSX Renderer Guide -> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on pdfnative 1.7.0), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). +> **Tracks the latest published `pdfnative-react`** (v1.1.0, built on the pdfnative 1.6 engine), with **React 19** and `pdfnative` ^1.6.0 as peer dependencies (semver-accepts 1.7.0). Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the [documentation home](../index.html). Full history: [pdfnative-react releases](https://github.com/Nizoka/pdfnative-react/releases). [`pdfnative-react`](https://github.com/Nizoka/pdfnative-react) turns declarative **JSX** into real, on-device PDFs powered by the zero-dependency [`pdfnative`](https://github.com/Nizoka/pdfnative) engine — no DOM, no headless browser, no SaaS round-trips. Your documents never leave the process. @@ -6800,6 +6822,11 @@ mapped through their `/ToUnicode` CMap — which pdfnative's own CIDFont output always embeds, so **pdfnative-generated documents round-trip losslessly**, including CJK, Arabic, and emoji. +Since pdfnative **1.7.0** the `/ToUnicode` coverage is complete across both +builders: base-14 font dictionaries are reached under tagged mode and the +AcroForm `/Helv` dictionary carries a CMap in every mode — so base-14 text in +tagged documents and **text typed into form fields** now extract cleanly too. + ## Reading order and runs `page.text` is assembled geometrically: runs are grouped into lines by diff --git a/docs/llms.txt b/docs/llms.txt index b9a4368..c742a42 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -49,7 +49,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). - [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. -- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.6.0 (pins `^1.6.0`, semver-accepts 1.7.0). +- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on the pdfnative 1.6 engine (pins `^1.6.0`, semver-accepts 1.7.0). - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. ## Source @@ -60,6 +60,6 @@ ## Ecosystem -- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. +- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 5228199..c489b78 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -85,7 +85,7 @@ https://pdfnative.dev/guides/accessibility.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -101,7 +101,7 @@ https://pdfnative.dev/guides/ltv.html - 2026-08-20 + 2026-08-25 monthly 0.7 @@ -125,7 +125,7 @@ https://pdfnative.dev/guides/colour-emoji.html - 2026-08-21 + 2026-08-25 monthly 0.7 @@ -149,7 +149,7 @@ https://pdfnative.dev/guides/pdf-manipulation.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -157,7 +157,7 @@ https://pdfnative.dev/guides/form-filling.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -165,7 +165,7 @@ https://pdfnative.dev/guides/charts.html - 2026-08-21 + 2026-08-25 monthly 0.7 @@ -173,7 +173,7 @@ https://pdfnative.dev/guides/text-extraction.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -213,7 +213,7 @@ https://pdfnative.dev/guides/cli.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -229,7 +229,7 @@ https://pdfnative.dev/guides/react.html - 2026-08-01 + 2026-08-25 monthly 0.7 @@ -279,7 +279,7 @@ https://pdfnative.dev/playgrounds/charts.html - 2026-08-01 + 2026-08-25 monthly 0.6 @@ -303,7 +303,7 @@ https://pdfnative.dev/learn/ - 2026-08-01 + 2026-08-25 monthly 0.9 diff --git a/llms.txt b/llms.txt index b9a4368..c742a42 100644 --- a/llms.txt +++ b/llms.txt @@ -49,7 +49,7 @@ - [Copilot instructions](https://github.com/Nizoka/pdfnative/blob/main/.github/copilot-instructions.md): Canonical project conventions — architecture, code style, build/test, PDF-specific invariants. - [AGENTS.md](https://github.com/Nizoka/pdfnative/blob/main/AGENTS.md): Editor-agnostic agent guidance (Cursor / Aider / Claude Code / Continue). - [MCP integration](https://pdfnative.dev/guides/mcp.html): `pdfnative-mcp` v1.6.0 — use pdfnative from Claude Desktop, Cursor, Continue, Zed via 28 tools — the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`), `update_metadata`, the read-only `inspect_layout` pagination preview, all 13 block kinds in `generate_basic_pdf`, print production, charts v2, native vector `add_chart`, `read_form_fields` / `fill_form`, `encrypt_pdf` / `decrypt_pdf`, markup `annotate_pdf`, the network-free `draft_governance_issue`, and the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages`. MCP 2026-07-28 spec with legacy fallback. Built on pdfnative 1.7.0. -- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on pdfnative 1.6.0 (pins `^1.6.0`, semver-accepts 1.7.0). +- [CLI](https://pdfnative.dev/guides/cli.html): `pdfnative-cli` v1.3.0 — 17 commands from the terminal — render, fill, annotate, merge / split / extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern; agent-native `--json`/`E_*`/`--dry-run` contract. Built on the pdfnative 1.6 engine (pins `^1.6.0`, semver-accepts 1.7.0). - [React](https://pdfnative.dev/guides/react.html): `pdfnative-react` v1.1.0 — declarative JSX components compiled on-device to pdfnative blocks; render functions, client hooks, token-frugal DocSpec. ## Source @@ -60,6 +60,6 @@ ## Ecosystem -- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on pdfnative 1.6.0 — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. +- [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). From 5f7165b638ec2933435c45f5c3a7d74c7569448a Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:29:31 +0200 Subject: [PATCH 06/15] docs(site): pre-render every guide server-side and publish the machine index The 28 guide pages used to ship an empty
    filled in by guide.js at runtime: any fetcher that does not execute JavaScript - most AI crawlers, social scrapers, curl - received the word "Loading..." instead of the guide, and the JSON-LD injected by the same script was equally invisible. scripts/build-guides.ts now renders each guide''s Markdown into its shell between guide:render markers, with deterministic GitHub-style heading ids plus visible anchor links (marked >= 5 stopped emitting heading ids, so in-page anchors had never actually existed), externalised links, and server-side TechArticle + BreadcrumbList JSON-LD (FAQPage for the FAQ) carrying dateModified from the manifest''s verifiedOn. marked@12.0.2 is pinned as an exact devDependency - the same version the pages load from the CDN. The new guide-render-sync rule rebuilds every shell in memory and fails CI when a committed render is stale, exactly as llms-sync polices llms-full.txt; the served site stays fully static. guide.js detects the pre-rendered article, skips the fetch, and keeps its old render path as a fallback; it now also adds copy buttons to every code block and a "Copy page as Markdown / View Markdown source" bar on both paths. Machine discoverability: llms.txt gains a "Machine-readable artefacts" section; robots.txt points AI pipelines at llms.txt and llms-full.txt; the sitemap lists the 28 raw .md URLs plus llms.txt and llms-full.txt; the major page heads declare rel="alternate" links to llms.txt; and the new generated docs/llms-index.json publishes per-guide HTML/Markdown URLs, titles, summaries, section anchors and exact byte / approximate token sizes (policed by llms-index-sync) - sizes live there rather than in llms.txt so no artefact embeds its own size. llms-full.txt now also embeds the README, the largest API reference in the repo and previously absent from the ingestible corpus. The docs test suite gains perturbation tests for both new rules (20/20), with the repo''s node_modules junction-linked into the sandbox so the generator''s marked import resolves there. Rule count 17 -> 19 updated in CONTRIBUTING and copilot-instructions. verify:docs: 19 rules green. --- .github/copilot-instructions.md | 2 +- CONTRIBUTING.md | 2 +- docs/guides/accessibility.html | 172 ++- docs/guides/agentic-workflows.html | 343 +++-- docs/guides/ai-governance.html | 338 +++-- docs/guides/annotations.html | 307 ++-- docs/guides/architecture.html | 174 ++- docs/guides/charts.html | 268 +++- docs/guides/cli.html | 2140 +++++++++++++++++++++++++-- docs/guides/colour-emoji-cli.html | 366 +++-- docs/guides/colour-emoji.html | 395 +++-- docs/guides/debugging.html | 290 ++-- docs/guides/faq.html | 308 +++- docs/guides/font-validation.html | 237 +-- docs/guides/form-filling.html | 160 +- docs/guides/guide.css | 65 + docs/guides/guide.js | 76 +- docs/guides/index.html | 1 + docs/guides/ltv.html | 151 +- docs/guides/mcp.html | 1008 ++++++++++++- docs/guides/onboarding.html | 281 ++-- docs/guides/outlines.html | 367 +++-- docs/guides/pdf-manipulation.html | 426 ++++-- docs/guides/pdfa.html | 382 ++++- docs/guides/print.html | 149 +- docs/guides/quickstart.html | 189 ++- docs/guides/react.html | 381 ++++- docs/guides/signatures.html | 569 +++++-- docs/guides/streaming.html | 333 +++-- docs/guides/tables.html | 491 ++++-- docs/guides/text-extraction.html | 165 ++- docs/guides/troubleshooting.html | 115 +- docs/guides/viewer-preferences.html | 337 +++-- docs/index.html | 1 + docs/learn/index.html | 1 + docs/llms-full.txt | 1490 +++++++++++++++++++ docs/llms-index.json | 539 +++++++ docs/llms.txt | 6 + docs/playgrounds/index.html | 1 + docs/responsibility.html | 1 + docs/robots.txt | 5 + docs/sitemap.xml | 244 +++ llms.txt | 6 + package-lock.json | 14 + package.json | 2 + scripts/build-guides.ts | 286 ++++ scripts/build-llms-full.ts | 126 +- scripts/verify-docs.ts | 47 + tests/docs/verify-docs.test.ts | 25 +- 49 files changed, 12477 insertions(+), 1305 deletions(-) create mode 100644 docs/llms-index.json create mode 100644 scripts/build-guides.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 82c849c..f5635d6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -113,7 +113,7 @@ npm run typecheck:scripts # tsc --project tsconfig.scripts.json --noEmit npm run typecheck:all # typecheck src/ + tests/ + scripts/ npm run lint # eslint src/ (ESLint 9 + typescript-eslint strict) npm run validate:pdfa # veraPDF over every PDF/A-claiming sample (auto-detected; coverage canary vs declared.pdfaSamples; skips with exit 0 when veraPDF is absent, blocking in CI) -npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth) +npm run verify:docs # 19 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth) ``` - Build tool: **tsup** (dual ESM/CJS, tree-shakeable, sourcemaps) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7e57b5..38a907a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ npm run test:watch # vitest (watch mode) npm run test:coverage # vitest with v8 coverage (95%+ stmts measured at the v1.6.0 release; CI enforces ≥88%) npm run test:generate # Generate 242 sample PDFs → test-output/ npm run validate:pdfa # veraPDF validation of every PDF/A-claiming sample (see below) -npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms files +npm run verify:docs # 19 offline rules over docs/, playgrounds, README, llms files npm run bench # Performance benchmarks (vitest bench) ``` diff --git a/docs/guides/accessibility.html b/docs/guides/accessibility.html index ee7b799..3059ae7 100644 --- a/docs/guides/accessibility.html +++ b/docs/guides/accessibility.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,171 @@

    Home  ›  Guides  ›  Accessibility

    -
    -

    Loading…

    - +
    + +

    Accessibility

    +

    pdfnative produces tagged, accessible PDFs out of the box. This guide covers PDF/UA, PDF/A, the structure tree, and best practices for screen-reader-friendly output.

    +

    Why tagged PDF matters#

    +

    A tagged PDF carries a parallel structure tree alongside the visual content. Assistive technologies (screen readers, refreshable braille displays, reflow tools) walk the structure tree to present the document in reading order — without it, they fall back to heuristic guessing of what's a heading, a paragraph, or a table cell.

    +

    pdfnative's tagged mode targets two related standards:

    +
      +
    • PDF/UA (ISO 14289-1) — accessibility conformance, structure tree required.
    • +
    • PDF/A (ISO 19005) — long-term archival, with PDF/A-2u additionally requiring Unicode mapping for every glyph.
    • +
    +

    Enabling tagged mode#

    +
    const pdf = buildDocumentPDFBytes(params, { tagged: true });
    +
    +

    tagged: true is shorthand for tagged: 'pdfa2b' — the modern default.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptionPDF versionStandardUse case
    tagged: false (default)1.4ISO 32000-1Simple PDFs, no accessibility guarantees
    tagged: true1.7PDF/A-2bRecommended — accessible + archival
    tagged: 'pdfa1b'1.4PDF/A-1bLegacy archives requiring PDF 1.4
    tagged: 'pdfa2u'1.7PDF/A-2uArchive with mandatory text extraction
    tagged: 'pdfa3b'1.7PDF/A-3bPDF/A-2b + embedded files (e.g. ZUGFeRD invoices)
    +

    What pdfnative emits#

    +

    When tagged is enabled, the document gets:

    +
      +
    • Structure tree (/StructTreeRoot) with semantic roles:
        +
      • /Document (root)
      • +
      • /H1, /H2, /H3 for headings
      • +
      • /P for paragraphs
      • +
      • /L/LI for lists
      • +
      • /Table/TR/TH / /TD for tables
      • +
      • /Figure for images, barcodes, SVG (with /Alt text)
      • +
      • /Link for hyperlinks (with /Contents description)
      • +
      • /Form for AcroForm fields
      • +
      • /TOC/TOCI for tables of contents
      • +
      +
    • +
    • Marked content (/Span << /MCID n /ActualText <hex> >> BDC) wrapping every text run, so screen readers receive the original Unicode even when the visible glyph is a shaped Arabic ligature or Devanagari conjunct.
    • +
    • Per-page parent trees (/StructParents + /ParentTree) — required by ISO 14289-1 §7.10.3.
    • +
    • Document metadata in XMP (PDF/A-required).
    • +
    • sRGB ICC OutputIntent so colors remain stable across viewers.
    • +
    +

    Block-level accessibility hints#

    +

    Images and SVG → require alt#

    +
    { type: 'image',  data: pngBytes, width: 300, alt: 'Q1 revenue chart, $1.2M peak in March' }
    +{ type: 'svg',    data: logoSvg, width: 200, alt: 'Company logo' }   // note: property is `data`
    +
    +

    Only image and svg blocks accept an alt property (and chart accepts altText, auto-generated when omitted); a barcode block has no alt field — its accessible description cannot currently be customised. Every /Figure in the structure tree gets an /Alt entry from the alt property. Always provide alt text for non-decorative images — empty strings are treated as decorative and may be skipped by screen readers.

    + +
    { type: 'link', text: 'Read the API reference', url: 'https://github.com/Nizoka/pdfnative#api' }
    +
    +

    Avoid text: 'click here' — screen reader users who navigate by link list lose context.

    +

    Tables → header rows are detected automatically#

    +

    When you pass a headers array, those cells are tagged /TH and the data rows are tagged /TD. For complex tables with span-cells or merged headers, pdfnative currently emits flat /Table → /TR → /TH|TD — explicit cell scopes (/Scope = Row|Column) are not yet exposed.

    +

    Forms → label every field#

    +
    { type: 'formField', fieldType: 'text', name: 'email', label: 'Email address', width: 400 }
    +
    +

    The label is drawn as a visible text label next to the widget. pdfnative does not emit a /TU (tooltip) entry, so the visible label is the field's only accessible name — keep it present and descriptive.

    +

    Verifying conformance#

    +

    veraPDF (PDF/A)#

    +
    # veraPDF is a Java application, not an npm package — install it from
    +# https://verapdf.org/software/ (installer) or run it via Docker:
    +docker run --rm -v "$PWD:/data" verapdf/cli --format text /data/my-document.pdf
    +
    +

    veraPDF is the reference PDF/A validator. The pdfnative test suite generates 242 sample PDFs and the PDF/A samples are validated against veraPDF on every release.

    +

    PAC 2024 (PDF/UA)#

    +

    PAC (PDF Accessibility Checker) is a free Windows tool that walks the structure tree and reports PDF/UA issues. Run it on samples in test-output/tagged/.

    +

    Screen reader spot-check#

    +

    For a real-world test, open your PDF with NVDA (Windows, free), VoiceOver (macOS, built in), or TalkBack (Android). Listen to the reading order — it should match the visual order.

    +

    Limitations and known caveats#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AreaStatus
    Heading roles /H1/H3Supported
    /H4/H6Mapped to /H3 (PDF/UA permits this)
    Cell scope /ScopeNot yet exposed — flat table tags
    Reading order overridesImplicit (block insertion order) — no explicit /Order array
    Language tags per text runDocument-level only (/Lang in catalog)
    Artifact tagging (decorative content)Not yet exposed — header/footer/watermark are tagged as content
    +

    Contributions to address these are welcome — see the Roadmap.

    +
    +

    PDF/A declaration guards (v1.7.0). Tagged documents that also claim +PDF/A are now checked at build time: configurations that would break the +declared level (base-14 text without embedded fonts, form fields under a +claim, DeviceCMYK images against the sRGB OutputIntent) surface a diagnostic +— console.warn by default, a custom onDiagnostic sink, or a thrown error +under strict: true. Base-14 dictionaries under tagged mode also gain full +/ToUnicode coverage, which changes output bytes versus 1.6.0. Details in +the PDF/A guide.

    +
    +

    Testing your PDFs are accessible#

    +

    A pragmatic checklist for solo developers:

    +
      +
    • tagged: true is set on every user-facing PDF.
    • +
    • Every image / svg / barcode block has a meaningful alt.
    • +
    • Every link has descriptive text (no "click here").
    • +
    • Every formField has a label.
    • +
    • Sample passes veraPDF for the chosen PDF/A variant.
    • +
    • Sample reads in the correct order in NVDA/VoiceOver.
    • +
    • If the document is multi-language, the dominant language is set in the document title.
    • +
    +

    Further reading#

    + +
    diff --git a/docs/guides/agentic-workflows.html b/docs/guides/agentic-workflows.html index 0417724..bad9c96 100644 --- a/docs/guides/agentic-workflows.html +++ b/docs/guides/agentic-workflows.html @@ -1,86 +1,261 @@ - - - - - - Agentic workflows — pdfnative - - - + + + + + + Agentic workflows — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Agentic workflows

    - -
    - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Agentic workflows

    +
    + +

    Agentic workflows

    +
    +

    How AI agents compose with pdfnative. pdfnative is designed so an autonomous +agent can do more than call the engine — it can extend it at runtime and +feed it content it generated itself, all without waiting for a library +release. This guide documents two concrete, factual patterns and the exact +public APIs they rely on.

    +
    +

    Both patterns are built entirely on already-shipped, public API surfaces:

    +
      +
    • The library's registerFont() / loadFontData() font registry, the +pdfnative/tools sub-path (parseFontData / compileFontData, since v1.5.0), +and the bundled pdfnative-build-font CLI.
    • +
    • The image entry points: the image document block (library + CLI) and the +embed_image MCP tool.
    • +
    +

    There is no new API here — the point is that the existing surfaces were +shaped so agents can use them autonomously, under the project's +AI-governance / human-in-the-loop contract.

    +
    +

    Pattern 1 — extend the engine at runtime, without a release#

    +

    pdfnative ships 26 bundled font-data modules (22 scripts plus Latin, math, and monochrome + colour emoji). But its font +system is open: any TrueType/OpenType font becomes a first-class, +CIDFont-embedded, subset-on-use font once it is registered. Registration is a +runtime call — it does not require rebuilding or republishing pdfnative.

    +

    This is what lets an agent add a capability the moment a document needs it. In a +previous iteration of this project, an agent using the MCP server needed +mathematical symbols before the bundled Noto Sans Math font existed as a +release. Because the font registry is a public runtime API, the agent was able to +compile the font data and register it on the spot; the same font later shipped as +registerFont('math', …) in pdfnative 1.5.0. The library did not need to change +for the document to render — the release simply promoted an already-working +runtime pattern into a bundled default.

    +

    The three building blocks#

    + + + + + + + + + + + + + + + + + + + + + + + +
    APISub-pathWhat it does
    registerFont(lang, loader) / registerFonts({ … })pdfnativeRegister a lazy font-data loader under a lang code. loadFontData(lang) resolves it on first use.
    parseFontData(bytes)FontDataObjectpdfnative/toolsParse a TTF/OTF in memory into a registerable font-data object (metrics, cmap, widths, GSUB/GPOS, /W array). Pure — no fs, no child_process, works in browsers / Deno / edge.
    compileFontData(bytes, { fontName })stringpdfnative/toolsEmit the ES/CJS module source for a font-data file — byte-identical to the pdfnative-build-font CLI. Useful when the agent wants to persist a reusable *-data.js.
    +

    An agent registers a font at runtime#

    +
    import { buildDocumentPDFBytes, registerFont, loadFontData } from 'pdfnative';
    +import { parseFontData } from 'pdfnative/tools';
    +
    +// The agent obtained the TTF bytes however it likes — a bundled asset,
    +// a user upload, or a fetch it performed itself.
    +const ttfBytes: Uint8Array = await getFontBytes();
    +
    +// Parse in memory → a registerable font-data object.
    +const fontData = parseFontData(ttfBytes);
    +
    +// Register it under a lang code. No release, no rebuild.
    +registerFont('custom', () => Promise.resolve(fontData));
    +
    +// The registry is only consulted through loadFontData + fontEntries —
    +// load the data and pass it explicitly (fontRef must be a PDF name; /F1 and /F2 are reserved):
    +const custom = await loadFontData('custom');
    +if (!custom) throw new Error('custom font failed to load');
    +
    +// It is now a first-class font: pdfnative subsets and embeds it on use.
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Runtime font',
    +  blocks: [{ type: 'paragraph', text: 'Rendered with an agent-registered font.' }],
    +  fontEntries: [{ fontData: custom, fontRef: '/F3', lang: 'custom' }],
    +});
    +
    +

    Persisting a reusable data module#

    +

    When an agent wants the font to be reusable across runs — or to hand a ready-made +module to a human — it can emit the module source instead:

    +
    import { compileFontData } from 'pdfnative/tools';
    +
    +const source = compileFontData(ttfBytes, { fontName: 'My Font' });
    +// `source` is byte-identical to what `pdfnative-build-font` writes to disk.
    +// A human (or a sandboxed file tool) can save it as `my-font-data.js`.
    +
    +

    The equivalent one-liner for a human at a terminal is the bundled CLI:

    +
    npx pdfnative-build-font fonts/ttf/MyFont.ttf fonts/my-font-data.js
    +
    +
    +

    Why this matters. The engine's coverage is not frozen at release time. An +agent can close a glyph gap — a new script, a symbol set, a brand font — the +instant a document requires it, then optionally graduate that work into a +committed data module for the whole team. This is runtime extensibility, not +autonomous modification of the published package: the agent extends its own +in-process pdfnative instance; the repository is only ever changed by a human +under the governance contract.

    +
    +
    +

    Pattern 2 — agent-generated images in the PDF#

    +

    Modern agents can generate raster content — charts, diagrams, illustrations, +photos. Image-generating agents (for example Antigravity, ChatGPT, and other +multimodal assistants) can pipe that output straight into a pdfnative document. +pdfnative treats a generated PNG/JPEG exactly like any other image: it parses it +natively and embeds it as an Image XObject (/DCTDecode for JPEG, +/FlateDecode for PNG) — no rasterization, no headless browser.

    +

    Via the MCP server — embed_image#

    +

    An agent that produced an image returns it as base64 and calls embed_image:

    +
    {
    +  "tool": "embed_image",
    +  "input": {
    +    "title": "Quarterly trend",
    +    "imageBase64": "<base64 PNG/JPEG the agent just generated>",
    +    "mimeType": "image/png",
    +    "outputMode": "base64"
    +  }
    +}
    +
    +

    For a richer layout, the same base64 payload can be dropped into an image +block on generate_basic_pdf, alongside headings, tables, and barcodes the agent +assembles in the same call.

    +

    Via the library or CLI — the image block#

    +

    In code, a generated image is just another block:

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Report with a generated figure',
    +  blocks: [
    +    { type: 'heading', text: 'Findings', level: 1 },
    +    { type: 'paragraph', text: 'The figure below was generated on-device by the agent.' },
    +    { type: 'image', data: generatedPngBytes, width: 480 },
    +  ],
    +});
    +
    +

    From the shell, an agent driving pdfnative-cli render supplies the same block +in its JSON document (image bytes are provided as a block field or an asset path, +subject to the CLI's path-validation rules).

    +
    +

    Safety. pdfnative validates image inputs at the boundary — it parses the +JPEG/PNG structure natively and rejects malformed or unsupported payloads +(e.g. raw RGBA). The agent supplies pixels; pdfnative decides whether they are +a well-formed image before embedding.

    +
    +
    +

    How the two patterns fit together#

    +

    A single agent turn can combine both: register a brand font, generate a cover +image, and assemble a signed, archive-grade PDF — in one MCP conversation or one +CLI pipeline, without a pdfnative release in the loop.

    +

    Agentic workflows: an AI agent generates images (embedded via the image block / embed_image) and compiles fonts (registered at runtime via registerFont + parseFontData), both feeding the pdfnative public API to produce an ISO 32000-1 PDF. Repository changes stay human-gated under the AI-governance / HITL contract.

    +

    The engine stays zero-dependency and unchanged; the agent supplies fonts and +images through public, validated entry points. Anything that would modify the +repository — a new bundled font, a code change — still goes through a human +under the AI-governance / human-in-the-loop contract.

    +
    +

    See also#

    + + +
    +
    + + + + + + + + + + + + diff --git a/docs/guides/ai-governance.html b/docs/guides/ai-governance.html index b3ddb0e..22fe7cd 100644 --- a/docs/guides/ai-governance.html +++ b/docs/guides/ai-governance.html @@ -1,86 +1,256 @@ - - - - - - AI governance & human-in-the-loop — pdfnative - - - + + + + + + AI governance & human-in-the-loop — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  AI governance

    - -
    - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  AI governance

    +
    + +

    AI governance & human-in-the-loop

    +
    +

    Shipped in v1.5.0. pdfnative is developed with the help of AI coding +agents — and it governs them. This repository ships a machine-readable +governance contract and a human-in-the-loop (HITL) protocol that keep +AI agents in an advisory role: they may draft proposals, but a human always +reviews, approves, and submits. Nothing reaches GitHub autonomously.

    +
    +

    This page exists for transparency. If you evaluate pdfnative for production, +you should be able to see exactly how AI participates in its maintenance — and +where the hard limits are.

    +

    AI governance and human-in-the-loop flow — an agent drafts across four steps (detect, reproduce, self-check, draft into .github/drafts/), automated checks run verify-issue.mjs and produce a compliance report, then a human review and sign-off gate is the only path to a submission published under the human's own GitHub identity. A red callout lists what agents must never do.

    +

    Why this exists#

    +

    Modern open-source projects increasingly receive AI-generated issues and pull +requests. Unbounded, that creates noise, low-signal duplicates, and — worst of +all — a diffusion of responsibility: who actually stands behind this change?

    +

    pdfnative's answer is a written, enforceable contract:

    +
      +
    • Agents are draftsmen, never submitters. Their authority ends at producing +a local markdown draft plus a compliance report.
    • +
    • A human is always in the loop. A person explicitly reviews, approves, and +triggers every issue, comment, pull request, and release.
    • +
    • Identity integrity. Anything submitted is published under the human's own +GitHub identity — so a human always shares responsibility for the content.
    • +
    +

    The two source-of-truth files live in the repository and are loaded by any agent +that scans project configuration on start-up:

    + +

    The six mandatory rules#

    +

    Every agent (Copilot, Cursor, Claude, Antigravity, Aider, Cline, Windsurf, +Gemini CLI, …) must satisfy all six before proposing anything:

    +
      +
    1. Zero runtime dependencies. Never suggest, add, or import an external npm +package for a runtime feature. This is a non-negotiable blocker.
    2. +
    3. No duplicates. Search open and closed issues/PRs first. Surface a +matching one instead of opening a new report.
    4. +
    5. Local validation & reproduction. Create and execute a minimal +reproduction. If it does not fail or show a measurable regression, do not +propose an issue.
    6. +
    7. Byte-identity awareness. For changes touching the builders, confirm the +change is additive and existing output stays byte-identical — or report any +intentional byte change.
    8. +
    9. Human-in-the-loop gate (ethics). Agents are strictly forbidden from +creating, editing, or submitting issues, comments, PRs, or releases via any +tool or API. They produce a local draft in +.github/drafts/ and +present it with a compliance report.
    10. +
    11. Identity integrity. The agent reminds the user that anything submitted is +published under their GitHub identity and that they share responsibility.
    12. +
    +

    The human-in-the-loop workflow#

    +
    [Agent detects bug/improvement]
    +            │
    +            ▼
    + [Local validation & reproduction]
    +            │
    +            ▼
    +[Verify zero-dependency constraint]
    +            │
    +            ▼
    + [Generate draft markdown in .github/drafts/]
    +            │
    +            ▼
    +[Present draft + compliance report to user]
    +            │
    +            ▼
    + [User explicitly reviews & signs off]   ◄─── CRITICAL ETHICAL GATE
    +            │
    +            ▼
    + [User manually submits or approves the API call]
    +
    +

    The agent's job ends at the draft. The human gate is the only path from a +proposal to a published issue, PR, or release.

    +

    The compliance report#

    +

    Every draft must be accompanied by a structured compliance report so the human +can review with full context. At minimum it contains:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldWhat it confirms
    Zero-dependency confirmedno new runtime dependency introduced
    Reproduction commandthe exact command the agent ran
    Reproduction resultthe observed failure or regression
    Duplicate searchwhat was searched and what was found
    Affected packageswhich monorepo packages are impacted
    Identity reminder shownthe user was told it publishes under their name
    +

    Validate a draft before presenting it#

    +

    A small, dependency-free verifier checks a draft mechanically:

    +
    node scripts/verify-issue.mjs .github/drafts/my-issue.md
    +# or via the npm script
    +npm run verify:issue .github/drafts/my-issue.md
    +
    +

    It fails when the draft proposes an external dependency or omits a reproduction +code block. A passing check is necessary but not sufficient — the human +review gate above always applies. The verifier is advisory in CI and exposes a +pure validateIssueMarkdown(content) function so its logic is unit-tested.

    +

    The machine-readable contract#

    +

    .github/ai-governance.json +encodes the policy so agents that scan repository configuration can honour it +without parsing prose. Key fields:

    +
    {
    +  "policy": {
    +    "automatic_issue_reporting": false,
    +    "runtime_dependencies_allowed": false,
    +    "human_in_the_loop_mandatory": true,
    +    "autonomous_github_writes_allowed": false,
    +    "required_issue_fields": [
    +      "minimal_reproduction", "environment", "expected_behavior"
    +    ]
    +  },
    +  "human_in_the_loop": {
    +    "role_of_agent": "draftsman",
    +    "gate": "A human MUST explicitly review, sign off on, and trigger any GitHub issue, comment, PR, or release.",
    +    "draft_location": ".github/drafts/"
    +  }
    +}
    +
    +

    The contract applies_to the whole ecosystem — pdfnative, pdfnative-cli, +pdfnative-mcp, and pdfnative-react.

    +

    Since pdfnative-mcp 1.6.0, that package's mirror of the charter also states +the single permitted egress class: the server makes no outbound request by +default, and the only network calls it can ever perform go to the TSA / OCSP / +CRL endpoints the operator configures for the PAdES tools — never to a URL +taken from a tool argument, and never to GitHub. "No GitHub write path" and +"no telemetry" remain absolute.

    +

    What agents must never do#

    +
      +
    • Add a runtime dependency.
    • +
    • Open, edit, label, close, or comment on issues/PRs autonomously.
    • +
    • Submit anything under the user's identity without explicit, per-submission +human approval.
    • +
    • Bypass local validation or duplicate checks.
    • +
    +

    In short#

    +

    pdfnative treats AI as a force multiplier for humans, not a replacement for +human judgement. Agents accelerate the tedious parts — reproduction, drafting, +compliance checking — while every externally visible action stays under +deliberate human control. That is the standard we hold ourselves to, and we +publish it so you can hold us to it too.

    + +
    +
    + + + + + + + + + + + + diff --git a/docs/guides/annotations.html b/docs/guides/annotations.html index 7418378..0fc614e 100644 --- a/docs/guides/annotations.html +++ b/docs/guides/annotations.html @@ -1,85 +1,226 @@ - - - - - - Annotations (read & write) — pdfnative - - - + + + + + + Annotations (read & write) — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Annotations

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Annotations

    +
    + +

    Annotations (read & write)

    +
    +

    New in v1.5.0. pdfnative now has a typed markup-annotation model plus +round-trip read and write support. Build annotations for a new +document, read the annotations out of an existing PDF, or inject new ones into +an existing PDF via incremental update — all zero-dependency, all +ISO 32000-1 §12.5.

    +
    +

    Supported annotation types#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    typePDF /SubtypeUse
    text/TextSticky-note comment with an icon
    highlight/HighlightHighlight a text region
    underline/UnderlineUnderline a text region
    strikeout/StrikeOutStrike through a text region
    squiggly/SquigglySquiggly (spellcheck-style) underline
    square/SquareRectangle
    circle/CircleEllipse
    line/LineStraight line
    freetext/FreeTextTypewriter / callout text
    +

    Every type shares AnnotationBase: rect, and optional contents, color, +opacity, title, modified, flags.

    +

    Building annotations#

    +
    import { buildAnnotation } from 'pdfnative';
    +import type { MarkupAnnotation } from 'pdfnative';
    +
    +const note: MarkupAnnotation = {
    +  type: 'text',
    +  rect: [72, 700, 96, 724],
    +  contents: 'Please double-check this figure.',
    +  title: 'Reviewer',
    +  icon: 'Comment',
    +};
    +
    +// Full indirect object, ready to concatenate into a PDF you assemble yourself:
    +const obj = buildAnnotation(note, /* objNum */ 42);
    +
    +
      +
    • buildAnnotation(annot, objNum) returns a complete N 0 obj … endobj string.
    • +
    • buildAnnotationBody(annot) returns just the dictionary body — used by the +modifier when injecting into an existing file.
    • +
    +

    /Contents and /T are safely encoded (UTF-16BE when needed), /C / /IC +run through parseColor, /QuadPoints are auto-derived from rect for the +text-markup types, and /F defaults to 4 (Print).

    +

    Reading annotations from an existing PDF#

    +
    import { openPdf } from 'pdfnative';
    +
    +const reader = openPdf(existingBytes);
    +const annots = reader.getAnnotations(0); // page index 0
    +
    +for (const a of annots) {
    +  console.log(a.subtype, a.rect, a.contents, a.url);
    +}
    +
    +

    getAnnotations(pageIndex) returns ParsedAnnotation[] with subtype, rect, +and optional contents (UTF-16BE decoded), title, color, quadPoints, and +url (for URI-action links). getPageRef(pageIndex) returns the page's +indirect reference when you need it.

    +

    Writing annotations into an existing PDF#

    +

    Add a new annotation to an existing document with a non-destructive incremental +update:

    +
    import { openPdf, createModifier, buildAnnotationBody } from 'pdfnative';
    +
    +const reader = openPdf(existingBytes);
    +const modifier = createModifier(reader);
    +
    +const body = buildAnnotationBody({
    +  type: 'highlight',
    +  rect: [72, 640, 300, 656],
    +  color: '#ffe14d',
    +  contents: 'Key clause',
    +});
    +
    +modifier.addAnnotation(0, body); // inject on page 0
    +const updated = modifier.save();  // appended incremental section, original preserved
    +
    +

    addAnnotation() appends the annotation object and rewrites the page's +/Annots array in an incremental update section — the original bytes are never +mutated, so any existing signature's byte range upstream of the appended section +stays intact.

    +

    Encrypted documents are supported since v1.6.0: open with +openPdf(bytes, { password }) and annotate as usual — the appended +annotation objects are encrypted under the document's existing scheme +(RC4, AES-128 and AES-256 sources), so the annotation text never appears +in cleartext in the file.

    +
    const modifier = createModifier(openPdf(encryptedBytes, { password: 'secret' }));
    +modifier.addAnnotation(0, body);
    +const updated = modifier.save(); // annotation stored encrypted
    +
    +

    Sample#

    +

    annotations-showcase.ts +builds a document with every annotation type, reads them back out, and injects a +highlight into an existing PDF.

    + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/architecture.html b/docs/guides/architecture.html index e58f537..a27ecf1 100644 --- a/docs/guides/architecture.html +++ b/docs/guides/architecture.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,173 @@

    Home  ›  Guides  ›  Architecture

    -
    -

    Loading…

    - +
    + +

    Architecture Guide

    +

    Two Builders#

    +

    pdfnative provides two PDF builders for different use cases:

    +

    buildPDF() / buildPDFBytes() — Table-Centric#

    +

    Best for: financial statements, reports, tabular data.

    +
    import { buildPDFBytes } from 'pdfnative';
    +
    +const pdf = buildPDFBytes({
    +  title: 'Monthly Report',
    +  headers: ['Date', 'Description', 'Amount'],
    +  rows: [
    +    { cells: ['01/15', 'Grocery', '-$45.00'], type: 'debit', pointed: false },
    +  ],
    +  // ... infoItems, balanceText, countText, footerText
    +});
    +
    +

    Produces a structured table layout with title, info items, balance line, column headers, data rows, and footer. Supports pagination, tagged PDF, encryption, compression.

    +

    buildDocumentPDF() / buildDocumentPDFBytes() — Free-Form#

    +

    Best for: reports, manuals, invoices, any document with mixed content.

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Project Report',
    +  blocks: [
    +    { type: 'heading', text: 'Introduction', level: 1 },
    +    { type: 'paragraph', text: 'This report covers...' },
    +    { type: 'table', headers: ['Q', 'Revenue'], rows: [...] },
    +    { type: 'image', data: pngBytes, width: 300 },
    +    { type: 'barcode', format: 'qr', data: 'https://example.com' },
    +  ],
    +});
    +
    +

    Supports 13 block types: heading, paragraph, list, table, image, link, spacer, pageBreak, toc, barcode, svg, formField, chart.

    +

    Generation Pipeline#

    +
    Input (params + options)
    +  │
    +  ├─ resolveLayout()         → page dimensions, margins, column positions
    +  ├─ normalizeColors()       → validate & normalize color values
    +  ├─ createEncodingContext()  → font encoding (WinAnsi + CIDFont)
    +  │
    +  ├─ Content Loop ─────────────────────────────────
    +  │   ├─ Text rendering      → BT/ET operators, font selection
    +  │   ├─ Image embedding     → XObject + Do operator
    +  │   ├─ Barcode rendering   → PDF path operators (re f)
    +  │   ├─ SVG rendering       → PDF path operators (m l c)
    +  │   └─ Page breaks         → new page object
    +  │
    +  ├─ Font subsetting         → TTF subset per used font
    +  ├─ Tagged PDF (optional)   → structure tree, /ActualText, MCID
    +  ├─ PDF/A (optional)        → XMP metadata, ICC OutputIntent
    +  ├─ Encryption (optional)   → AES-128/256, key derivation
    +  ├─ Compression (optional)  → FlateDecode on content streams
    +  │
    +  └─ Assembly ─────────────────────────────────────
    +      ├─ createPdfWriter()   → binary writer with offset tracking
    +      ├─ Object emission     → N 0 obj ... endobj
    +      └─ writeXrefTrailer()  → xref table + trailer + startxref
    +
    +

    Module Dependency Flow#

    +
    types/ → core/ ← fonts/ ← shaping/ ← worker/
    +              ↑
    +          crypto/ (near-standalone, imports core/pdf-encrypt for sha256)
    +          parser/ (imports core/pdf-compress, pdf-encrypt, pdf-tags)
    +
    +
      +
    • No circular dependencies — strict unidirectional flow, with one +sanctioned, documented reverse edge: the signature-workflow core modules +(pdf-dss, pdf-sig-utils, pdf-doc-timestamp, pdf-sig-placeholder, +pdf-form-fill) import the parser to read existing documents before +appending incremental revisions
    • +
    • crypto/ is near-standalone — its only cross-module import is sha256 from core/pdf-encrypt.ts (re-exported by crypto/sha.ts)
    • +
    • parser/ imports from core/pdf-compress.ts (FlateDecode), core/pdf-encrypt.ts (decryption, incremental /ID) and core/pdf-tags.ts (XMP resync in updateMetadata)
    • +
    • fonts/ imports from shaping/ for script detection
    • +
    • shaping/ imports from fonts/ encoding context (via core/encoding-context.ts to break cycle)
    • +
    +

    Key Design Decisions#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DecisionRationale
    No classesPure functions — easier to test, tree-shake, and reason about
    String-based PDF operatorsDirect control over output, no AST overhead
    Lazy font loadingregisterFonts() + loadFontData() — load only needed scripts
    Shared assemblerpdf-assembler.ts eliminates xref/trailer duplication between builders
    Extracted rendererspdf-renderers.ts — block renderers, text wrapping, constants extracted from pdf-document.ts for maintainability. planTable() and TableSlice provide planner-driven multi-page table rendering — _paginateBlocks() measures once then slices at row boundaries, keeping renderTable() page-lifecycle-free. See Smart tables guide.
    Encoding context in core/Dependency inversion — breaks fonts/ → shaping/ cycle
    +

    Ecosystem#

    +

    The architecture diagram above shows the internal library modules. External consumers sit above the library and import from pdfnative like any npm package. Three official companion packages — a CLI, an MCP server, and a React renderer — cover the most common non-library workflows. All live in separate repositories, version independently, and depend on pdfnative only through its public API, so the core engine stays zero-dependency.

    +
                             ┌─────────────────────────────────────────────┐
    +   [shell / CI / Docker] │  pdfnative-cli (npm) — 17 commands          │
    +                         └─────────────────────────────────────────────┘
    +                         ┌─────────────────────────────────────────────┐
    +    [Claude / Cursor / …] │  pdfnative-mcp (npm) — 28 AI tools          │
    +                         └───────────────────────────────────────────┘
    +                         ┌─────────────────────────────────────────────┐
    +   [React / Next.js app] │  pdfnative-react (npm) — declarative JSX    │
    +                         └─────────────────────────────────────────────┘
    +                            │  import { … } from 'pdfnative'  (public API only)
    +                         ┌─────────────────────────────────────────────┐
    +                         │  pdfnative (npm) — zero-dependency engine   │  ← this repo
    +                         └─────────────────────────────────────────────┘
    +
    +

    pdfnative-cli#

    +

    pdfnative-cli is the official command-line interface. It exposes seventeen commands — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern — that map directly to public pdfnative APIs, with an agent-native --json/E_*/--dry-run automation contract:

    +
    [shell / Makefile / GitHub Actions / Docker]
    +              │ argv + stdin/stdout
    +     ┌──────────────────────────┐
    +     │  pdfnative-cli (npm)     │  ← dispatch layer, 17 commands + agent contract
    +     └──────────────────────────┘
    +              │ import { buildDocumentPDFBytes, signPdfBytes, openPdf, validatePdfUA } from 'pdfnative'
    +     ┌──────────────────────────┐
    +     │      pdfnative (npm)     │  ← core library (this repo)
    +     └──────────────────────────┘
    +
    +

    Like pdfnative-mcp and pdfnative-react, the CLI lives in a separate repository and depends on pdfnative only via the public API surface. See the CLI Guide for usage and the security model.

    +

    pdfnative-mcp#

    +

    pdfnative-mcp is a Model Context Protocol server that wraps the pdfnative public API and exposes it as 28 structured tools to any MCP-compatible AI client (Claude Desktop, Cursor, Continue, Zed, ChatGPT, …). It commits to API stability via a per-tool _meta.apiVersion field.

    +
    [Claude Desktop / Cursor / Continue / Zed]
    +              │ MCP stdio protocol
    +     ┌──────────────────────────┐
    +      │  pdfnative-mcp (npm)     │  ← MCP server, 28 tools
    +     └──────────────────────────┘
    +              │ import { buildDocumentPDFBytes, … } from 'pdfnative'
    +     ┌──────────────────────────┐
    +     │      pdfnative (npm)     │  ← core library (this repo)
    +     └──────────────────────────┘
    +
    +

    pdfnative-mcp is not an internal module — it is a separate npm package with its own repository, versioning, and release cadence. It references pdfnative only through the public API.

    +

    For setup instructions, tool reference, and per-client configuration, see the MCP Integration Guide.

    +

    pdfnative-react#

    +

    pdfnative-react v1.1.0 is the declarative React renderer. A custom React reconciler compiles a JSX component tree — synchronously, with no DOM — into the pdfnative DocumentParams model, which the engine renders to bytes:

    +
    [React / Next.js / Remix component tree]
    +              │ custom react-reconciler (no DOM, no headless browser)
    +     ┌──────────────────────────┐
    +     │  pdfnative-react (npm)   │  ← <Document>/<Table>/<Barcode> → pdfnative blocks
    +     └──────────────────────────┘
    +              │ import { buildDocumentPDFBytes, … } from 'pdfnative'
    +     ┌──────────────────────────┐
    +     │      pdfnative (npm)     │  ← core library (this repo)
    +     └──────────────────────────┘
    +
    +

    React 19 is a peer dependency of pdfnative-react only — the core pdfnative engine remains zero-dependency. Components map 1:1 onto pdfnative blocks, and the token-frugal DocSpec lets AI agents author the same documents with a fraction of the tokens. See the React Guide for the component reference and the React playground to try it in your browser.

    +
    diff --git a/docs/guides/charts.html b/docs/guides/charts.html index 9462d0c..fbc76b7 100644 --- a/docs/guides/charts.html +++ b/docs/guides/charts.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,267 @@

    Home  ›  Guides  ›  Charts

    -
    -

    Loading…

    - +
    + +

    Charts (native vector)

    +
    +

    New in v1.6.0, extended in v1.7.0. Render bar, horizontal-bar, stacked-bar, +line, area, scatter, pie, and donut charts as pure PDF path operators — +rectangles, line segments, and cubic-Bézier arcs. Zero dependencies, no +rasterisation, no image embedding. Charts are crisp at any zoom, searchable +(labels are real text), and PDF/A-safe.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Report',
    +  blocks: [
    +    {
    +      type: 'chart',
    +      chartType: 'bar',
    +      title: 'Quarterly revenue vs cost',
    +      categories: ['Q1', 'Q2', 'Q3', 'Q4'],
    +      series: [
    +        { label: 'Revenue', values: [120, 150, 170, 140] },
    +        { label: 'Cost',    values: [80, 90, 100, 95] },
    +      ],
    +    },
    +  ],
    +});
    +
    +

    Add a chart block anywhere in a document's blocks array, like a heading, +paragraph, or table.

    +

    Chart types#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    chartTypeShapeSeries
    'bar'Vertical grouped bars1+
    'barH'Horizontal grouped bars1+
    'line'Multi-series line, optional markers1+
    'pie'Pieexactly 1
    'donut'Donut (pie with a hole)exactly 1
    'stackedBar' (v1.7.0)Vertical stacked bars (positive and negative running totals stack separately)1+
    'stackedBarH' (v1.7.0)Horizontal stacked bars1+
    'area' (v1.7.0)Line closed to the zero baseline, opaque tinted fill1+
    'scatter' (v1.7.0)X/Y point markers positioned by xValues1+
    +

    The ChartBlock#

    +
    interface ChartBlock {
    +  type: 'chart';
    +  chartType: 'bar' | 'barH' | 'line' | 'pie' | 'donut'
    +    | 'stackedBar' | 'stackedBarH' | 'area' | 'scatter';    // last four: v1.7.0
    +  series: {
    +    label: string;
    +    values: number[];
    +    color?: PdfColor;
    +    xValues?: (number | string)[]; // x positions for scatter / linear / time x-axes (v1.7.0)
    +    yAxis?: 'left' | 'right';      // bind the series to the secondary right axis (v1.7.0)
    +  }[];
    +  categories?: string[];        // x-axis / slice labels; defaults to 1-based indices
    +  width?: number;               // plot width in pt (clamped to content width). Default 460
    +  height?: number;              // plot-area height in pt. Default 240
    +  title?: string;
    +  legend?: 'bottom' | 'none';   // default: shown for multi-series / pie
    +  axis?: { yMin?: number; yMax?: number; ticks?: number; grid?: boolean;
    +           scale?: 'linear' | 'log' };                      // scale: v1.7.0
    +  axis2?: { yMin?: number; yMax?: number; ticks?: number;
    +            scale?: 'linear' | 'log' };                     // right value axis (v1.7.0)
    +  xAxis?: { type?: 'category' | 'linear' | 'time';          // v1.7.0
    +            min?: number | string; max?: number | string;
    +            ticks?: number; grid?: boolean };
    +  dataLabels?: boolean                                      // v1.7.0
    +    | { decimals?: number; prefix?: string; suffix?: string };
    +  labelStride?: number;         // draw every Nth category label. Default: auto (v1.7.0)
    +  labelRotation?: number;       // rotate category labels 0–90 degrees (v1.7.0)
    +  markers?: boolean;            // line only. Default false
    +  colors?: PdfColor[];          // palette override (per series / per slice)
    +  align?: 'left' | 'center' | 'right';
    +  altText?: string;             // tagged-PDF /Figure /Alt (auto-generated if omitted)
    +}
    +
    +

    Colours accept the same forms as the rest of pdfnative — '#4e79a7', +[78, 121, 167], or a PDF RGB string — and are validated (injection-safe). When +omitted, a built-in 8-colour categorical palette is used deterministically.

    +

    Examples#

    +

    Multi-series line with markers#

    +
    {
    +  type: 'chart', chartType: 'line', markers: true,
    +  categories: ['Jan', 'Feb', 'Mar', 'Apr'],
    +  series: [
    +    { label: 'Free', values: [10, 25, 22, 40] },
    +    { label: 'Paid', values: [3, 8, 12, 18] },
    +  ],
    +}
    +
    +

    Donut with a custom palette#

    +
    {
    +  type: 'chart', chartType: 'donut', title: 'Traffic sources',
    +  categories: ['Organic', 'Referral', 'Direct', 'Social'],
    +  colors: ['#59a14f', '#4e79a7', '#f28e2b', '#e15759'],
    +  series: [{ label: 'Share', values: [55, 20, 15, 10] }],
    +}
    +
    +

    Negative values#

    +

    Bar and line charts anchor at a zero baseline, so negative values render below +the axis automatically:

    +
    { type: 'chart', chartType: 'bar', categories: ['Jan', 'Feb', 'Mar'],
    +  series: [{ label: 'Net flow', values: [30, -15, 20] }] }
    +
    +

    Stacked bars (v1.7.0)#

    +

    Positive and negative values stack in separate running totals per category, so +mixed-sign data reads correctly:

    +
    {
    +  type: 'chart', chartType: 'stackedBar',
    +  categories: ['Q1', 'Q2', 'Q3', 'Q4'],
    +  series: [
    +    { label: 'EMEA', values: [40, 55, 60, 52] },
    +    { label: 'APAC', values: [25, 30, 35, 41] },
    +    { label: 'AMER', values: [50, 48, 62, 70] },
    +  ],
    +}
    +
    +

    Area with data labels (v1.7.0)#

    +

    The area fill is the series colour mixed 35 % toward white — an opaque +tint, not transparency, so area charts remain PDF/A-safe (PDF/A-1b forbids +/ExtGState alpha):

    +
    {
    +  type: 'chart', chartType: 'area',
    +  categories: ['Jan', 'Feb', 'Mar', 'Apr'],
    +  dataLabels: { suffix: ' k€', decimals: 1 },
    +  series: [{ label: 'ARR', values: [10.4, 12.1, 13.8, 16.2] }],
    +}
    +
    +

    dataLabels: true reuses the axis tick formatter; the object form controls +decimals and adds a prefix/suffix.

    +

    Scatter on a time axis (v1.7.0)#

    +

    scatter positions each point by xValues (required). On xAxis.type: 'time', +xValues accepts ISO-8601 strings or epoch milliseconds; tick positions and +labels are computed with UTC getters only — never Intl or the host time +zone — so output bytes are identical on every machine:

    +
    {
    +  type: 'chart', chartType: 'scatter',
    +  xAxis: { type: 'time', grid: true },
    +  series: [{
    +    label: 'Deploys',
    +    xValues: ['2026-01-05', '2026-02-14', '2026-03-02', '2026-04-20'],
    +    values: [3, 7, 5, 11],
    +  }],
    +}
    +
    +

    line and area charts may also opt into a 'linear' or 'time' x-axis; +bar variants stay categorical.

    +

    Dual value axes & log scale (v1.7.0)#

    +

    Bind a series to a secondary right axis with yAxis: 'right' and configure its +range with axis2. The right axis (and its gutter) appears only when at least +one series binds to it. Either axis can switch to scale: 'log' (decade +ticks); log scales require strictly positive values and cannot be combined +with stacked charts:

    +
    {
    +  type: 'chart', chartType: 'line', markers: true,
    +  categories: ['2023', '2024', '2025', '2026'],
    +  axis2: { scale: 'log' },
    +  series: [
    +    { label: 'Revenue (k€)', values: [120, 150, 170, 140] },
    +    { label: 'Requests/day', values: [900, 8000, 65000, 400000], yAxis: 'right' },
    +  ],
    +}
    +
    +

    Crowded category labels (v1.7.0)#

    +

    Long or numerous x-labels no longer collide (issue #67). By default the engine +measures every label and draws every Nth one — the smallest stride at which +labels no longer overlap (1 when everything fits, so existing charts are +unchanged). Override with labelStride (1 forces every label), or rotate +instead:

    +
    {
    +  type: 'chart', chartType: 'bar',
    +  categories: ['January', 'February', 'March', 'April', 'May', 'June'],
    +  labelRotation: 45,   // 0–90° counter-clockwise, right-aligned to the tick
    +  series: [{ label: 'Signups', values: [12, 18, 14, 22, 19, 27] }],
    +}
    +
    +

    Rotated labels read upward toward their tick; setting labelRotation disables +the automatic stride (rotation defeats horizontal overlap on its own) unless +labelStride is also set. Both options apply to category axes only — +scatter charts reject them.

    +

    Accessibility (tagged PDF)#

    +

    In tagged mode ({ tagged: true } or a PDF/A profile), each chart is emitted as +a /Figure structure element with an /Alt description. Provide altText for a +meaningful summary; otherwise a deterministic one is generated +("bar chart: 2 series, 4 categories"). Charts use solid fills only — no +transparency — so they are PDF/A-safe. That includes area charts: the fill +is an opaque tint (series colour mixed toward white), never /ExtGState alpha.

    +

    Internationalised labels#

    +

    All chart text (title, axis ticks, category labels, legend, slice percentages) +flows through pdfnative's standard text pipeline, so CJK, Arabic/RTL, and emoji +labels shape correctly when the matching font is registered — no extra work.

    +

    Limits & scope (v1.7.0)#

    +
      +
    • In: bar, horizontal bar, stacked bar (both orientations), multi-series +line (straight segments + optional markers), area, scatter, pie, donut; +linear and log value axes with "nice"/decade ticks; secondary right value +axis; category, linear, and time (UTC-deterministic) x-axes; gridlines; +legend; negative values; per-point data labels; automatic label stride and +0–90° label rotation.
    • +
    • Out (planned): curved (spline) line interpolation, error bars, bubble +sizing, combined chart types in one plot.
    • +
    • A hard guard rejects charts with more than 10,000 total data points.
    • +
    • Validation is strict and throws before any bytes are produced: log scales +reject non-positive values (and stacked charts), scatter requires +xValues on every series, xValues lengths must match values, +'linear'/'time' x-axes apply only to line/area/scatter, date strings in +xValues require xAxis.type: 'time', yAxis: 'right' is cartesian-only, +and labelStride/labelRotation apply to category axes only.
    • +
    +

    See also#

    + +
    diff --git a/docs/guides/cli.html b/docs/guides/cli.html index f26a94b..6ff60fb 100644 --- a/docs/guides/cli.html +++ b/docs/guides/cli.html @@ -1,86 +1,2058 @@ - - - - - - pdfnative-cli — Command-Line Interface Guide - - - - + + + + + + pdfnative-cli — Command-Line Interface Guide + + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  CLI

    -
    -

    Loading…

    - -
    -
    - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  CLI

    +
    + +

    pdfnative-cli — Command-Line Interface Guide

    +
    +

    Tracks the latest published pdfnative-cli (v1.3.0, built on the pdfnative 1.6 engine — pins ^1.6.0, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the pdfnative version each one is built on — are shown at the top of the documentation home. Full history: pdfnative-cli releases.

    +
    +

    pdfnative-cli is the official command-line interface for the pdfnative library. It exposes 17 commands in five groups — create & edit (render, fill, annotate), page tree (merge, split, extract), security (sign, verify, encrypt, decrypt), read & extract (inspect, extract-text), and automation & meta (batch, doctor, schema, completion, govern) — that together cover the full document lifecycle from JSON to a signed, verified, archive-grade PDF, plus page-tree editing, markup annotations, and an AI-governance gate, with an agent-native automation contract for autonomous AI and CI pipelines.

    +
    +

    Why a CLI? Many real-world workflows live outside Node.js: shell scripts, CI pipelines, Docker containers, Makefiles, batch jobs, build tools written in other languages. The CLI lets all of them call pdfnative without writing JavaScript, and is fully composable through stdin/stdout pipelines.

    +
    +

    The CLI is a pure dispatch layer over pdfnative. No PDF logic lives in the CLI itself — every command forwards to a public pdfnative API:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CLI commandpdfnative API
    renderbuildDocumentPDFBytes() / buildDocumentPDFStream() / buildDocumentPDFStreamTrue() / buildPDFBytes() (table variant = buildPDFBytes, the table-centric builder; document variant = buildDocumentPDFBytes, the free-form block builder)
    fillreadFormFields() / fillForm() / flattenForm()
    encrypt / decryptopenPdf(bytes, { password }) + MergeOptions.encrypt
    extract-textextractText()
    doctor— (environment probe, no library equivalent)
    signsignPdfBytes() / addSignaturePlaceholder() — the native constant-time provider is implemented in the CLI (createNativeCryptoProvider is a CLI utility, not a pdfnative API) and passed per call as options.provider (pdfnative also exposes a global setCryptoProvider(), which the CLI does not use)
    inspectopenPdf() / getInfo() / pageCount / getPageLabels() / getAnnotations() / validatePdfUA()
    verifyopenPdf() + verifyCertSignature() (X.509 certificate-signature checks); the CMS/PKCS#7, RFC 3161 timestamp and OCSP/CRL revocation verification is implemented in the CLI itself
    mergemergePdfs()
    splitsplitPdf()
    extractextractPages()
    annotatePdfModifier.addAnnotation() / buildAnnotationBody()
    governAI-governance contract (.github/ai-governance.json, AGENT_RULES.md) — draft gating uses the CLI's own validateGovernanceDraft() (the core repo's equivalent check is a repo script, not a published API)
    batchthe render pipeline, applied in parallel across a directory
    schemaversioned JSON Schemas (Draft 2020-12) for every input/output shape
    +

    This means every feature of the library is one release away from the CLI, and any bug fix in pdfnative is automatically picked up by pdfnative-cli on its next dependency bump.

    +
    +

    Installation#

    +
    # Run directly with npx — no global install required
    +npx pdfnative-cli render --input document.json --output report.pdf
    +
    +# Or install globally
    +npm install --global pdfnative-cli
    +pdfnative render --input document.json --output report.pdf
    +
    +

    Requirements: Node.js ≥ 20 · Bun · Deno (node dist/cli.cjs).

    +

    The CLI ships with NPM provenance — verify the published artifact with npm audit signatures or on npmjs.com.

    +
    +

    When to use the CLI vs the library#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Use the CLI when…Use the library when…
    You write shell scripts, Makefiles, or Bash/PowerShell pipelinesYou build a Node.js / Bun / Deno service
    Your CI/CD job runs in Docker or GitHub ActionsYou need fine-grained streaming control or Web Worker offloading
    You want to compose with cat, jq, tee, gzip, etc.You target browsers, Web Workers, or Deno Deploy
    You sign, verify, or inspect PDFs ad-hoc from the terminalYou bundle PDFs through a custom pipeline (custom font registry, hooks, etc.)
    You want a one-liner instead of a 30-line Node.js scriptYou need 100 % programmatic control of the API surface
    +

    The two are complementary. A typical full-stack project uses the library at runtime and the CLI in CI scripts and operator workflows.

    +
    +

    Quick start#

    +

    1. Render a document#

    +

    Create report.json:

    +
    {
    +  "title": "April 2026 Report",
    +  "blocks": [
    +    { "type": "heading", "text": "April 2026 Report", "level": 1 },
    +    { "type": "paragraph", "text": "Summary for the financial period ending 30 April 2026." },
    +    { "type": "list", "style": "bullet", "items": [
    +      "Revenue: +18% year-on-year",
    +      "Net Promoter Score: 72",
    +      "Active customers: 12,400"
    +    ]},
    +    { "type": "table",
    +      "headers": ["Quarter", "Revenue", "Profit"],
    +      "rows": [
    +        { "cells": ["Q1", "$1.2M", "$400K"], "type": "credit", "pointed": false },
    +        { "cells": ["Q2", "$1.5M", "$600K"], "type": "credit", "pointed": true }
    +      ]
    +    }
    +  ],
    +  "footerText": "Confidential",
    +  "metadata": { "author": "Finance Team", "subject": "April 2026 Report" }
    +}
    +
    +

    Render it:

    +
    pdfnative render --input report.json --output report.pdf
    +
    +

    That's it — the file report.pdf is now a valid ISO 32000-1 PDF, ready to send.

    +

    2. Sign the rendered PDF (with metadata)#

    +
    # Set keys via environment variables (recommended for CI/CD — never logged)
    +export PDFNATIVE_SIGN_KEY="$(cat private.pem)"
    +export PDFNATIVE_SIGN_CERT="$(cat cert.pem)"
    +
    +pdfnative sign \
    +  --input report.pdf \
    +  --output report.signed.pdf \
    +  --reason "Approved by Finance" \
    +  --name "Finance Team" \
    +  --location "Paris, FR" \
    +  --signing-time 2026-04-28T10:00:00Z
    +
    +

    The CLI accepts both RSA PKCS#1 v1.5 and ECDSA P-256 keys, both with SHA-256 digests. The signed PDF carries a CMS/PKCS#7 signature embedded as ISO 32000-1 §12.8 prescribes, validatable by Adobe Acrobat, MuPDF, and any other PAdES-compatible reader.

    +

    3. Verify embedded signatures#

    +
    pdfnative verify --input report.signed.pdf --strict --trust ca-root.pem
    +
    +

    v0.3.0 performs real CMS/PKCS#7 verification — the CLI recomputes the byte-range digest, validates the signature value (RSA-SHA256 or ECDSA-SHA256), walks the certificate chain via pdfnative's verifyCertSignature, evaluates trust against --trust roots and self-signed acceptance, and reports the presence of an RFC 3161 timestamp token. Exit code is 0 on success, 1 on any failure under --strict.

    +

    A sample JSON report:

    +
    {
    +  "signatures": [
    +    {
    +      "integrity": true,
    +      "signatureValid": true,
    +      "signatureAlgorithm": "ecdsa-sha256",
    +      "chainValid": true,
    +      "trustedRoot": true,
    +      "timestampPresent": false,
    +      "signerSubject": "CN=pdfnative-cli ECDSA Test, O=pdfnative-cli, C=FR",
    +      "signerIssuer": "CN=pdfnative-cli ECDSA Test, O=pdfnative-cli, C=FR",
    +      "notes": ["no --trust provided; accepted self-signed root"]
    +    }
    +  ]
    +}
    +
    + +

    +

    4. Inspect any PDF#

    +
    pdfnative inspect --input report.signed.pdf --format text
    +
    +
    Version:        1.7
    +Pages:          2
    +Encrypted:      no
    +PDF/A:          none
    +Signatures:     1
    +Title:          April 2026 Report
    +Author:         Finance Team
    +Created:        2026-04-28T10:00:00+00:00
    +Subject:        April 2026 Report
    +Producer:       —
    +
    +

    JSON output (default) is suited for piping into jq or storing as a CI artifact.

    +
    +

    Hybrid layout model#

    +

    render adopts the same layout philosophy as gh, kubectl, and docker: high-frequency knobs are first-class flags, while the full PdfLayoutOptions shape is reachable via a JSON layout file.

    +

    Precedence: CLI flags > --layout file > pdfnative defaults.

    +
    # 1) Flags only — best for ad-hoc invocations
    +pdfnative render --input doc.json --output report.pdf \
    +  --page-size A4 --margin 50 --compress --tagged pdfa2b
    +
    +# 2) Layout file only — best for reproducible CI configs
    +pdfnative render --input doc.json --output report.pdf \
    +  --layout layout.json
    +
    +# 3) Hybrid — base config in a file, per-job overrides on the CLI
    +pdfnative render --input doc.json --output report.pdf \
    +  --layout layout.json \
    +  --watermark-text "DRAFT $(date +%Y-%m-%d)"
    +
    +

    layout.json accepts any subset of PdfLayoutOptions:

    +
    {
    +  "pageSize": { "width": 595, "height": 842 },
    +  "margin": { "top": 60, "right": 50, "bottom": 60, "left": 50 },
    +  "compress": true,
    +  "tagged": "pdfa2b",
    +  "headerTemplate": { "left": "{title}", "right": "{date}" },
    +  "footerTemplate": { "center": "Page {page} / {pages}" }
    +}
    +
    +
    +

    Security: --layout paths are validated against directory traversal, and any attachments[].data field embedded in the JSON is stripped on load. Binary attachment payloads must come from --attachment <path> so the CLI can apply the same path-validation rules.

    +
    +
    +

    Command reference#

    +

    pdfnative render#

    +

    Renders a JSON document into a PDF. Supports both renderer variants exposed by pdfnative.

    +

    Core flags#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinJSON file (DocumentParams for --variant document, PdfParams for --variant table)
    --output <file>stdoutOutput PDF path
    --variant document|tabledocumentSelects buildDocumentPDFBytes (free-form) or buildPDFBytes (table-centric)
    --streamoffStreaming output via buildDocumentPDFStream (AsyncGenerator<Uint8Array>) — recommended for >100-page documents
    --stream-page-by-pageoffStreaming chunked at PDF object boundaries. Assembles the full document first, so TOC blocks and {pages} are supported. Mutually exclusive with --stream
    --stream-true (v1.1.0)offStreaming via buildDocumentPDFStreamTrue / buildPDFStreamTrue — PDF parts are emitted and freed as they go, so the joined binary never materialises. Byte-identical to the buffered builders. Same constraints as --stream (no TOC, no {pages}); mutually exclusive with the other --stream* flags
    --max-blocks <n> (v1.1.0)100000Exposes layout.maxBlocks so very large multi-thousand-page reports no longer hit a spurious ceiling
    --layout <file.json>Load any subset of PdfLayoutOptions
    +

    Smart tables (v1.3.0, document variant)#

    +

    These flags fill TableBlock fields left unset in the JSON (caption is per-table — set it in the JSON TableBlock):

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDescription
    --table-wrap <mode>auto (default), always, or never
    --repeat-header [true|false]Repeat the header row on continuation pages
    --zebra [true|false|"R G B"]Alternate-row striping
    --min-row-height <pt>Minimum row height in points
    --cell-padding <pt>Horizontal cell padding in points
    +

    Page geometry#

    + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --page-size <name|WxH>a4Named (a4, letter, legal, a3, tabloid, a5) or WxH in points
    --margin <N> or <t,r,b,l>50Uniform or per-side margin in points
    --compressoffApply /Filter /FlateDecode to all content streams
    +

    PDF/A conformance#

    + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --tagged <level>noneUnified PDF/A flag: none, pdfa1b, pdfa2b, pdfa2u, pdfa3b
    --conformance <level>Deprecated. Maps to --tagged pdfa<level> with a one-line stderr notice. Removed in v1.0.0
    +

    Watermarks#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDescription
    --watermark-text <str>Diagonal text watermark
    --watermark-image <path>Image watermark (PNG/JPEG, centered, aspect-preserved)
    --watermark-opacity <0..1>ExtGState /ca value
    --watermark-angle <deg>Rotation angle
    --watermark-color <hex|R,G,B>Fill color (text only)
    --watermark-font-size <pt>Font size (text only)
    --watermark-position background|foregroundDrawing order vs. content
    +
    +

    Watermarks with transparency are mutually exclusive with PDF/A-1b (ISO 19005-1 §6.4). The pdfnative core enforces this at render time — the run fails with the generic exit code 1 (Watermark transparency is not allowed with PDF/A-1b), not a CLI usage error.

    +
    +

    Headers / footers#

    + + + + + + + + + + + + + + + +
    FlagDescription
    --header-left <str> / --header-center / --header-rightPage-template zones
    --footer-left <str> / --footer-center / --footer-rightPage-template zones
    +

    Supported placeholders: {page}, {pages}, {date}, {title}. The {pages} placeholder is rejected with --stream because the total page count is only known after multi-pass pagination.

    +

    Encryption#

    +

    Since v1.3.0, render speaks the same unified encryption vocabulary as merge / split / extract:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagEnv varDescription
    --encrypt [aes-128|aes-256]Enable encryption (bare --encrypt = aes-128)
    --owner-password <pass>PDFNATIVE_ENCRYPT_OWNER_PASSRequired whenever encryption is requested
    --user-password <pass>PDFNATIVE_ENCRYPT_USER_PASSOptional password needed to open the document
    --permissions <list>Comma list: print, copy, modify, extract
    +

    The legacy v0.2.0 flags — --encrypt-algorithm, --encrypt-owner-pass, --encrypt-user-pass, --encrypt-permissions — remain accepted as aliases; when both are given, the unified value wins.

    +

    Env vars take precedence over flags, ensuring secrets never appear in shell history. Encryption is mutually exclusive with --tagged pdfa* per ISO 19005-1 §6.3.2 — rejected with exit 2.

    +

    PDF/A-3 attachments#

    + + + + + + + + + + + +
    FlagDescription
    --attachment <path>[:mime[:rel[:desc]]]Embed a file as /EmbeddedFile. Repeatable
    +

    The Windows drive-letter colon (D:\path) is detected and not split — see Troubleshooting.

    +

    Multilingual fonts#

    + + + + + + + + + + + + + + + +
    FlagDescription
    --lang <code,code>Activate font loaders for the listed languages (e.g. th,ja,ar,te,si,km)
    --font <name> (v1.1.0)Register a bundled pdfnative font shortcut. Repeatable. Allow-list covers every bundled font: latin, emoji, color-emoji, math (v1.2.0), and the 22 script codes (ar hy bn ru hi am ka el he ja km ko my pl zh si ta te th bo tr vi). Each shortcut name doubles as its --lang code
    +

    --lang activates a programmatically registered font loader via loadFontData(code). Latin scripts are built-in. With v1.1.0, every bundled font is registrable directly through --font — no wrapper script needed for the 22 bundled scripts, colour emoji, or Latin. pdfnative routes each code point to the font whose cmap covers it. See Recipes → Multilang fonts only for fonts you ship yourself (those require calling the library directly).

    +

    Bookmarks, math & layout tooling (v1.2.0)#

    + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDescription
    --outline auto|<tree.json>Add a navigable PDF bookmark tree (/Outlines). auto derives it from the document's headings; a file supplies an explicit OutlineItem[] tree
    --font mathRegister the bundled Noto Sans Math font; pdfnative auto-routes math-operator and geometric-shape code points to it
    --inspect-layoutEmit a LayoutInspection JSON report (per-page blocks, positions, sizes) instead of a PDF (document variant only)
    --debug-layout [margins,content,cells]Render a normal PDF with the opt-in layout-debug guides overlaid
    +
    # Bookmarks derived from headings + math font
    +pdfnative render --input paper.json --output paper.pdf --outline auto --font math
    +
    +# Layout introspection instead of a PDF
    +pdfnative render --input report.json --inspect-layout > layout.json
    +
    +

    Iteration helpers (v0.3.0)#

    + + + + + + + + + + + + + + + +
    FlagDescription
    --watchRe-render on input file change. 200 ms debounce, stderr-only logs. Requires --input <file> and a file --output (stdin / stdout pipelines are not supported — watch needs a stable on-disk source)
    --template <file.json>Deep-merge a base template under stdin / --input. Plain objects merge recursively; arrays and primitives are replaced (caller wins). Useful for centralising title / layout / headers in CI
    +
    # Watch a file
    +pdfnative render --input report.json --output report.pdf --watch
    +
    +# Template + override (template carries title/layout/headers, stdin overrides body)
    +echo '{"blocks":[{"type":"paragraph","text":"Today only."}]}' \
    +  | pdfnative render --template template.json -o today.pdf
    +
    +# Bundled fonts via flag (no wrapper)
    +echo '{"blocks":[{"type":"paragraph","text":"Hi \ud83d\ude80"}]}' \
    +  | pdfnative render --font latin --font emoji --lang latin,emoji -o out.pdf
    +
    +

    pdfnative sign#

    +

    Applies a CMS/PKCS#7 digital signature to an existing PDF.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinInput PDF
    --output <file>stdoutOutput signed PDF
    --key <file>PDFNATIVE_SIGN_KEY envPEM-encoded private key (env var takes precedence)
    --cert <file>PDFNATIVE_SIGN_CERT envPEM-encoded X.509 certificate (env var takes precedence)
    --cert-chain <file>PDFNATIVE_SIGN_CHAIN envIntermediate-CA PEM (repeatable, concatenated into certChain[])
    --algorithm <algo>rsa-sha256rsa-sha256 or ecdsa-sha256 (both fully wired in v0.3.0; SEC1 / PKCS#8 P-256 keys accepted)
    --reason <str>PdfSignOptions.reason
    --name <str>PdfSignOptions.name
    --location <str>PdfSignOptions.location
    --contact <str>PdfSignOptions.contact
    --signing-time <ISO 8601>nowExplicit timestamp; validated up-front before any credential I/O
    --pure-crypto (v1.2.0)offForce pdfnative's portable pure-JS bignum CMS path instead of the default native provider
    +
    +

    Native constant-time signing (v1.2.0). sign now routes CMS signing through Node's node:crypto by default (via createNativeCryptoProvider), for side-channel-resistant RSA/ECDSA. Pass --pure-crypto to select the portable pure-JS path (e.g. on a runtime without node:crypto).

    +
    +

    Signing keys are never logged — not in error output, not in debug traces, not in stack traces. The CLI redacts them at every code path that surfaces error context.

    +

    pdfnative inspect#

    +

    Inspects metadata and conformance of an existing PDF. Read-only — never modifies the input.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinInput PDF
    --format <fmt>jsonjson or text
    --verboseoffAdds verbose.{trailerKeys, catalogKeys, objectCount, xmpMetadata}. Sanitised — no raw stream bytes
    --pagesoffAdds pages: [{ index, width, height, rotation, annotations, formFields }]
    --pdfua (v1.1.0)offAdds a pdfua: { valid, errors, warnings } report from validatePdfUA() (ISO 14289-1 structural checks: MarkInfo, StructTree, ParentTree, Lang, per-page MCID uniqueness)
    --annotations (v1.2.0)offLists markup + link annotations per page (from getAnnotations()). /PageLabels are reported automatically when present
    --form-fields (v1.3.0)offLists AcroForm fields (name, type, value, required/read-only)
    --encryption (v1.3.0)offReports the encryption scheme (algorithm, revision, opened-as)
    --password <pass> (v1.3.0)Password for an encrypted PDF (env PDFNATIVE_PASSWORD)
    --check <assertion>Repeatable; ANDed. Values: pdfa, signed, encrypted, pdfua (v1.1.0). Sets exit 0 = pass, 1 = fail
    --summary (v1.1.0)offUnder --json, emit a canonical minimal verdict ({ pages, encrypted, signatures, pdfa })
    --fields <a,b.c> (v1.1.0)Project the JSON result to named dot-paths (array segments map over elements; unknown paths omitted)
    --prettyoffForce indented JSON even under the global --json (agent mode is compact)
    +

    Composable example:

    +
    pdfnative inspect --input dist/q1.pdf \
    +  --check pdfa --check signed \
    +  --format json > dist/q1.report.json
    +echo "exit code: $?"   # 0 if both assertions hold
    +
    +

    pdfnative verify#

    +

    Verifies CMS/PKCS#7 signatures embedded in a PDF.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinInput PDF
    --format <fmt>jsonjson or text
    --strictoffExit 1 on any failure or zero signatures
    --trust <pem>Trust-anchor certificate (repeatable)
    --revocation <mode>offlineoffline (embedded /DSS only), online (opt-in, SSRF-guarded OCSP/CRL fetch), or disabled
    --revocation-policy <p>soft-failsoft-fail or strict
    --summary (v1.1.0)offUnder --json, emit a minimal verdict ({ valid, signatures, invalid })
    --fields <a,b.c> (v1.1.0)Project the JSON result to named dot-paths
    +

    Scope (since v1.0.0):

    +
      +
    • ✅ Byte-range integrity (SHA-256 recomputed and compared with CMS messageDigest attribute)
    • +
    • ✅ Signature value verification — RSA-SHA256 and ECDSA-SHA256
    • +
    • ✅ Certificate chain verification via pdfnative's verifyCertSignature
    • +
    • ✅ Trust evaluation against --trust roots, with self-signed acceptance for testing
    • +
    • RFC 3161 timestamp validation (PAdES-T) — TSA signature, messageImprint binding, chain, genTime
    • +
    • OCSP (RFC 6960) + CRL (RFC 5280) revocation — embedded /DSS offline by default, opt-in SSRF-guarded online via AIA / CDP
    • +
    +

    Out of scope (upstream-blocked in pdfnative):

    +
      +
    • ⚠️ Sign-side LTV (timestamp embedding / DSS) — sign --timestamp reserved
    • +
    +

    pdfnative merge (v1.2.0)#

    +

    Concatenates 2–50 PDFs into one document, in order, via mergePdfs.

    +
    pdfnative merge a.pdf b.pdf c.pdf --output combined.pdf
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    <paths…> / --input <file>(required)Source PDFs — positional paths and/or repeatable --input. 2–50 total
    --output <file>stdoutOutput PDF
    --drop-annotationsoffDrop non-link annotations from the sources
    --max-output-size <bytes>256 MiBReject an output larger than this ceiling
    --password <pass> (v1.3.0)Password for encrypted sources (env PDFNATIVE_PASSWORD). One password is applied to every source; mixed passwords fail with E_PASSWORD
    --encrypt [aes-128|aes-256] (v1.3.0)Re-encrypt the output (bare = aes-128); needs --owner-password
    --owner-password / --user-password (v1.3.0)Passwords for --encrypt (env PDFNATIVE_ENCRYPT_OWNER_PASS / _USER_PASS)
    --permissions <list> (v1.3.0)print,extractComma list: print, copy, modify, extract — without the flag, print and extract are allowed, copy and modify denied
    --stream (v1.3.0)offConstant-memory streaming output (--chunk-size N)
    --dry-runoffValidate inputs without writing output
    +
    +

    Encrypted sources are supported since v1.3.0 via --password (a single password applied to every source), and the output can be re-encrypted via --encrypt. Signatures and /AcroForm are dropped (page edits invalidate /ByteRange); self-contained URI /Link annotations are preserved. Every path — positionals included — is validated against traversal.

    +
    +

    pdfnative split (v1.2.0)#

    +

    Splits one PDF into many via splitPdf — one output per page (default) or one per comma-separated range.

    +
    # One output per page
    +pdfnative split --input report.pdf --output-dir pages/ --prefix page
    +
    +# One output per range (1-based, inclusive)
    +pdfnative split --input report.pdf --output-dir out/ --pages "1-2,3-4"
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinSource PDF
    --output-dir <dir>(required)Destination directory; parts are written as <prefix>-<n>.pdf (zero-padded)
    --prefix <str>input basename (part on stdin)Output filename prefix
    --pages <ranges>per-pageComma-separated 1-based inclusive ranges (e.g. 1-2,3-4)
    --max-output-size <bytes>256 MiBPer-part output ceiling
    --password <pass> (v1.3.0)Password for an encrypted source (env PDFNATIVE_PASSWORD)
    --encrypt [aes-128|aes-256] (v1.3.0)Re-encrypt each output (needs --owner-password; --user-password / --permissions as in merge)
    --stream (v1.3.0)offConstant-memory streaming output (--chunk-size N)
    --dry-runoffValidate without writing
    +

    pdfnative extract (v1.2.0)#

    +

    Pulls a selected, order-preserving subset of pages into a single PDF via extractPages.

    +
    pdfnative extract --input report.pdf --output cover.pdf --pages "4,1-2"
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input <file>stdinSource PDF
    --output <file>stdoutOutput PDF
    --pages <list>(required)1-based page list/ranges; order is preserved and repeats are allowed
    --max-output-size <bytes>256 MiBOutput ceiling
    --password <pass> (v1.3.0)Password for an encrypted source (env PDFNATIVE_PASSWORD)
    --encrypt [aes-128|aes-256] (v1.3.0)Re-encrypt the output (needs --owner-password; --user-password / --permissions as in merge)
    --stream (v1.3.0)offConstant-memory streaming output (--chunk-size N)
    --dry-runoffValidate without writing
    +

    pdfnative annotate (v1.2.0)#

    +

    Attaches markup annotations to an existing PDF via an incremental save, so the original bytes — and any existing signature — stay intact.

    +
    pdfnative annotate --input report.pdf --output annotated.pdf \
    +  --annotations notes.json
    +
    +

    notes.json is a JSON array (or { "annotations": […] }), each entry a markup annotation plus a 1-based page:

    +
    [
    +  { "page": 1, "type": "highlight", "rect": [72, 700, 520, 715], "color": "#ffe066", "contents": "Review this clause" },
    +  { "page": 2, "type": "text", "rect": [80, 640, 100, 660], "contents": "Sticky note" }
    +]
    +
    +

    Supported types: text, highlight, underline, strikeout, squiggly, square, circle, line, freetext. Only known fields are forwarded (no dictionary injection). Read them back with inspect --annotations.

    +
    +

    Overlay, not redaction. Annotations are a visual review layer; the underlying bytes remain. They do not remove or obscure content for security purposes.

    +
    +

    pdfnative govern (v1.2.0)#

    +

    Surfaces pdfnative's AI-governance / Human-in-the-Loop (HITL) contract. Agents act as draftsmen: a human must always review and submit under their own GitHub identity.

    +
    pdfnative govern rules                  # human/agent protocol (AGENT_RULES)
    +pdfnative govern policy --json          # machine-readable policy JSON
    +pdfnative govern verify-issue draft.md  # gate a draft (exit 1 / E_POLICY)
    +
    + + + + + + + + + + + + + + + + + + + +
    SubcommandDescription
    rulesPrint the human-and-agent protocol (mirrors .github/AGENT_RULES.md)
    policy [--json]Print the governance policy (mirrors .github/ai-governance.json)
    verify-issue <draft.md>Gate a local issue draft; exit 1 / E_POLICY on a violation (proposes a runtime dependency, omits a reproduction code block). Missing recommended fields surface as warnings
    +
    +

    verify-issue is a pure, fully offline validator — no GitHub or network access. A passing check is necessary but not sufficient: the human review gate always applies.

    +
    +

    pdfnative fill (v1.3.0)#

    +

    Fill, flatten or export an AcroForm. The three modes compose into a round trip: +export the current values, edit the JSON, feed it back.

    +
    pdfnative fill --input form.pdf --export --output values.json   # read
    +pdfnative fill --input form.pdf --data values.json --output filled.pdf
    +pdfnative fill --input filled.pdf --flatten --output flat.pdf   # freeze
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultPurpose
    --data <file>Values JSON. Required unless --flatten or --export
    --flattenoffFlatten after filling, or flatten the existing values
    --exportoffRead-only: emit current values in --data shape
    --forceoffFlatten even when a signed signature field is present
    --on-unknown <mode>throwthrow or ignore for unknown field names
    --need-appearancesoffSet /NeedAppearances to allow non-WinAnsi values
    --password <pass>Open an encrypted form (env PDFNATIVE_PASSWORD)
    --dry-runoffValidate and enumerate fields without writing
    +

    Flattening a signed document invalidates the signature, which is why --force +exists rather than it being silent.

    +

    pdfnative encrypt (v1.3.0)#

    +

    Re-secure an existing PDF. --owner-password is required; without a +--user-password the document opens with no prompt and the owner password +only governs permissions.

    +
    pdfnative encrypt --input report.pdf --output secure.pdf \
    +  --owner-password "$OWNER" --user-password "$USER" \
    +  --algorithm aes-256 --permissions print,extract
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultPurpose
    --owner-password <pass>Required. Env PDFNATIVE_ENCRYPT_OWNER_PASS
    --user-password <pass>Password needed to open. Env PDFNATIVE_ENCRYPT_USER_PASS
    --algorithm <alg>aes-128aes-128 or aes-256
    --permissions <list>print,extractComma-separated: print, copy, modify, extract — without the flag, print and extract are allowed, copy and modify denied
    --password <pass>Open an already-encrypted source (password rotation)
    --drop-annotationsoffDrop all annotations; the default keeps URI links
    --max-output-size <n>256 MiBCap on the assembled size
    --streamoffStream the output (--chunk-size N)
    +

    pdfnative decrypt (v1.3.0)#

    +

    Remove encryption, given the password. Reads RC4, AES-128 and AES-256 sources.

    +
    pdfnative decrypt --input secure.pdf --password "$PASS" --output plain.pdf
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultPurpose
    --password <pass>Document password. Env PDFNATIVE_PASSWORD
    --drop-annotationsoffDrop all annotations; the default keeps URI links
    --max-output-size <n>256 MiBCap on the assembled size
    --streamoffStream the output (--chunk-size N)
    --dry-runoffValidate without writing
    +

    pdfnative extract-text (v1.3.0)#

    +

    Reading-order Unicode text from an existing PDF. ndjson emits one JSON object +per page, which makes it a natural feed for a RAG ingestion pipeline.

    +
    pdfnative extract-text --input paper.pdf --format ndjson --runs > pages.ndjson
    +pdfnative extract-text --input paper.pdf --pages 1,3,5-7 --format text
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultPurpose
    --format, -f <fmt>texttext, json, or ndjson (one object per page)
    --pages <selector>all1-based selector, e.g. 1,3,5-7
    --runsoffInclude positioned runs { text, x, y, fontSize, fontName }
    --password <pass>Extract from an encrypted PDF
    --max-length <n>16000000Hard cap on total characters; 0 disables
    --summaryoff(json) Emit only { pages, characters }
    --fields <paths>(json) Comma-separated dot-paths to keep
    +

    Text comes from the /ToUnicode mapping, so it is real Unicode rather than +glyph indices. A page whose content decodes entirely to U+FFFD is reported as +not extractable rather than returning noise.

    +

    pdfnative doctor (v1.3.0)#

    +

    Environment and capability preflight. The first thing to run in a new +environment, and the first thing an agent should call before planning work.

    +
    pdfnative doctor              # human-readable
    +pdfnative doctor --format json --pretty
    +
    + + + + + + + + + + + + + + + + + + +
    FlagDefaultPurpose
    --format, -f <fmt>texttext or json
    --prettyoffIndented JSON even under the global --json
    +

    pdfnative batch#

    +

    Renders every JSON file in a directory to PDF in parallel, reusing the full render pipeline.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultDescription
    --input-dir <dir>(required)Directory of *.json documents
    --output-dir <dir>(required)Destination directory for *.pdf (created if absent)
    --concurrency <n>4Bounded parallelism
    --fail-fastoffAbort on the first failure
    --dry-run (v1.1.0)offValidate every input without writing output
    --json / --summary (v1.1.0)offMachine-readable per-file report; --summary emits { total, succeeded, failed }
    +
    pdfnative batch --input-dir inputs/ --output-dir outputs/ --tagged pdfa2b --concurrency 4
    +
    +

    pdfnative schema#

    +

    Prints a versioned JSON Schema (Draft 2020-12) for a CLI input/output shape, so agents can self-validate before invoking a command. The $id embeds the CLI version.

    +
    pdfnative schema render            # input schema for `render`
    +pdfnative schema inspect-summary   # compact inspect verdict shape
    +pdfnative schema list              # enumerate every subject
    +
    +

    Subjects: render, inspect, verify, batch, annotate (v1.2.0), govern-verify (v1.2.0), extract-text, fill, form-export, status, manifest, doctor (all v1.3.0), and the compact inspect-summary / verify-summary / batch-summary shapes. pdfnative schema list enumerates them all.

    +

    pdfnative completion#

    +

    Emits a shell-completion script: pdfnative completion bash|zsh|fish|powershell (PowerShell added in v1.3.0).

    +
    +

    Agent-native automation contract#

    +

    v1.1.0 makes the CLI deterministic to drive from autonomous AI agents and CI pipelines. The full contract is documented in pdfnative-cli AGENTS.md.

    +
      +
    • Global --json envelope. Any command run with --json emits a single machine-readable object on stderr: { ok: false, command, error: { code, message } } on failure, and a { ok: true, … } status line for render / sign / batch on success (since v1.3.0, every write command emits the success line). stdout stays reserved for the primary artifact (PDF, report, schema, script).
    • +
    • Stable E_* error codes on every failure: E_USAGE, E_INPUT, E_PARSE, E_IO, E_SIGN, E_VERIFY_FAILED, E_CHECK_FAILED, E_POLICY (v1.2.0 — governance-gate failure), E_UNSUPPORTED, E_PASSWORD (v1.3.0 — encrypted PDF: password missing or incorrect), E_RUNTIME. Numeric exit codes (0/1/2) are unchanged.
    • +
    • --dry-run for render, sign, batch, and — since v1.2.0 — merge, split, extract, and annotate (v1.3.0 extends it to fill, encrypt and decrypt) — fully validate inputs (and, for sign, parse credentials and prepare the PDF) without producing output.
    • +
    • Token-economy output projection (inspect / verify / batch / extract-text): stdout JSON is compact by default under --json (--pretty opts back into the human 2-space form), --summary emits a canonical minimal verdict, and --fields a,b.c projects the result to named dot-paths. Typically ~90 % fewer output tokens with no loss of the fields agents branch on. Non---json human output is unchanged.
    • +
    +
    # Agent-friendly: compact verdict on stdout, structured status on stderr
    +pdfnative inspect --input report.pdf --json --summary 2>status.json
    +
    +# Branch on a stable error class
    +pdfnative verify --input report.pdf --json --strict \
    +  || echo "failed: $(jq -r .error.code status.json)"
    +
    +
    +

    Recipes#

    +

    Render → sign → verify → inspect, in a single chain#

    +
    cat report.json \
    +  | pdfnative render --tagged pdfa2b --compress \
    +  | pdfnative sign --reason "Approved" \
    +  | tee signed.pdf \
    +  | pdfnative verify --strict --trust ca.pem
    +pdfnative inspect --input signed.pdf --check pdfa --check signed
    +
    +

    Encrypted PDF/A-3 hybrid invoice (Factur-X / ZUGFeRD)#

    +
    pdfnative render \
    +  --input invoice.json --output invoice.pdf \
    +  --tagged pdfa3b \
    +  --attachment factur-x.xml:application/xml:Source:"Structured invoice data" \
    +  --footer-center "Page {page} / {pages}"
    +
    +

    Encrypted distribution copy#

    +
    pdfnative render \
    +  --input contract.json --output contract.encrypted.pdf \
    +  --encrypt-algorithm aes256 \
    +  --encrypt-permissions print
    +# PDFNATIVE_ENCRYPT_OWNER_PASS read from the env — never on the command line
    +
    +

    Multilang fonts#

    +

    For the 22 bundled scripts (plus latin, emoji, color-emoji, math), no wrapper is needed — --font registers each bundled font inside the CLI process:

    +
    echo '{"blocks":[{"type":"paragraph","text":"สวัสดี こんにちは"}]}' \
    +  | pdfnative render --font th --font ja --lang th,ja -o out.pdf
    +
    +

    For a font you ship yourself (not bundled), the CLI cannot use it: in-memory font registration does not cross a process boundary, so a wrapper that registers fonts and then spawns the CLI does not work. Call the library directly instead:

    +
    // render-custom-font.mjs
    +import { registerFont, loadFontData, buildDocumentPDFBytes } from 'pdfnative';
    +import { writeFile } from 'node:fs/promises';
    +
    +registerFont('th', () => import('./fonts/my-thai-data.js'));
    +const th = await loadFontData('th');
    +if (!th) throw new Error('Thai font failed to load');
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Thai demo',
    +  blocks: [{ type: 'paragraph', text: 'สวัสดี' }],
    +  fontEntries: [{ fontData: th, fontRef: '/F3', lang: 'th' }], // /F1 and /F2 are reserved
    +});
    +await writeFile('out.pdf', bytes);
    +
    +

    CI assertion (GitHub Actions)#

    +
    - name: Render and assert PDF/A + signed
    +  run: |
    +    pdfnative render --input data/q1.json --output dist/q1.pdf --tagged pdfa2b
    +    pdfnative sign  --input dist/q1.pdf  --output dist/q1.signed.pdf
    +    pdfnative verify --input dist/q1.signed.pdf --strict
    +    pdfnative inspect --input dist/q1.signed.pdf --check pdfa --check signed
    +
    +

    Batch-render a directory of JSON files#

    +
    for f in inputs/*.json; do
    +  pdfnative render --input "$f" --output "outputs/$(basename "$f" .json).pdf" --tagged pdfa2b
    +done
    +
    +
    +

    Security model#

    +

    pdfnative-cli is built with the same zero-trust posture as the underlying library:

    +
      +
    • No eval, no Function, no dynamic code — input JSON is parsed via the standard JSON.parse with a 50 MB cap to prevent memory exhaustion.
    • +
    • Path traversal protection — all --input / --output / --key / --cert / --cert-chain / --trust / --layout / --attachment / --watermark-image paths are validated against .. segments before any file system access.
    • +
    • Secrets never loggedloadPem / loadPemChain surface only generic error messages on parse failure; raw key material never appears in CliError messages or stderr. Encryption passwords are never echoed.
    • +
    • Layout-file injection blockedattachments[].data fields embedded in --layout JSON are stripped on load. Binary attachment payloads must come from --attachment <path> so the CLI can apply path validation.
    • +
    • Env-var precedence for secretsPDFNATIVE_SIGN_KEY / PDFNATIVE_SIGN_CERT / PDFNATIVE_SIGN_CHAIN / PDFNATIVE_ENCRYPT_OWNER_PASS / PDFNATIVE_ENCRYPT_USER_PASS are preferred over file-path flags so secrets never enter shell history.
    • +
    • Stdin/stdout safe — binary streams are passed through without interpretation; no shell-quoting issues.
    • +
    • NPM provenance — every published version is signed via GitHub Actions OIDC. Verify with npm audit signatures.
    • +
    +

    The CLI does not open network connections, write to system directories outside the working directory, or load arbitrary code. It only reads the files you point it at.

    +
    +

    Comparison with the library API#

    +

    The CLI now covers nearly the full library surface; only Web Worker offloading remains library-only.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FeatureCLI v1.3.0Library
    Document rendering (13 block types)
    Streaming output--stream / --stream-truebuildDocumentPDFStream() / buildDocumentPDFStreamTrue()
    Configurable block cap--max-blockslayout.maxBlocks
    PDF/A conformance (1b, 2b, 2u, 3b)--taggedtagged: '…'
    Digital signatures (RSA-SHA256)signPdfBytes()
    Digital signatures (ECDSA-SHA256)--algorithm ecdsa-sha256signPdfBytes()
    Native constant-time signing✅ default (--pure-crypto opts out; the native provider is a CLI utility)setCryptoProvider() (accepts any CryptoProvider)
    Inspection / metadataPdfReader
    PDF/UA structural validationinspect --pdfuavalidatePdfUA()
    Annotation listinginspect --annotationsgetAnnotations()
    Signature verification (CMS/PKCS#7)verify (real CMS, RSA + ECDSA — implemented in the CLI)openPdf() + verifyCertSignature() only (X.509 certificate-signature check, not CMS PDF-signature verification)
    PAdES-T timestamp + OCSP/CRL revocationverify --revocation (implemented in the CLI)
    Encryption (AES-128/256)--encrypt-*encryption: {…}
    Watermarks--watermark-*watermark: {…}
    PDF/A-3 attachments--attachmentattachments: [...]
    22 scripts + COLRv1 emoji + math--font / --langregisterFont() / loadFontData()
    Page-tree editing (merge / split / extract)merge / split / extractmergePdfs() / splitPdf() / extractPages()
    Markup annotationsannotatePdfModifier.addAnnotation() / buildAnnotationBody()
    Bookmarks / outlinerender --outlineoutline: '…'
    Layout introspection / debugrender --inspect-layout / --debug-layoutinspectDocumentLayout() / layout.debug
    AI-governance / HITL gategovern (draft validation via the CLI's validateGovernanceDraft())— (contract files in the repo; the core's check is a repo script, not a published API)
    Parallel batch renderbatch— (compose render)
    JSON Schema exportschema— N/A
    Agent-native --json/E_*/--dry-run— N/A
    Table-centric variant--variant tablebuildPDFBytes()
    Full PdfLayoutOptions--layout file.json
    Web Worker offloading❌ N/ApdfWorker.ts
    +
    +

    Examples — ready-to-run#

    +

    The samples/ directory in the CLI repository ships 40+ ready-to-run examples organized by feature:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CategoryWhat it shows
    render/document/Minimal document, all blocks reference, invoice, technical spec, multi-page report
    render/table/Project status, financial summary
    render/table-variant/PdfParams-shaped financial ledger via --variant table
    render/barcode/QR code, Code 128, EAN-13
    render/form/Contact form, survey
    render/toc/Auto-generated table of contents with /GoTo links
    render/link/Resource directory with hyperlinks
    render/watermark/Draft / Confidential watermarks
    render/layout/US Letter, A5 portrait, A4 landscape
    render/pdfa/PDF/A-1b, 2b, 3b archival conformance
    render/encryption/AES-128 password-protected PDF
    render/headers-footers/Page templates with {page}/{pages}/{date}/{title}
    render/attachments/PDF/A-3 hybrid invoice with embedded XML (Factur-X / ZUGFeRD)
    render/multilang/Multilang rendering via the bundled --font shortcuts
    render/chart/Native vector charts (bar, line, pie, donut) (v1.3.0)
    render/math/Noto Sans Math font routing (v1.2.0)
    render/outline//Outlines bookmark trees (--outline auto and explicit JSON) (v1.2.0)
    render/table-smart/Smart-table flags (--table-wrap, --zebra, …) (v1.3.0)
    render/font/, render/template/, render/watch/, render/inspect-layout/Font shortcuts, templates, watch mode, layout introspection
    fill/AcroForm fill / flatten / export round trip (v1.3.0)
    encrypt/Encrypt / decrypt an existing PDF (v1.3.0)
    extract-text/Reading-order text extraction (text / json / ndjson) (v1.3.0)
    merge/, split/, extract/, annotate/Page-tree editing and markup annotations (v1.2.0)
    sign/Bash + PowerShell signing scripts (basic + with metadata)
    inspect/JSON & text inspection, --verbose --pages, --check pdfa
    verify/Self-signed verification, strict-mode CI gating
    batch/, doctor/, govern/, completion/, agent/, config/Automation, preflight, governance, shell completion, agent patterns
    streaming/200-section document via streaming render
    +

    Render them all at once:

    +
    git clone https://github.com/Nizoka/pdfnative-cli
    +cd pdfnative-cli
    +node samples/run-all.js
    +
    +
    +

    Release history#

    +

    What's new in v1.3.0#

    +

    v1.3.0 is built on the pdfnative 1.6 engine (pin ^1.6.0) and surfaces its engine additions as five new commands (extract-text, fill, encrypt, decrypt, doctor), native vector charts in render, and password / re-encryption / constant-memory streaming on the page-tree commands. Fixes a silent render --encrypt no-op. 100 % backward-compatible with v1.2.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Areav1.2.0v1.3.0
    Commands12 commands adds fill (fill / flatten / export AcroForms via an incremental save), encrypt, decrypt, extract-text (reading-order Unicode text: text | json | ndjson), and doctor (offline environment / capability preflight)
    Chartsthe engine's chart document block (bar, barH, line, pie, donut — the pdfnative 1.6 set) renders as pure vector path operators through render
    Encryption vocabulary--encrypt-* flags on render onlyunified vocabulary--encrypt [aes-128|aes-256], --owner-password, --user-password, --permissions print,copy,modify,extract — shared by render, merge, split and extract (legacy --encrypt-* flags kept as aliases)
    Page-tree commandsplaintext sources only--password reads encrypted sources; --encrypt re-encrypts the output; --stream (+ --chunk-size) streams the output with constant memory
    Inspectionmetadata, PDF/UA, annotationsadds inspect --form-fields, --encryption and --password
    Shell completionbash, zsh, fishadds powershell
    Agent contract--json / E_* / --dry-run / schemasadds the stable E_PASSWORD code and an agent capability manifest (schema manifest + llms.txt)
    Compatibilitypdfnative ^1.5.0pdfnative ^1.6.0
    +

    Full changelog: pdfnative-cli release notes v1.3.0.

    +

    What's new in v1.2.0#

    + +

    v1.2.0 lands the pdfnative 1.5.0 engine's page-tree and annotation APIs on the CLI as five new commands, adds document bookmarks, a math font, layout introspection, native constant-time signing, and — for autonomous agents — surfaces pdfnative's AI-governance / Human-in-the-Loop (HITL) contract. 100 % backward-compatible with v1.1.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Areav1.1.0v1.2.0
    Commandsrender, sign, inspect, verify, batch, schemaadds merge, split, extract (page-tree), annotate (markup), and govern (rules / policy / verify-issue)
    Bookmarksrender --outline auto derives a /Outlines bookmark tree from headings; --outline <tree.json> supplies an explicit OutlineItem[]
    Math fontrender --font math registers the bundled Noto Sans Math font; pdfnative auto-routes math-operator / geometric-shape code points to it
    Layout toolingrender --inspect-layout emits a LayoutInspection JSON report; --debug-layout [margins,content,cells] overlays layout guides on a normal PDF
    Signingpure-JS bignum CMSnative node:crypto by default (constant-time, side-channel-resistant RSA/ECDSA); --pure-crypto opts back into the portable pure-JS path
    Inspectionmetadata, PDF/UAinspect --annotations lists markup + link annotations; /PageLabels are reported automatically when present
    Agent / governance--json/E_*/--dry-runadds the stable E_POLICY code; schema gains annotate + govern-verify subjects; --dry-run now also covers merge / split / extract / annotate
    Compatibilitypdfnative ^1.3.0pdfnative ^1.5.0
    +

    Full changelog: pdfnative-cli release notes v1.2.0.

    +

    Previously in v1.1.0#

    + +

    v1.1.0 is built on pdfnative 1.3.0 and surfaces its new engine capabilities through the CLI, plus a full agent-native automation contract. 100 % backward-compatible with v0.3.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Areav0.3.0v1.1.0
    Fonts--font {latin,emoji}22 Unicode scripts + COLRv1 colour emoji--font/--lang allow-list covers every bundled font (latin, emoji, color-emoji, and the 22 script codes incl. Telugu te, Sinhala si, Tibetan bo, Khmer km, Myanmar my, Amharic am)
    Streaming--stream (single-pass), page-by-pageadds --stream-true — true constant-memory streaming via buildDocumentPDFStreamTrue / buildPDFStreamTrue; the joined binary never materialises
    Block caphard-coded ceiling--max-blocks <n> exposes layout.maxBlocks (default 100 000) for very large reports
    AccessibilityPDF/UA (ISO 14289-1) structural validatorinspect --pdfua and --check pdfua as a CI accessibility gate
    Agent contractglobal --json status/error envelope on stderr, stable E_* error codes, --dry-run, token-economy --summary / --fields projection, compact JSON by default
    Commandsrender, sign, inspect, verifyadds batch (parallel directory render) and schema (JSON Schema export)
    Supply chainprovenanceadds a CycloneDX SBOM (sbom.cdx.json) attached to every release + OpenSSF Scorecard badge
    Compatibilitypdfnative ^1.1.0pdfnative ^1.3.0
    +

    Full changelog: pdfnative-cli release notes v1.1.0.

    +

    Previously in v0.3.0#

    +

    v0.3.0 finished the digital-signature story and added three iteration-friendly render flags. 100 % backward-compatible with v0.2.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Areav0.2.0v0.3.0
    sign algorithmRSA-SHA256 only (ECDSA stub)RSA-SHA256 and ECDSA-SHA256 — fully wired via parseEcPrivateKey (SEC1 / PKCS#8 P-256)
    sign placeholderRequired a prior prepare_signature_placeholder callAuto-injection — CLI detects PDFs with no AcroForm signature field and adds /Sig via a single incremental update
    verify scopeByte-range integrity + cert chainReal CMS/PKCS#7 verification — signature value (RSA + ECDSA), message digest, certificate chain, trust roots, RFC 3161 timestamp detection
    render --watchRe-render on input change (200 ms debounce, stderr-only logs)
    render --template <file.json>Deep-merge a base template under stdin / --input
    render --font <name>Bundled font shortcut (latin, emoji)
    Compatibilitypdfnative ^1.0.5pdfnative ^1.1.0
    +

    Previously in v0.2.0#

    +

    The v0.2.0 release expanded the CLI from ~10 flags to a near-complete projection of the pdfnative v1.0.5 surface, while remaining 100 % backward-compatible with v0.1.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Areav0.1.0v0.2.0
    Layout--conformance onlyHybrid model — high-frequency knobs as flags, full PdfLayoutOptions via --layout file.json
    PDF/A--conformance 1b|2b|3b--tagged none|pdfa1b|pdfa2b|pdfa2u|pdfa3b (--conformance deprecated)
    Encryption--encrypt-owner-pass, --encrypt-user-pass, --encrypt-algorithm, --encrypt-permissions (env-var precedence)
    Watermarks--watermark-text/-image/-opacity/-angle/-color/-font-size/-position
    Headers / footers--header-{l,c,r}, --footer-{l,c,r} with {page}/{pages}/{date}/{title} placeholders
    PDF/A-3 attachments--attachment <path>[:mime[:rel[:desc]]] (repeatable)
    Multilingual fonts--lang th,ja,ar (at the time, required a wrapper registering fonts via registerFont(); superseded by --font in v1.1.0)
    verify commandn/abyte-range integrity + cert chain + --trust roots
    +
    +

    Migration v0.3.0 → v1.1.0#

    +

    100 % backward-compatible. Every v0.3.0 invocation continues to produce a byte-equivalent PDF. New, optional opportunities:

    +
    # 1. True constant-memory streaming for very large reports
    +- pdfnative render --input big.json --output big.pdf --stream
    ++ pdfnative render --input big.json --output big.pdf --stream-true
    +
    +# 2. CI accessibility gate (PDF/UA)
    ++ pdfnative inspect --input report.pdf --check pdfua
    +
    +# 3. Agent-native, token-frugal output
    ++ pdfnative inspect --input report.pdf --json --summary
    +
    +# 4. Parallel directory render
    ++ pdfnative batch --input-dir inputs/ --output-dir outputs/ --concurrency 4
    +
    +

    Migration v0.2.0 → v0.3.0#

    +

    100 % backward-compatible. Three forward-looking opportunities:

    +
    # 1. ECDSA signing now works without a workaround
    +- pdfnative sign -i in.pdf -o out.pdf --algorithm rsa-sha256 ...
    ++ pdfnative sign -i in.pdf -o out.pdf --algorithm ecdsa-sha256 \
    ++   --key ec-key.pem --cert ec-cert.pem
    +
    +# 2. Sign without a prior placeholder step
    +- pdfnative sign -i with-placeholder.pdf -o signed.pdf ...
    ++ pdfnative sign -i any-pdf.pdf -o signed.pdf ...   # placeholder auto-injected
    +
    +

    Migration v0.1.0 → v0.2.0#

    +

    100 % backward-compatible — every v0.1.0 invocation continues to produce a byte-equivalent PDF, modulo a one-line stderr notice for --conformance. All v0.1.0 exit codes and JSON shapes are preserved; new inspect JSON fields are additive only.

    +

    The only soft change you should plan for:

    +
    - pdfnative render --input doc.json --output report.pdf --conformance 2b
    ++ pdfnative render --input doc.json --output report.pdf --tagged pdfa2b
    +
    +

    --conformance will be removed in v1.0.0 of the CLI.

    +
    +

    Troubleshooting#

    +

    command not found: pdfnative#

    +

    You installed via npx (one-shot) and not globally. Either prepend npx to every invocation, or run npm install --global pdfnative-cli.

    +

    JSON parse error: input too large#

    +

    The CLI caps input JSON at 50 MB to prevent memory exhaustion. For very large documents, either split the document into multiple PDFs or use the library directly with the streaming API.

    +

    Error: invalid private key#

    +

    Both RSA PKCS#1 and ECDSA P-256 keys are accepted, but they must be PEM-encoded. Convert DER to PEM with openssl pkcs8 -topk8 -in key.der -out key.pem -nocrypt. As of v0.3.0, ECDSA support in sign is fully wired (SEC1 / PKCS#8 P-256 via parseEcPrivateKey).

    +

    Encryption requires an owner password. Provide --owner-password <pass> or $PDFNATIVE_ENCRYPT_OWNER_PASS.#

    +

    Any encryption flag was set without an owner password. Provide it via --owner-password <pass> (legacy alias: --encrypt-owner-pass) or — recommended — the PDFNATIVE_ENCRYPT_OWNER_PASS env var so it never enters shell history.

    +

    Error: --tagged pdfa* and --encrypt-* are mutually exclusive#

    +

    ISO 19005-1 §6.3.2 forbids encryption in PDF/A. Pick one — either an archival PDF/A document, or an encrypted distribution copy, but not both.

    +

    ENOENT: no such file or directory, 'D\' (Windows)#

    +

    This was a v0.1.0 / pre-v0.2.0 regression: --attachment D:\file.xml was split at the drive-letter colon. Fixed in v0.2.0 — make sure you're on pdfnative-cli@^0.2.0.

    +

    Layout file is ignored when I also pass CLI flags#

    +

    That is the intended precedence: CLI flags > --layout file > pdfnative defaults. To merge nested objects (e.g. a watermark in the layout file plus a --watermark-text on the CLI), the CLI now correctly merges params.layout with CLI-derived flags as of v0.2.0 (previously the JSON-embedded layout could be silently dropped — fixed).

    +

    --lang th does not produce Thai glyphs#

    +

    --lang only activates fonts that are registered in the CLI process — pass --font th as well so the bundled Noto Thai module is registered (--font covers all 22 bundled scripts, latin, emoji, color-emoji, math). A wrapper script that registers fonts and then spawns the CLI does not work: in-memory registration does not cross a process boundary. For a non-bundled font you ship yourself, call the library directly — see Recipes → Multilang fonts.

    +

    Signed PDF fails Adobe verification#

    +

    Ensure your certificate's signing-key usage extension includes digitalSignature (key usage 0). Self-signed certificates work for testing but require the validator to trust the issuer — pass --trust ca-root.pem to verify for self-signed setups.

    +
    +

    Resources#

    + +
    +

    Citation#

    +

    If you use the CLI in research or academic pipelines, cite both repositories:

    +
    @software{pdfnative_cli_2026,
    +  title  = {pdfnative-cli: Official CLI for the pdfnative PDF generation library},
    +  author = {Nizoka},
    +  year   = {2026},
    +  url    = {https://github.com/Nizoka/pdfnative-cli},
    +  license = {MIT}
    +}
    +
    + +
    +
    + + + + + + + + + + + + diff --git a/docs/guides/colour-emoji-cli.html b/docs/guides/colour-emoji-cli.html index cdc943c..2b25d83 100644 --- a/docs/guides/colour-emoji-cli.html +++ b/docs/guides/colour-emoji-cli.html @@ -1,85 +1,285 @@ - - - - - - Colour-emoji font CLI — pdfnative - - - + + + + + + Colour-emoji font CLI — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Colour-emoji font CLI

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Colour-emoji font CLI

    +
    + +

    Colour-emoji font CLI (pdfnative-build-emoji-font)

    +
    +

    New in v1.4.0, expanded in v1.6.0 and v1.7.0. pdfnative ships a curated +1167-glyph colour-emoji module by default (plus 73 flag/ZWJ sequences since +v1.7.0). When you need more emoji — up to the full ~3 600-glyph set, or +skin-tone sequence variants — the +pdfnative-build-emoji-font CLI generates a data module containing exactly the +glyphs and sequences you choose. It's bundled with the pdfnative package, so +any user can run it with npx — no extra install, no editing library source.

    +
    +

    Why a CLI instead of bundling everything?#

    +

    The complete Noto Color Emoji font is ~24 MB and resolves to a ~32 MB data +module. Bundling that into the npm package would penalise every install, +including the vast majority of users who only ever need a handful of emoji. The +lean curated subset stays the default; the CLI lets you opt into precisely the +coverage you need, when you need it. This is the same deterministic build core +that produces the bundled module — you get identical, reproducible output.

    +

    TL;DR#

    +
    # Full coverage — download + checksum-verify the font, then build every glyph:
    +npx pdfnative-build-emoji-font --download --all --out ./emoji-full.js
    +
    +# Exactly the emoji you need (hex scalars and/or inclusive ranges):
    +npx pdfnative-build-emoji-font --download \
    +  --ranges 1F600-1F64F,2600-27BF --codepoints 2764,1F680 --out ./emoji.js
    +
    +# Offline: point at a font you already have on disk:
    +npx pdfnative-build-emoji-font --ttf ./NotoColorEmoji-Regular.ttf --all
    +
    +# Flag/ZWJ sequences (v1.7.0) — the curated sets plus any skin-tone form you need:
    +npx pdfnative-build-emoji-font --download --sequences all \
    +  --sequence-list 1F468-1F3FB-200D-1F4BB,1F469-1F3FD-200D-2695-FE0F --out ./emoji.js
    +
    +

    Then register the module you generated, load it, and pass it via fontEntries:

    +
    import { registerFont, loadFontData, buildDocumentPDFBytes } from 'pdfnative';
    +
    +registerFont('emoji', () => import('./emoji-full.js'));
    +const emoji = await loadFontData('emoji');
    +if (!emoji) throw new Error('emoji font failed to load');
    +
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Emoji',
    +  blocks: [{ type: 'paragraph', text: 'Ship it 🚀🎉🥳🦄🌈' }],
    +  fontEntries: [{ fontData: emoji, fontRef: '/F3', lang: 'emoji' }], // /F1 and /F2 are reserved
    +});
    +
    +

    Getting the source font#

    +

    The CLI needs a COLRv1/CPAL colour font — the canonical one is +NotoColorEmoji-Regular.ttf (OFL-1.1). Two ways to provide it:

    +
      +
    • --download fetches it from the official Google Fonts repository and +verifies its SHA-256 against the build pdfnative was tested with. A mismatch is +a transparent warning (Google periodically ships newer Unicode revisions), +not a hard failure — the download is still used.
    • +
    • --ttf <path> uses a font already on disk (fully offline, no network).
    • +
    +
    +

    Only the pinned official Google Fonts URL is ever contacted; the CLI never +fetches arbitrary user-supplied URLs.

    +
    +

    Selecting glyphs#

    +

    Combine these freely; selections are merged, de-duplicated and sorted. If you +pass none, the curated set is used.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagMeaning
    --allEvery colour glyph in the font (large module).
    --preset curatedpdfnative's lean 1167-glyph default set.
    --preset allSame as --all.
    --codepoints <list>Comma-separated hex scalars: 1F600,1F680,2764. U+, 0x, # prefixes are tolerated.
    --ranges <list>Comma-separated inclusive hex ranges: 1F600-1F64F,2600-27BF.
    +

    Selecting sequences (v1.7.0)#

    +

    Multi-codepoint emoji — flags (regional-indicator pairs) and ZWJ sequences — +are selected separately from single glyphs. Each requested sequence is +resolved through the font's GSUB ligature lookups to a single colour glyph +and emitted in the module's sequences table; both the VS-16 and the +VS-16-free spelling register to the same glyph. Default: none (the +pre-1.7 output shape, plus an inert sequences = null export).

    + + + + + + + + + + + + + + + +
    FlagMeaning
    --sequences <preset>flags (the curated 51-flag set), zwj (the curated 22-sequence ZWJ set), all (both), or none.
    --sequence-list <list>Comma-separated entries: a 2-letter country code (FR, DE) and/or hyphen-joined hex scalars (1F468-200D-1F680, 1F469-1F3FD-200D-2695-FE0F). Skin-tone forms welcome.
    +

    Selections merge and de-duplicate, so --sequences flags --sequence-list 1F469-1F3FB-200D-2695-FE0F is valid. The bundled npm module already carries +the curated 51 + 22 set — reach for the CLI when you need skin-tone variants +or sequences beyond it. Note that flags render flat (Noto's wave-shading +overlay uses a COLRv1 compositing mask with no PDF equivalent — see the +colour-emoji guide).

    +

    Output options#

    + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultMeaning
    --out <path>./noto-color-emoji-data.jsOutput .js path. A sibling .d.ts is written next to it.
    --font-name <name>NotoColorEmoji-RegularEmbedded PostScript font name.
    --types <path>pdfnativeThe type import used in the generated .d.ts (e.g. 'pdfnative' or a relative path).
    +

    The generated module exports the same shape as the bundled one +(metrics, cmap, widths, pdfWidthArray, colorGlyphs, sequences, +ttfBase64, …), so it drops straight into +registerFont('emoji', () => import('…')).

    +

    Verifying the result#

    +

    Pair the CLI with validateFontData() to sanity-check a +freshly generated module before shipping it:

    +
    import { validateFontData } from 'pdfnative';
    +import * as emoji from './emoji-full.js';
    +
    +const report = validateFontData(emoji);
    +if (!report.valid) console.error(report.errors);
    +
    +

    Full option reference#

    +
    pdfnative-build-emoji-font — colour-emoji font data module generator
    +
    +Source font (one of):
    +  --ttf <path>            Path to a COLRv1/CPAL colour font.
    +  --download              Fetch the official Noto Color Emoji (OFL-1.1) and verify checksum.
    +
    +Glyph selection (combine freely; default: --preset curated):
    +  --all                   Every colour glyph in the font.
    +  --preset <curated|all>  Named selection.
    +  --codepoints <list>     Comma-separated hex scalars, e.g. 1F600,1F680,2764.
    +  --ranges <list>         Comma-separated inclusive hex ranges, e.g. 1F600-1F64F.
    +
    +Sequence selection (v1.7.0; default: none):
    +  --sequences <preset>    flags | zwj | all | none — bundle the curated flag
    +                          and/or ZWJ sequence sets (GSUB-resolved ligatures).
    +  --sequence-list <list>  Comma-separated country codes and/or hyphen-joined
    +                          hex sequences, e.g. FR,DE,1F468-200D-1F680,
    +                          1F469-1F3FD-200D-2695-FE0F (skin tones welcome).
    +
    +Output:
    +  --out <path>            Output .js path (a sibling .d.ts is written).
    +  --font-name <name>      Embedded PostScript font name.
    +  --types <path>          Type import used in the generated .d.ts.
    +
    +Other:
    +  -h, --help              Show this help.
    +
    +

    Licensing#

    +

    Noto Color Emoji is licensed under the SIL Open Font License 1.1 (OFL-1.1). +Generated modules embed a subset of that font; keep the OFL notice with any +redistributed module, exactly as you would for any other Noto data module that +pdfnative ships.

    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/colour-emoji.html b/docs/guides/colour-emoji.html index 8270fd4..5949383 100644 --- a/docs/guides/colour-emoji.html +++ b/docs/guides/colour-emoji.html @@ -1,85 +1,314 @@ - - - - - - Colour emoji (COLRv1) — pdfnative - - - + + + + + + Colour emoji (COLRv1) — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Colour emoji

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Colour emoji

    +
    + +

    Colour emoji (COLRv1)

    +
    +

    New in v1.3.0. pdfnative renders colour emoji natively — no rasterisation, no external dependency. Glyph colour layers become PDF Form XObjects with solid fills and axial/radial gradients. The monochrome emoji font is unchanged and remains the default.

    +
    +

    TL;DR#

    +
    import { registerFont, loadFontData, buildDocumentPDFBytes } from 'pdfnative';
    +
    +// Opt in to the curated colour-emoji subset (1167 glyphs + 73 sequences, ~4.5 MB).
    +registerFont('emoji', () => import('pdfnative/fonts/noto-color-emoji-data.js'));
    +
    +const emoji = await loadFontData('emoji');
    +if (!emoji) throw new Error('emoji font failed to load');
    +
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Colour emoji',
    +  blocks: [
    +    { type: 'paragraph', text: 'Status: 🟢 online · 🔴 offline · 🎉 launch day!' },
    +  ],
    +  fontEntries: [{ fontData: emoji, fontRef: '/F3', lang: 'emoji' }], // /F1 and /F2 are reserved
    +});
    +
    +

    The registerFont('emoji', …) call swaps the monochrome Noto Emoji font for +the COLR/CPAL colour build — but registration alone embeds nothing: you must +await loadFontData('emoji') and pass the result in fontEntries (as above) +or the emoji render as tofu. With the entry in place, everything else — +detection, multi-font run splitting, line breaking — is automatic.

    +

    How it works#

    +

    Noto Color Emoji is an OpenType COLR/CPAL font: each emoji codepoint maps +to a base glyph plus a colour glyph describing ordered paint layers. pdfnative +parses these tables with self-written, zero-dependency readers +(src/fonts/colr-parser.ts, +src/fonts/glyf-outline.ts) +and renders each colour glyph as a PDF Form XObject:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    COLR featurePDF mapping
    Solid layer (COLR v0 / PaintSolid)rg fill of the layer's glyf outline, clipped with W n
    Linear gradient (PaintLinearGradient)/ShadingType 2 axial shading + /ExtGState constant alpha
    Radial gradient (PaintRadialGradient)/ShadingType 3 radial shading
    Sweep gradient (PaintSweepGradient)flat-colour triangular wedges fanned around the centre (v1.4.0)
    Compositing (PaintComposite)separable blend modes mapped to PDF /BM (Multiply, Screen, Overlay, Darken, Lighten, …) (v1.4.0)
    CPAL paletteper-stop RGB(A) colours
    +

    Each unique emoji produces one indirect Form XObject, deduplicated and +forward-referenced into every page's /XObject resource dictionary. The text +run emits q s 0 0 s x y cm /CEm0 Do Q to place the glyph.

    +
    +

    Advanced compositing (v1.4.0). COLRv1 sweep (conic) gradients render as +native flat-shaded wedges, and PaintComposite separable blend modes map to +PDF /BM ExtGState operators. Structural Porter-Duff modes (Clear / Src / Dest / +Xor / …) and PaintMask fall back to the documented monochrome path — except +that since v1.7.0, a composite whose source subtree alone is unsupported +keeps its backdrop layers (this is what makes flags render flat instead of +monochrome).

    +
    +

    Opt-in, not default#

    +

    Colour emoji is opt-in for two reasons:

    +
      +
    1. Module size. The curated subset is ~4.5 MB (expanded to 1167 glyphs in +v1.6.0, plus 73 flag/ZWJ sequences in v1.7.0); bundling it by default would +bloat every consumer. Register it only when you need colour.
    2. +
    3. Byte stability. When no colour-emoji font is registered, documents are + byte-identical to the pre-colour-emoji path — the colour path is fully gated.
    4. +
    +

    To keep monochrome emoji instead, register the monochrome font:

    +
    registerFont('emoji', () => import('pdfnative/fonts/noto-emoji-data.js'));
    +
    +

    Coverage & limits#

    +

    The bundled module is a curated subset of 1167 common single-codepoint +emoji (expanded from 221 in v1.6.0): the complete Emoticons and Supplemental +Symbols & Pictographs blocks, Miscellaneous Symbols & Pictographs through +U+1F53D (nature, food, objects, hearts, office, av/ui symbols) plus clocks and +the emoji-presentation stragglers, and the complete assigned Transport & Map +block (U+1F680–1F6FF). Since v1.7.0 it also carries 73 multi-codepoint +sequences — 51 flags and 22 ZWJ sequences (see the next section). Symbols & +Pictographs Extended-A (U+1FA70–1FAFF) is +not bundled — use the CLI below. It ships pre-built because every file under the package +files allowlist is included in the npm tarball regardless of tree-shaking — the +full Noto Color Emoji build (thousands of glyphs, ~32 MB) would weigh down +every npm install, even for consumers who never touch emoji. The subset keeps +the install within a 5120 KB budget (actual: ~4.5 MB) while the lazy +() => import(...) keeps it out of bundles that don't reference it.

    +
    +

    Out of scope for the bundled subset: skin-tone-modified forms +(👍🏽, 👩🏻‍🚀, …) and any flag or ZWJ sequence beyond the curated 73. The +combinatorial skin-tone set is deliberately CLI territory — build a module +with --sequences / --sequence-list below to cover exactly the forms you +need. An uncovered sequence degrades to the per-codepoint behaviour +described further down (base emoji render, joiners drop) — never worse.

    +
    +

    To cover the full Noto Color Emoji set — or any custom selection — pdfnative +ships an official generator CLI, pdfnative-build-emoji-font, so you never have +to edit library source:

    +
    # Every colour glyph (~3 600), fetched + checksum-verified from Google Fonts:
    +npx pdfnative-build-emoji-font --download --all --out ./emoji-full.js
    +
    +# Or exactly the emoji you need (hex scalars and/or inclusive ranges):
    +npx pdfnative-build-emoji-font --download \
    +  --ranges 1F600-1F64F,2600-27BF --codepoints 2764,1F680 --out ./emoji.js
    +
    +

    Then register the module you generated:

    +
    registerFont('emoji', () => import('./emoji-full.js'));
    +
    +

    See the colour-emoji CLI guide for every flag, offline +usage with --ttf, and the checksum/licensing details.

    +

    Flag & ZWJ sequences (v1.7.0)#

    +

    Multi-codepoint emoji now render as single colour glyphs. The bundled +module resolves the font's GSUB ligature lookups at build time into a +sequences table (first codepoint → resolved ligature glyph), and the text +pipeline runs a longest-match pre-pass over it, so a flag pair or a ZWJ +sequence produces exactly one COLR Form XObject:

    +
      +
    • 51 flags — 🇪🇺 EU, 🇺🇳 UN, the G20 members, and other widely used +locales (regional-indicator pairs).
    • +
    • 22 ZWJ sequences — families (👨‍👩‍👧), professions (👩‍🚀, 👨‍💻), +❤️‍🔥, 🏳️‍🌈, 🏳️‍⚧️, 🏴‍☠️, 🐻‍❄️, 😮‍💨, and friends — +in their skin-tone-free RGI forms.
    • +
    +

    No API change is required: register the bundled colour-emoji module as usual +and write the sequences in your text. Three behaviours worth knowing:

    +
      +
    • VS-16 tolerance. Both the VS-16 spelling and the VS-16-free spelling of +a sequence match the same glyph — real-world text is inconsistent about +presentation selectors, so ❤️‍🔥 works with or without the invisible +U+FE0F.
    • +
    • Fallback is never worse. A sequence absent from the table degrades to +exactly the historical per-codepoint behaviour: unmatched joiners, +variation selectors, and skin-tone modifiers are dropped, and unmatched +regional indicators pass through the normal cmap lookup. A module without a +sequences table renders byte-identically to v1.6.0.
    • +
    • Flags render flat. Noto's flag glyphs stack a wave-shading overlay on +the flat artwork via a COLRv1 PaintComposite SRC_IN mask that has no PDF +equivalent. pdfnative degrades the composite to its supported backdrop, so +every flag renders as its flat (unwaved) artwork rather than falling back +to monochrome.
    • +
    +

    Skin-tone-modified sequences stay CLI-only — generate a module with +--sequences / --sequence-list (see the CLI guide).

    +

    Advanced compositing (v1.4.0)#

    +

    COLRv1 includes two paint types beyond solid + axial/radial gradients, and +pdfnative now maps both where a faithful PDF translation exists:

    + + + + + + + + + + + + + + + + + + + +
    COLRv1 featurePDF mapping (v1.4.0)
    Sweep / conic gradient (PaintSweepGradient, format 8)Flat-shaded triangular wedges fanned around the centre — no /Shading resource, pure path fills. Matrix rotation is folded into the start/end angles via Math.atan2.
    Composite (PaintComposite, format 32) — separable blend modesBackdrop + source layers, with the source tagged via a /BM (blend mode) /ExtGState: Normal, Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, Hue, Saturation, Color, Luminosity.
    Composite — structural Porter-Duff modes (SrcOver, DestIn, clipping masks, …)No exact PDF equivalent → the glyph falls back to the monochrome outline. Since v1.7.0, when only the composite's source subtree is unsupported, its partial layers roll back and the backdrop renders alone (best-effort) — Noto's flags degrade to their flat artwork this way.
    +

    Sweep wedges approximate the smooth conic sweep with a fan of flat-colour +triangles whose count scales with the angular span — close enough for emoji at +text sizes while staying within plain PDF path operators. Separable blend modes +are exactly the set PDF defines in ISO 32000-1 §11.3.5, so they round-trip +faithfully in any conformant viewer.

    +
    +

    PDF/A note: blend modes and constant-alpha /ExtGState are transparency +features that PDF/A-1b forbids. Use solid-layer emoji for archival documents.

    +
    +

    Limitations#

    +
      +
    • PaintMask and COLRv1 variable (animated) paints are not yet rendered; +glyphs using them fall back gracefully — to the backdrop layers when the +unsupported paint sits inside a composite's source subtree (v1.7.0), to the +monochrome outline otherwise. Tracked for a future release.
    • +
    • PDF/A: gradient transparency uses /ExtGState alpha, which PDF/A-1b +forbids. Use solid-layer emoji or a non-PDF/A document for colour gradients.
    • +
    +

    Variation selectors & skin-tone modifiers (v1.3.0)#

    +

    Text such as ❤️ carries an invisible VS-16 variation selector +(U+FE0F), and 👍🏽 carries a Fitzpatrick skin-tone modifier +(U+1F3FB–U+1F3FF); ZWJ sequences (👨‍👩‍👧) join several codepoints. Before +v1.3.0 these zero-width formatting characters could route to the Latin font and +render as .notdef tofu (the box). As of v1.3.0 they are dropped during +run-splitting when no registered font covers them, so the base emoji renders +cleanly. (Internally this is the isZeroWidthFormat(cp) predicate in the +shaping engine — it is not part of the published API surface.) Joiners are +still preserved when an Indic shaper font maps them.

    +

    Since v1.7.0, sequences covered by the registered font's sequences table +take a different path entirely: the whole sequence — joiners, selectors and +all — resolves to a single colour ligature glyph before run-splitting (see +Flag & ZWJ sequences above). The drop-during-run-splitting +behaviour described here remains the fallback for uncovered sequences.

    +

    Colour-glyph Form /BBox is also now computed from the glyph's transformed +contour bounds (v1.3.0), so emoji that dip below the baseline are no longer +clipped at the top or bottom.

    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/debugging.html b/docs/guides/debugging.html index fa5713e..fc8b09a 100644 --- a/docs/guides/debugging.html +++ b/docs/guides/debugging.html @@ -1,85 +1,209 @@ - - - - - - Layout debugging & inspection — pdfnative - - - + + + + + + Layout debugging & inspection — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Layout debugging

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Layout debugging

    +
    + +

    Layout debugging & inspection

    +
    +

    New in v1.5.0. Two complementary tools help you understand where +pdfnative places every block on the page: an opt-in visual overlay +(layout: { debug: true }) that draws margin / content / cell boxes straight +onto the PDF, and a programmatic inspection API +(inspectDocumentLayout()) that returns the per-page block geometry as plain +data. Both are byte-neutral: when debug is off, output is byte-identical +to previous releases.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes, inspectDocumentLayout } from 'pdfnative';
    +
    +const params = {
    +  title: 'Invoice',
    +  blocks: [
    +    { type: 'heading', level: 1, text: 'Invoice #42' },
    +    { type: 'paragraph', text: 'Thanks for your business.' },
    +  ],
    +};
    +
    +// 1. Visual overlay — margin / content / cell boxes drawn on the PDF
    +const pdf = buildDocumentPDFBytes(params, { debug: true });
    +
    +// 2. Programmatic geometry — no rendering, just data
    +const layout = inspectDocumentLayout(params);
    +for (const page of layout.pages) {
    +  for (const block of page.blocks) {
    +    console.log(page.index, block.type, block.x, block.top, block.width, block.height);
    +  }
    +}
    +
    +

    Visual overlay#

    +

    Pass debug: true (or a granular object) in the layout options:

    +
    buildDocumentPDFBytes(params, { debug: true });
    +
    +// or select exactly what you want to see:
    +buildDocumentPDFBytes(params, {
    +  debug: { showMargins: true, showContentBounds: true, showCells: false },
    +});
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    OptionColourDraws
    showMarginsbluethe page margin box (content area boundary)
    showContentBoundsreda rectangle around every rendered block
    showCellsgreenper-cell rectangles for tables
    +

    debug: true is shorthand for enabling all three. The overlay is drawn last, +on top of the content, each shape wrapped in its own graphics state (q … Q) so +it never leaks colour or line-width into your document. Turn it off (or omit it) +and the bytes are exactly what you'd get without the option.

    +

    Programmatic inspection#

    +

    inspectDocumentLayout(params, layout?) runs the same pagination engine as +the builder but produces data instead of PDF bytes — no rendering, no font +embedding:

    +
    import { inspectDocumentLayout } from 'pdfnative';
    +import type { LayoutInspection } from 'pdfnative';
    +
    +const report: LayoutInspection = inspectDocumentLayout(params);
    +
    +report.pages.forEach((page) => {
    +  console.log(`Page ${page.index + 1}: ${page.blocks.length} blocks`);
    +  page.blocks.forEach((b) => {
    +    console.log(`  ${b.type} @ (${b.x}, ${b.top}) ${b.width}×${b.height}`);
    +  });
    +});
    +
    +

    Shape#

    + + + + + + + + + + + + + + + + + + + +
    TypeFields
    LayoutInspection{ pageWidth, pageHeight, margins, totalPages, pages: InspectedPage[] }
    InspectedPage{ index, blocks: InspectedBlock[] }
    InspectedBlock{ type, page, x, top, width, height } (PDF user-space points, origin bottom-left; top is the block's upper edge, height extends downward)
    +

    Because it shares the builder's estimateBlockHeight / planTable logic and +constants, the reported geometry matches where the real renderer places each +block — including table slicing across page breaks.

    +

    When to use which#

    +
      +
    • Overlay — eyeball a single document: "why is this paragraph clipped?", +"is my table overflowing the margin?" Open the PDF and see the boxes.
    • +
    • Inspection — automate it: assert block positions in a test, drive a +layout linter, or feed geometry to another tool.
    • +
    +

    Sample#

    +

    layout-debug-overlay.ts +renders the same document twice — once clean, once with the overlay — and prints +the inspectDocumentLayout() report.

    + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/faq.html b/docs/guides/faq.html index 229d0a7..b05325e 100644 --- a/docs/guides/faq.html +++ b/docs/guides/faq.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,307 @@

    Home  ›  Guides  ›  FAQ

    -
    -

    Loading…

    - +
    + +

    FAQ

    +
    +

    Frequently asked questions about pdfnative. Can't find your answer? Open a discussion or read the Troubleshooting guide.

    +
    +

    Getting started#

    +

    How do I generate my first PDF?#

    +
    import { buildDocumentPDFBytes, downloadBlob } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Hello',
    +  blocks: [
    +    { type: 'heading',   text: 'Hello world', level: 1 },
    +    { type: 'paragraph', text: 'My first PDF.' },
    +  ],
    +});
    +
    +downloadBlob(pdf, 'hello.pdf');     // browser
    +// or, in Node.js:
    +// fs.writeFileSync('hello.pdf', pdf);
    +
    +

    See the Quick Start guide for Node.js, browser, and Web Worker walkthroughs.

    +

    Which builder should I pick: buildPDFBytes or buildDocumentPDFBytes?#

    + + + + + + + + + + + + + + + + + + +
    BuilderBest forKey shape
    buildPDFBytesTabular reports, bank statements, invoices with a single table{ title, headers, rows, infoItems, balanceText, ... }
    buildDocumentPDFBytesMixed-content documents (manuals, articles, multi-section reports){ title, blocks: [...] } with 13 block types
    +

    Both return Uint8Array and accept the same layout / encryption / compression / tagged-PDF options.

    +

    Does it work in the browser?#

    +

    Yes — pdfnative is a dual ESM/CJS build with zero Node.js-specific APIs in the core:

    +
    import { buildDocumentPDFBytes, downloadBlob } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({ title: 'Hello', blocks: [/* … */] });
    +downloadBlob(pdf, 'output.pdf');
    +
    +

    For optimal compression in Node.js, call initNodeCompression() once at startup. In the browser, a stored-block fallback is used automatically.

    +

    Does it work with Deno / Bun?#

    +

    Yes. The library is standard ESM with no Node.js-specific imports in the core. Both Deno and Bun import it directly.

    +
    +

    Fonts and Unicode#

    +

    Can I use custom fonts?#

    +

    Yes. Build a font data module from any TTF file:

    +
    npx pdfnative-build-font path/to/MyFont.ttf my-font-data.js
    +
    +

    Then register it:

    +
    import { registerFonts, loadFontData, buildDocumentPDFBytes } from 'pdfnative';
    +
    +registerFonts({
    +  custom: () => import('./my-font-data.js'),
    +});
    +
    +const myFont = await loadFontData('custom');
    +const pdf = buildDocumentPDFBytes({
    +  blocks: [{ type: 'paragraph', text: '…' }],
    +  fontEntries: [{ fontData: myFont!, fontRef: '/F3', lang: 'custom' }],
    +});
    +
    +

    Font subsetting is automatic — only the glyphs you actually used are embedded.

    +

    How do I render Arabic / Hebrew / Thai / Devanagari?#

    +

    Register the matching pre-built font module and pass it as a fontEntry:

    +
    registerFonts({
    +  ar: () => import('pdfnative/fonts/noto-arabic-data.js'),
    +  he: () => import('pdfnative/fonts/noto-hebrew-data.js'),
    +  th: () => import('pdfnative/fonts/noto-thai-data.js'),
    +  hi: () => import('pdfnative/fonts/noto-devanagari-data.js'),
    +});
    +
    +const langs = ['ar', 'he', 'th', 'hi'];
    +const fontEntries = (await Promise.all(langs.map(loadFontData)))
    +  .map((fd, i) => (fd ? { fontData: fd, fontRef: `/F${3 + i}`, lang: langs[i] } : null))
    +  .filter((e): e is NonNullable<typeof e> => e !== null); // .filter(Boolean) alone does not narrow the type
    +
    +const pdf = buildDocumentPDFBytes({
    +  blocks: [
    +    { type: 'paragraph', text: 'مرحبا — שלום — สวัสดี — नमस्ते' },
    +  ],
    +  fontEntries,
    +});
    +
    +

    The lang property triggers BiDi resolution for RTL scripts and OpenType GSUB/GPOS shaping for Arabic, Devanagari, Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, Myanmar, and Thai.

    +

    Why does my Arabic text appear backwards?#

    +

    The most common cause: missing lang: 'ar' on the font entry. Without it, BiDi resolution and Arabic positional shaping are skipped. See Troubleshooting → RTL Text Backwards.

    +

    Which scripts are supported out of the box?#

    +

    26 Noto font-data modules ship with the package: the 22 scripts — Amharic/Ethiopic, Arabic, Armenian, Bengali, Cyrillic, Devanagari, Georgian, Greek, Hebrew, Japanese, Khmer, Korean, Myanmar, Polish, Simplified Chinese, Sinhala, Tamil, Telugu, Thai, Tibetan, Turkish, Vietnamese — plus Latin (Noto Sans), math (Noto Sans Math), and monochrome + COLRv1 colour emoji.

    +
    +

    Standards and compliance#

    +

    What PDF versions can pdfnative produce?#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ModePDF versionStandard
    Default1.4ISO 32000-1
    tagged: true1.7PDF/A-2b (default tagged mode, ISO 19005-2)
    tagged: 'pdfa1b'1.4PDF/A-1b (ISO 19005-1)
    tagged: 'pdfa2u'1.7PDF/A-2u (ISO 19005-2 with unicode)
    tagged: 'pdfa3b'1.7PDF/A-3b (ISO 19005-3, supports embedded files)
    +

    Which PDF/A variant should I pick?#

    +
      +
    • PDF/A-1b — strictest, oldest. Pick only if your validator requires PDF 1.4 and you don't need transparency, JPEG2000, or layers.
    • +
    • PDF/A-2b (recommended default) — PDF 1.7, supports transparency, layers, OpenType. Most modern archive systems target 2b.
    • +
    • PDF/A-2u — same as 2b plus mandatory Unicode mapping for all text. Pick if downstream consumers need text extraction.
    • +
    • PDF/A-3b — PDF/A-2b + ability to embed arbitrary attachments (e.g. source XML for invoicing standards like ZUGFeRD/Factur-X). Note: not all archive policies allow A-3.
    • +
    +

    Can I combine PDF/A and encryption?#

    +

    No. ISO 19005-1 §6.3.2 forbids encryption inside PDF/A documents. pdfnative validates this at the buildPDF() boundary and throws an error if both are requested. Pick one or the other.

    +

    Is the output tagged for accessibility (PDF/UA)?#

    +

    Yes — when you set tagged: true (or any PDF/A mode), pdfnative emits a structure tree with /Document → /Table → /TR → /TH|TD, /H1/H3, /P, /L//LI, /Figure, /Link, /Form, /TOC//TOCI, and uses /ActualText for shaped Unicode. See the Accessibility guide.

    +
    +

    Security#

    +

    How do I encrypt a PDF?#

    +
    const pdf = buildPDFBytes(params, {
    +  encryption: {
    +    algorithm: 'aes256',           // or 'aes128'
    +    userPassword: 'reader',
    +    ownerPassword: 'editor',
    +    permissions: {
    +      print: true,        // default: true
    +      copy: false,        // default: false
    +      modify: false,      // default: false
    +      extractText: true,  // accessibility text extraction — default: true
    +    },
    +  },
    +});
    +
    +

    aes256 (V5/R6) is recommended for new documents. Use aes128 (V4/R4) only when you need compatibility with very old viewers.

    +

    How do I sign a PDF digitally?#

    +
    import { addSignaturePlaceholder, signPdfBytes, parseCertificate, parseRsaPrivateKey } from 'pdfnative';
    +
    +// 1. The PDF must contain a /Sig placeholder — add one if it doesn't
    +//    (skipping this throws "No /Contents placeholder found"):
    +const prepared = addSignaturePlaceholder(pdfBytes);
    +
    +// 2. Sign it
    +const signed = signPdfBytes(prepared, {
    +  signerCert: parseCertificate(certDer),   // DER-encoded X.509 certificate
    +  rsaKey: parseRsaPrivateKey(keyDer),      // DER-encoded RSA key (or `ecKey` for ECDSA P-256)
    +  algorithm: 'rsa-sha256',                 // or 'ecdsa-sha256'
    +  reason: 'Approved',
    +  name: 'Jane Doe',
    +});
    +
    +

    pdfnative implements ISO 32000-1 §12.8 — CMS/PKCS#7 SignedData with RSA (PKCS#1 v1.5) or ECDSA (P-256); RSA also offers SHA-384/512 since v1.7.0. The crypto stack is implemented in pure TypeScript inside src/crypto/ — no native modules, no node:crypto (an optional constant-time native provider can be plugged in via setCryptoProvider).

    +

    How do I make a signature "LTV enabled" (valid for years)?#

    +

    Since v1.7.0 pdfnative covers the full PAdES baseline (ETSI EN 319 142-1): +sign with profile: 'pades', add an RFC 3161 timestamp with +signPdfBytesWithTimestamp, embed revocation material with +addValidationInfo (/DSS + /VRI), and cap with addDocumentTimestamp. +The engine never opens sockets — you inject the TSA/OCSP/CRL transport via +setTimestampProvider / setRevocationProvider. Full pipeline in the +LTV guide.

    +

    Can a PDF carry several signatures?#

    +

    Yes (v1.7.0): create each placeholder with allowMultiple: true and target +it by fieldName when signing. Each signature is appended as a +non-destructive incremental revision, so earlier signatures remain valid; +inspect any file with listSignatures(bytes). See +signatures — multiple signatures.

    +

    Is the build supply-chain safe?#

    +
      +
    • Zero runtime dependenciesnpm install pdfnative brings in nothing transitive at runtime.
    • +
    • NPM provenance — every release is signed via GitHub Actions OIDC (SLSA L3-equivalent).
    • +
    • OpenSSF Scorecard scanned weekly; CodeQL on every push.
    • +
    • Pinned dev dependencies managed via Dependabot.
    • +
    +
    +

    Modifying existing PDFs#

    +

    Can I read or modify existing PDFs?#

    +

    Yes. pdfnative includes a tokenizer, object parser, xref/stream parser, and a non-destructive incremental modifier:

    +
    import { openPdf, createModifier } from 'pdfnative';
    +
    +const reader = openPdf(new Uint8Array(fs.readFileSync('input.pdf')));
    +
    +console.log(`Pages: ${reader.pageCount}`);
    +console.log(`Title: ${reader.getInfo()?.get('Title')}`);
    +
    +const mod = createModifier(reader);
    +mod.setMetadata('Title', 'Updated Title');
    +const updated = mod.save();   // appends a new xref/trailer with /Prev chain
    +
    +

    What's the maximum document size?#

    +

    buildPDFBytes enforces a 100 000 row limit on tables. buildDocumentPDFBytes paginates automatically, with a default cap of 100 000 blocks (DEFAULT_MAX_BLOCKS) that you can raise via layout.maxBlocks; past that, the practical ceiling is your available memory. For very large documents, see the streaming question below.

    +

    How do I avoid loading the whole PDF into memory?#

    +

    Use the streaming API — it returns an AsyncGenerator<Uint8Array> that yields chunks as they're produced:

    +
    import { buildDocumentPDFStream } from 'pdfnative';
    +
    +for await (const chunk of buildDocumentPDFStream(params, {}, { chunkSize: 65536 })) {
    +  await writeStream.write(chunk);
    +}
    +
    +
    +

    Performance#

    +

    How can I make the PDF smaller?#

    +
      +
    1. Enable compression: compress: true (FlateDecode, 50–90 % size reduction).
    2. +
    3. Initialize native zlib in Node.js: await initNodeCompression() once at startup.
    4. +
    5. Use JPEG for photos — JPEG (DCTDecode) is already compressed; PNG is FlateDecode-compressed.
    6. +
    7. Font subsetting is automatic — but each script you embed adds one subset.
    8. +
    +

    Can I generate PDFs in a Web Worker?#

    +

    Yes. For large datasets pdfnative ships a built-in worker pipeline — use createPDF, which routes to a worker or the main thread for you:

    +
    import { createPDF } from 'pdfnative';
    +
    +const pdf = await createPDF(params, {
    +  workerUrl: new URL('./pdf-worker.js', import.meta.url),
    +  threshold: 500, // default WORKER_THRESHOLD = 500 rows
    +});
    +
    +

    Tables above the threshold run off the main thread (with an automatic main-thread fallback); smaller ones render synchronously. The lower-level generatePDFInWorker(workerUrl, params, { timeout, onProgress }) drives a worker directly — the worker URL is its first argument, and it has no threshold logic.

    +
    +

    Why pdfnative?#

    +

    How does it compare to jsPDF / pdfkit / pdf-lib / pdfmake?#

    +

    See the feature comparison table in the README. Key differentiators:

    +
      +
    • Zero runtime dependencies (others ship 3–6).
    • +
    • 22 Unicode scripts with built-in BiDi and OpenType GSUB/GPOS shaping.
    • +
    • PDF/A (1b, 2b, 2u, 3b) — none of the others support this directly.
    • +
    • Built-in digital signatures (RSA + ECDSA) without external crypto modules.
    • +
    • 5 native barcode formats (Code 128, EAN-13, QR, Data Matrix, PDF417) as PDF vector paths.
    • +
    +

    Does it convert HTML to PDF?#

    +

    No, and it never will. HTML→PDF is a different problem domain (browser engine, CSS, layout) better solved by tools like puppeteer or weasyprint. pdfnative is a structured-data → PDF library — you describe blocks, it emits ISO-compliant PDF.

    +

    Why no classes / no inheritance?#

    +

    pdfnative is built from pure functions. State is passed explicitly. This makes the library trivially tree-shakeable, easy to test, and easy to reason about. See the Architecture guide.

    +
    +

    Common errors#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ErrorLikely causeFix
    Blank / missing glyphs for a script (no error is thrown)loadFontData('xx') resolved to null because no loader was registered for that code — it returns null rather than throwingCall registerFonts({ xx: … }) (or registerFont) first, then check the loadFontData result for null before building fontEntries
    PDF/A and encryption are mutually exclusivetagged: 'pdfa…' combined with encryption: …Pick one
    Invalid color format: … / Invalid color tuple …Unrecognized color value passed to a layout optionUse #rrggbb, an [r, g, b] tuple with channels 0–255, or a PDF RGB string 'r g b' with channels 0.0–1.0
    Boxes / blank glyphsFont for that script is not loadedSee Troubleshooting → Missing glyphs
    Parser throws on external PDFEncrypted PDF opened without a password, or non-standard structurePass openPdf(bytes, { password }) — the parser decrypts RC4, AES-128 and AES-256 since v1.6.0. A missing or wrong password throws PdfPasswordError
    +

    For more cases, see the Troubleshooting guide.

    +
    diff --git a/docs/guides/font-validation.html b/docs/guides/font-validation.html index 8a55426..5519a02 100644 --- a/docs/guides/font-validation.html +++ b/docs/guides/font-validation.html @@ -1,85 +1,156 @@ - - - - - - Font-data validation — pdfnative - - - + + + + + + Font-data validation — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Font-data validation

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Font-data validation

    +
    + +

    Font-data validation

    +
    +

    New in v1.4.0. validateFontData() is an opt-in, read-only structural +sanity check for custom font-data modules built from an untrusted or +unfamiliar TTF/OTF. It catches the common failure modes early and returns +{ valid, errors, warnings } instead of letting a cryptic .notdef/NaN +surface deep inside the encoding/subsetting pipeline. The bundled +pdfnative/fonts/* modules are already trusted — this is for fonts you +build via tools/build-font-data.cjs or by hand.

    +
    +

    TL;DR#

    +
    import { validateFontData } from 'pdfnative';
    +import type { FontValidationResult } from 'pdfnative';
    +
    +const result: FontValidationResult = validateFontData(myFontData);
    +if (!result.valid) {
    +  console.error('Font rejected:', result.errors);
    +}
    +for (const w of result.warnings) console.warn('Font warning:', w);
    +
    +

    What it checks#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AreaRule
    MetricsunitsPerEm > 0, finite; bbox is 4 finite numbers
    IdentityfontName non-empty
    Coveragecmap non-empty; every glyph id is an integer in [0, numGlyphs)
    Widthswidths present (missing → warning); pdfWidthArray non-empty
    BinaryttfBase64 decodes as valid base64 and begins with an SFNT magic (0x00010000, OTTO, true, ttcf)
    +

    Blocking problems land in errors; suspicious-but-survivable ones land in +warnings. It never throws — even on completely malformed input.

    +

    It is opt-in by design#

    +

    validateFontData() is not invoked automatically by registerFont(): +running it on every load would add cost and risk false-rejecting edge-valid +fonts. Call it yourself once when ingesting third-party font data, e.g. in a +build step or a test:

    +
    import { validateFontData, registerFont } from 'pdfnative';
    +import myFont from './my-font-data.js';
    +
    +const { valid, errors } = validateFontData(myFont);
    +if (!valid) throw new Error(`Bad font data: ${errors.join('; ')}`);
    +registerFont('mylang', () => Promise.resolve(myFont));
    +
    +

    Sample#

    +

    font-validation-showcase.ts +validates a bundled font plus deliberately broken payloads and renders the +verdicts into a report PDF.

    + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/form-filling.html b/docs/guides/form-filling.html index 7e5f23f..bf7ce0d 100644 --- a/docs/guides/form-filling.html +++ b/docs/guides/form-filling.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,159 @@

    Home  ›  Guides  ›  Form filling

    -
    -

    Loading…

    - +
    + +

    Form filling & flattening

    +
    +

    New in v1.6.0. Read, fill, and flatten the interactive AcroForm fields +of existing PDFs — whether authored by pdfnative or a third party — via +non-destructive incremental update. Complements the form builder (which +creates fields from scratch).

    +
    +

    TL;DR#

    +
    import { readFormFields, fillForm, flattenForm } from 'pdfnative';
    +import { readFileSync, writeFileSync } from 'node:fs';
    +
    +const src = readFileSync('application.pdf');
    +
    +// 1. Discover the fields
    +for (const f of readFormFields(src)) {
    +  console.log(f.name, f.type, f.value);
    +}
    +
    +// 2. Fill by field name
    +const filled = fillForm(src, {
    +  fullName: 'Ada Lovelace',
    +  agree: true,               // checkbox
    +  country: 'France',         // dropdown option
    +});
    +writeFileSync('filled.pdf', filled);
    +
    +// 3. Optionally flatten (make it non-editable)
    +writeFileSync('flat.pdf', flattenForm(filled));
    +
    +

    readFormFields(bytes, options?)#

    +

    Returns a ParsedFormField[] describing every terminal field:

    +
    interface ParsedFormField {
    +  name: string;                               // fully-qualified (dotted) name
    +  type: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'listbox'
    +      | 'button' | 'signature' | 'unknown';
    +  value: string | string[] | boolean | null;
    +  readOnly: boolean;
    +  required: boolean;
    +  multiline: boolean;
    +  options?: { export: string; label: string }[]; // choice fields
    +  maxLen?: number;
    +  onState?: string;                           // checkbox/radio "on" state name
    +  widgets: { pageIndex: number; rect: [number, number, number, number] }[];
    +  ref: number;                                // terminal field object number
    +}
    +
    +

    Inherited attributes (/FT, /Ff, /DA, /Opt, /MaxLen) are resolved up the +/Parent chain, and UTF-16BE names/values are decoded.

    +

    fillForm(bytes, values, options?)#

    +

    values maps fully-qualified field name → value:

    + + + + + + + + + + + + + + + + + + + + + + + +
    Field typeValue
    text / multilinestring
    dropdownstring (one option's export/label)
    listboxstring or string[] (multi-select)
    checkbox / radioboolean, or the export-state string
    +
    interface FillFormOptions {
    +  flatten?: boolean;                          // fill then flatten in one call
    +  onUnknownField?: 'throw' | 'ignore';        // default 'throw'
    +  nonWinAnsi?: 'throw' | 'needAppearances';   // default 'throw'
    +  password?: string;                          // for encrypted documents (see below)
    +}
    +
    +
      +
    • Text/choice fields get a freshly generated, self-contained Helvetica +appearance stream (its own /Resources /Helv), so filling never depends on the +document's /DR.
    • +
    • Checkbox/radio set /V and /AS from the widget's own /AP on/off states, +preserving the document's original look.
    • +
    • Non-WinAnsi text (e.g. CJK) can't be drawn with the built-in Helvetica +appearance; pass nonWinAnsi: 'needAppearances' to write the value and let the +viewer regenerate the appearance, or the call throws by default.
    • +
    +

    Errors: FormFieldNotFoundError, FormValueTypeError, FormUnsupportedError.

    +

    flattenForm(bytes, options?)#

    +

    Stamps each widget's appearance into its page content and removes the +interactive layer (/AcroForm, widget /Annots):

    +
    interface FlattenFormOptions {
    +  force?: boolean;      // flatten even over a signed signature field
    +  password?: string;    // for encrypted documents
    +}
    +
    +

    Flattening a document with a signed signature field throws by default +(flattening is destructive to what was signed); pass { force: true } to +override.

    +

    Encrypted & signed documents#

    +
      +
    • Encrypted documents are supported (v1.6.0): pass the password —

      +
      const filled = fillForm(encryptedPdf, { fullName: 'Grace Hopper' }, { password: 'secret' });
      +const flat = flattenForm(filled, { password: 'secret' });
      +
      +

      The appended objects — field values, regenerated appearance streams, +flatten overlays — are encrypted under the document's existing scheme +(same /Encrypt dictionary, same file key; RC4, AES-128 and AES-256 +sources all work), so no plaintext ever leaks into an encrypted file and +no downgrade or upgrade of the scheme is possible. A wrong or missing +password throws PdfPasswordError. Note: the /P permission bits are +not enforced — the update proceeds if the password authenticates; +honouring the modify bit is the caller's responsibility +(openPdf(bytes, { password }).encryption.authenticatedAs tells you +which password opened the file).

      +
    • +
    • Signed documents: filling non-signature fields is allowed and preserves +the signed revision byte-for-byte (incremental update only appends). Viewers +will report "document modified after signing" for the added revision — expected, +since the fill is a new, unsigned revision layered on top. Adding a new +signature placeholder to an encrypted document is not supported +(addSignaturePlaceholder needs a verbatim byte layout that cannot be +transparently encrypted — it fails fast with a clear error).

      +
    • +
    +

    How it works#

    +

    Both operations use the incremental modifier +(createModifier), so the original bytes are never +rewritten — the update is appended after the existing body with a new xref and a +/Prev chain. That is why signatures on earlier revisions stay valid.

    +
    +

    Searchable form text (v1.7.0). The AcroForm /Helv font dictionary now +carries a /ToUnicode CMap in every mode, so text typed into form fields is +searchable and extractable (see the text extraction guide). +Because of that CMap, every form-carrying document changes bytes compared +to v1.6.0 output (about 20 bytes) — a deliberate correctness fix, recorded in +the v1.7.0 release notes. Under a PDF/A claim, a form field also raises the +PDFA_UNEMBEDDED_FORM_FONT diagnostic (see the PDF/A guide).

    +
    +

    See also#

    + +
    diff --git a/docs/guides/guide.css b/docs/guides/guide.css index f9f18a0..7a938cf 100644 --- a/docs/guides/guide.css +++ b/docs/guides/guide.css @@ -189,3 +189,68 @@ font-weight: 400; color: var(--c-text-dim); } + +/* ── Pre-rendered guide enhancements (build-guides.ts + guide.js) ── */ + +.heading-anchor { + margin-left: 8px; + font-size: 0.75em; + font-weight: 400; + color: var(--c-text-muted); + opacity: 0; + transition: opacity 0.15s; + text-decoration: none; +} + +.guide-content h2:hover .heading-anchor, +.guide-content h3:hover .heading-anchor, +.guide-content h4:hover .heading-anchor, +.heading-anchor:focus-visible { + opacity: 1; +} + +.guide-content pre { position: relative; } + +.guide-content pre .copy-btn { + position: absolute; + top: 8px; + right: 8px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + border: 1px solid var(--c-border); + border-radius: 4px; + background: var(--c-surface); + color: var(--c-text); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s; +} + +.guide-content pre:hover .copy-btn, +.guide-content pre .copy-btn:focus-visible { + opacity: 1; +} + +.guide-source-bar { + display: flex; + gap: 14px; + align-items: center; + margin: 0 0 18px; + font-size: 13px; +} + +.guide-source-btn { + padding: 5px 12px; + font-size: 13px; + font-weight: 600; + border: 1px solid var(--c-border); + border-radius: 6px; + background: var(--c-surface); + color: var(--c-text); + cursor: pointer; +} + +.guide-source-btn:hover { border-color: var(--c-primary); } + +.guide-source-link { color: var(--c-text-dim); } diff --git a/docs/guides/guide.js b/docs/guides/guide.js index d244cc8..720649c 100644 --- a/docs/guides/guide.js +++ b/docs/guides/guide.js @@ -47,6 +47,76 @@ var src = container.getAttribute('data-md'); if (!src) return; + // ── Progressive enhancements shared by both paths ───────── + // (pre-rendered shells and the runtime-rendered fallback) + + function addCopyButtons(scope) { + scope.querySelectorAll('pre').forEach(function (pre) { + if (pre.querySelector('.copy-btn')) return; + var btn = document.createElement('button'); + btn.className = 'copy-btn'; + btn.type = 'button'; + btn.textContent = 'Copy'; + btn.addEventListener('click', function () { + var code = pre.querySelector('code'); + navigator.clipboard.writeText(code ? code.textContent : pre.textContent).then(function () { + btn.textContent = 'Copied!'; + setTimeout(function () { btn.textContent = 'Copy'; }, 1500); + }, function () { btn.textContent = 'Failed'; }); + }); + pre.appendChild(btn); + }); + } + + function addSourceBar(scope) { + if (document.querySelector('.guide-source-bar')) return; + var bar = document.createElement('div'); + bar.className = 'guide-source-bar'; + var copyMd = document.createElement('button'); + copyMd.type = 'button'; + copyMd.className = 'guide-source-btn'; + copyMd.textContent = 'Copy page as Markdown'; + copyMd.addEventListener('click', function () { + fetch(src, { cache: 'no-cache' }) + .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }) + .then(function (md) { return navigator.clipboard.writeText(md); }) + .then(function () { + copyMd.textContent = 'Copied!'; + setTimeout(function () { copyMd.textContent = 'Copy page as Markdown'; }, 1500); + }) + .catch(function () { copyMd.textContent = 'Copy failed'; }); + }); + var view = document.createElement('a'); + view.className = 'guide-source-link'; + view.href = src; + view.textContent = 'View Markdown source'; + bar.appendChild(copyMd); + bar.appendChild(view); + scope.parentNode.insertBefore(bar, scope); + } + + function enhance(scope) { + addSourceBar(scope); + addCopyButtons(scope); + if (window.Prism && typeof window.Prism.highlightAllUnder === 'function') { + window.Prism.highlightAllUnder(scope); + } + } + + // ── Pre-rendered path ───────────────────────────────────── + // build-guides.ts bakes the rendered article and its JSON-LD into the + // shell (rule guide-render-sync keeps it fresh). Nothing to fetch: just + // enhance in place. Prism loads with `defer` after this script, so wait + // for it briefly instead of highlighting a not-yet-loaded page. + if (container.getAttribute('data-prerendered') === 'true') { + var tries = 20; + (function enhanceWhenReady() { + if (window.Prism || tries-- <= 0) { enhance(container); return; } + setTimeout(enhanceWhenReady, 100); + })(); + return; + } + function showError(msg) { container.innerHTML = '
    ' + 'Failed to load this guide. ' + @@ -123,10 +193,8 @@ if (el) el.scrollIntoView(); } - // Trigger Prism if loaded - if (window.Prism && typeof window.Prism.highlightAllUnder === 'function') { - window.Prism.highlightAllUnder(container); - } + // Copy buttons, source bar, Prism + enhance(container); // Update document title from first

    var h1 = container.querySelector('h1'); diff --git a/docs/guides/index.html b/docs/guides/index.html index 147fc84..2428e19 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -8,6 +8,7 @@ + diff --git a/docs/guides/ltv.html b/docs/guides/ltv.html index 2824f78..49b6da8 100644 --- a/docs/guides/ltv.html +++ b/docs/guides/ltv.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,150 @@

    Home  ›  Guides  ›  Long-term validation

    -
    -

    Loading…

    - +
    + +

    Long-term validation (LTV) — PAdES B-B to B-LTA

    +
    +

    New in v1.7.0. pdfnative signs PDFs that stay verifiable for decades: PAdES baseline signatures (ETSI EN 319 142-1), RFC 3161 signature timestamps, embedded revocation material (/DSS + /VRI), and document timestamps — all with zero runtime dependencies and zero network access in the engine. Transport is injected by your code.

    +
    +

    TL;DR#

    +
    import {
    +  buildDocumentPDFBytes, addSignaturePlaceholder, estimateContentsSize,
    +  signPdfBytesWithTimestamp, addValidationInfo, addDocumentTimestamp,
    +  setTimestampProvider, setRevocationProvider,
    +} from 'pdfnative';
    +
    +// 1. Your transports (the engine never fetches on its own).
    +setTimestampProvider({
    +  async getTimestamp(request) {           // DER TimeStampReq in…
    +    const res = await fetch('https://freetsa.org/tsr', {
    +      method: 'POST',
    +      headers: { 'Content-Type': 'application/timestamp-query' },
    +      body: request.slice(),
    +    });
    +    return new Uint8Array(await res.arrayBuffer());  // …DER TimeStampResp out
    +  },
    +});
    +setRevocationProvider({
    +  async fetchOcsp(url, request) {
    +    const res = await fetch(url, {
    +      method: 'POST',
    +      headers: { 'Content-Type': 'application/ocsp-request' },
    +      body: request.slice(),
    +    });
    +    return new Uint8Array(await res.arrayBuffer());
    +  },
    +  async fetchCrl(url) {
    +    const res = await fetch(url);
    +    return new Uint8Array(await res.arrayBuffer());
    +  },
    +});
    +
    +// 2. Build → placeholder → sign+timestamp → embed validation → archive.
    +const unsigned = addSignaturePlaceholder(buildDocumentPDFBytes(params), {
    +  metadata: { subFilter: 'ETSI.CAdES.detached', reason: 'Contract v2' },
    +  placeholderBytes: estimateContentsSize([certDer.length], 'rsa-sha256', { timestamp: true }),
    +});
    +const signed  = await signPdfBytesWithTimestamp(unsigned, {
    +  signerCert, certChain: [intermediateCert], rsaKey,
    +  profile: 'pades',                       // ESS signing-certificate-v2, no CMS signing-time
    +});
    +const ltv     = await addValidationInfo(signed);       // /DSS + /VRI (B-LT)
    +const archive = await addDocumentTimestamp(ltv);       // DocTimeStamp (B-LTA)
    +
    +

    Every step appends a non-destructive incremental revision — earlier bytes are never modified, so prior signatures stay intact.

    +

    Why LTV#

    +

    A plain signature proves who signed, but its verifiability decays: certificates expire, CAs go offline, revocation servers disappear. The PAdES baseline levels fix each failure mode in turn:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    LevelAddsProtects against
    B-BCAdES signature with the ESS signing-certificate-v2 attributeCertificate substitution
    B-TRFC 3161 signature timestamp (id-aa-signatureTimeStampToken)"Was the signature made while the certificate was valid?"
    B-LT/DSS: embedded certificates + OCSP/CRL responses, per-signature /VRIRevocation servers disappearing
    B-LTA/DocTimeStamp covering the whole documentAlgorithm/key aging — re-timestamp before the TSA certificate expires to extend the chain
    +

    Adobe Reader shows a B-LT/B-LTA document as "LTV enabled" (with the chain's root trusted).

    +

    The provider architecture#

    +

    pdfnative never opens a socket. Two small interfaces move bytes; you own transport, proxies, retries and trust decisions:

    +
    interface TimestampProvider {
    +  getTimestamp(request: Uint8Array): Promise<Uint8Array>;   // TimeStampReq → TimeStampResp
    +}
    +interface RevocationProvider {
    +  fetchOcsp?(url: string, request: Uint8Array): Promise<Uint8Array>; // OCSPRequest → OCSPResponse
    +  fetchCrl?(url: string): Promise<Uint8Array>;                      // → CertificateList
    +}
    +
    +

    Install globally (setTimestampProvider / setRevocationProvider) or pass per call (timestampProvider: / revocationProvider: options — per-call wins). pdfnative builds and parses every RFC 3161 / RFC 6960 / RFC 5280 structure itself, verifies the token's message imprint and nonce echo, and refuses to embed a rejected or tampered response.

    +

    Step by step#

    +

    B-B — a PAdES baseline signature#

    +
    const signed = signPdfBytes(placeheld, {
    +  signerCert, certChain, rsaKey,
    +  profile: 'pades',          // adds ESS signing-certificate-v2 (RFC 5035)
    +});
    +
    +

    Pair profile: 'pades' with the placeholder's metadata: { subFilter: 'ETSI.CAdES.detached' } so the /Sig dictionary declares the PAdES SubFilter. The legacy default (adbe.pkcs7.detached, profile: 'pkcs7') is unchanged. digestAlgorithm: 'sha384' | 'sha512' upgrades the whole CMS digest chain.

    +

    B-T — the signature timestamp#

    +

    signPdfBytesWithTimestamp (async) signs, hashes the CMS signature value, asks your TimestampProvider for a token, verifies it, and attaches it as an unsigned attribute — the signed bytes are untouched, so the signature stays valid. Reserve room for the token when creating the placeholder:

    +
    placeholderBytes: estimateContentsSize(certSizes, 'rsa-sha256', { timestamp: true })  // +8 KiB
    +
    +

    B-LT — the Document Security Store#

    +
    const data = await collectValidationInfo(signed);   // async: chains + OCSP/CRL via provider
    +const ltv  = embedValidationInfo(signed, data);     // sync, offline, deterministic
    +// …or in one call: await addValidationInfo(signed)
    +
    +

    collectValidationInfo walks every signed signature (and the TSA certificates inside embedded timestamp tokens), deduplicates the certificate pool, and requests OCSP (preferred) or CRL data for every certificate that advertises a source via its AIA / CRL-distribution-point extensions — skipping self-signed roots and id-pkix-ocsp-nocheck responders. Missing intermediates can be supplied with extraCertificates.

    +

    embedValidationInfo writes the /DSS dictionary — /Certs, /OCSPs, /CRLs stream arrays plus a /VRI entry per signature keyed by the uppercase-hex SHA-1 of its full /Contents value (the Adobe convention; vriKeyForContents exposes the computation). An existing /DSS is merged, never replaced. The split matters: collect once online, embed deterministically offline (or in tests).

    +

    B-LTA — the document timestamp#

    +
    const archive = await addDocumentTimestamp(ltv);
    +
    +

    Appends a signature field whose dictionary is /Type /DocTimeStamp /SubFilter /ETSI.RFC3161 with a bare TimeStampToken as /Contents (ISO 32000-2 §12.8.5 — no /M: the token's genTime is the time assertion). Field names auto-suffix (DocTimeStamp1, DocTimeStamp2, …) so periodic re-timestamping chains naturally.

    +

    Multiple signatures#

    +
    const a = signPdfBytes(addSignaturePlaceholder(pdf, { fieldName: 'Author' }), opts);
    +const b = signPdfBytes(
    +  addSignaturePlaceholder(a, { fieldName: 'Reviewer', allowMultiple: true }),
    +  { ...opts, fieldName: 'Reviewer' },
    +);
    +
    +

    allowMultiple: true opts out of the historical one-signature short-circuit; the fieldName selector on signPdfBytes targets the right placeholder. Signed signatures are located by their real ByteRange values and can never be overwritten. Inspect any document with listSignatures(bytes).

    +

    Testing without a real TSA or CA#

    +

    The repository ships a deterministic offline mock PKI (scripts/helpers/mock-pki.ts): a root CA, a signer with AIA/CRL-DP extensions, a TSA with the critical id-kp-timeStamping EKU and an OCSP responder with ocsp-nocheck — plus providers that produce real, signed TimeStampTokens, BasicOCSPResponses and CRLs. The signature/signature-pades-*.pdf samples and the ltv-pipeline integration suite run the full B-LTA chain with zero network. Public TSAs for manual testing include https://freetsa.org/tsr.

    +

    Verification checklist#

    +
      +
    • listSignatures(bytes) — every entry, its SubFilter, ByteRange, placeholder/timestamp status.
    • +
    • Adobe Reader: Signature Panel → Certificate Details — trust the mock/company root, then look for "LTV enabled" on B-LT documents and a valid document timestamp on B-LTA.
    • +
    • openssl ts -reply -in token.der -text inspects extracted timestamp tokens.
    • +
    • pdfnative itself ships no signature verifier — validation belongs to dedicated tooling (veraPDF for PDF/A, Adobe/DSS for signatures, pdfnative-cli verify for full cryptographic verification: CMS signature values, certificate chain and trust, RFC 3161 timestamps, OCSP/CRL revocation).
    • +
    +

    Limits & scope (v1.7.0)#

    +
      +
    • ECDSA stays P-256 with SHA-256; RSA supports SHA-256/384/512.
    • +
    • Chain building uses exact issuer-name matching over the embedded pool; cross-certification and bridge PKIs are out of scope.
    • +
    • collectValidationInfo fetches from the first advertised OCSP/CRL URL; provider-side failover is your transport's business.
    • +
    • Encrypted documents cannot take signature placeholders (unchanged from v1.2).
    • +
    • PDF 2.0 /Extensions declarations are not emitted; the ISO 32000-2 constructs used here are accepted by all mainstream validators.
    • +
    +
    diff --git a/docs/guides/mcp.html b/docs/guides/mcp.html index 46d44b9..a82823b 100644 --- a/docs/guides/mcp.html +++ b/docs/guides/mcp.html @@ -10,6 +10,7 @@ + @@ -27,6 +28,9 @@ + + + @@ -58,9 +62,1007 @@

    Home  ›  Guides  ›  MCP Integration

    -
    -

    Loading…

    - +
    + +

    pdfnative-mcp — AI Client Integration Guide

    +
    +

    Tracks the latest published pdfnative-mcp (v1.6.0, built on pdfnative 1.7.0). Full release notes: pdfnative-mcp releases. Live package versions — and the pdfnative version each one is built on — are shown at the top of the documentation home.

    +
    +

    pdfnative-mcp is an MCP server that exposes the full pdfnative library to any AI client supporting the Model Context Protocol — Claude Desktop, Cursor, Continue, Zed, ChatGPT, and more.

    +
    +

    What is MCP? The Model Context Protocol is an open standard (originally developed by Anthropic) that lets AI assistants call external tools in a structured, safe way. An MCP server declares a set of tools with typed inputs and outputs; the AI client invokes those tools on your behalf during a conversation.

    +
    +

    With pdfnative-mcp installed, you can say to your AI assistant:

    +
    +

    "Generate a Q1 2026 financial report as PDF/A-2b with a QR code pointing to our dashboard, then inspect the result to confirm it's archive-grade."

    +
    +

    …and the AI will call the right combination of generate_basic_pdf, add_barcode, add_table, and inspect_pdf tools, returning a ready-to-download PDF.

    +
    +

    Installation#

    +
    # Run directly with npx — no global install required (recommended)
    +npx -y pdfnative-mcp
    +
    +# Or install globally
    +npm install -g pdfnative-mcp
    +pdfnative-mcp
    +
    +

    Requirements: Node.js ≥ 22.

    +
    +

    Configuration by client#

    +

    Claude Desktop#

    +

    Edit the config file for your OS:

    +
      +
    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    • +
    • Windows: %APPDATA%\Claude\claude_desktop_config.json
    • +
    +
    {
    +  "mcpServers": {
    +    "pdfnative": {
    +      "command": "npx",
    +      "args": ["-y", "pdfnative-mcp"],
    +      "env": {
    +        "PDFNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pdfs"
    +      }
    +    }
    +  }
    +}
    +
    +

    Restart Claude Desktop after saving. The pdfnative server will appear in the tools panel.

    +

    Cursor#

    +

    In your project .cursor/mcp.json (or global ~/.cursor/mcp.json):

    +
    {
    +  "mcpServers": {
    +    "pdfnative": {
    +      "command": "npx",
    +      "args": ["-y", "pdfnative-mcp"],
    +      "env": {
    +        "PDFNATIVE_MCP_OUTPUT_DIR": "/path/to/pdf-output"
    +      }
    +    }
    +  }
    +}
    +
    +

    Continue#

    +

    In your ~/.continue/config.json:

    +
    {
    +  "mcpServers": [
    +    {
    +      "name": "pdfnative",
    +      "command": "npx",
    +      "args": ["-y", "pdfnative-mcp"],
    +      "env": {
    +        "PDFNATIVE_MCP_OUTPUT_DIR": "/path/to/pdf-output"
    +      }
    +    }
    +  ]
    +}
    +
    +

    Zed#

    +

    In your Zed settings.json:

    +
    {
    +  "context_servers": {
    +    "pdfnative": {
    +      "command": {
    +        "path": "npx",
    +        "args": ["-y", "pdfnative-mcp"],
    +        "env": {
    +          "PDFNATIVE_MCP_OUTPUT_DIR": "/path/to/pdf-output"
    +        }
    +      }
    +    }
    +  }
    +}
    +
    +
    +

    Environment variables#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    VariablePurpose
    PDFNATIVE_MCP_OUTPUT_DIRAbsolute path to the sandbox directory. Required to enable outputMode: "file". When unset, only base64 output is available. (The misspelt PDFNATIVE_MPC_OUTPUT_DIR still works as a deprecated alias.)
    PDFNATIVE_MCP_CACHE_DIRAbsolute path to enable the persistent SHA-256-keyed result cache (1 h TTL, 256 MiB LRU). When unset, the cache is disabled.
    PDFNATIVE_MCP_PORTWhen set to a valid port (1–65535), starts an HTTP server on http://127.0.0.1:<port>/mcp instead of stdio.
    PDFNATIVE_MCP_HTTP_TOKEN (v1.6.0)Opt-in bearer token for the HTTP transport (≥ 16 characters, no whitespace — a weaker value aborts startup). When set, /mcp requires Authorization: Bearer <token>; otherwise requests get 401 + WWW-Authenticate. Without it the loopback endpoint has no authentication. Never logged.
    PDFNATIVE_MCP_MAX_INFLATE_BYTES (v1.6.0)Operator override of the engine's 100 MiB per-stream decompression cap (integer ≥ 1024, read once at startup; an invalid value refuses to start with one clean fatal: line).
    PDFNATIVE_MCP_TSA_URL (v1.6.0)RFC 3161 timestamp authority endpoint. Required by sign_pdf timestamp: true and timestamp_pdf (TSA_NOT_CONFIGURED otherwise).
    PDFNATIVE_MCP_TSA_AUTH (v1.6.0)Optional Authorization header value sent to the TSA. Treated as a secret; never logged.
    PDFNATIVE_MCP_REVOCATION (v1.6.0)Revocation sources for add_ltv mode: 'online'ocsp, crl, or ocsp,crl (REVOCATION_NOT_CONFIGURED otherwise).
    PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS (v1.6.0)Mandatory allow-list of hosts the OCSP/CRL fetcher may contact. A certificate-supplied URL outside the list fails with NETWORK_HOST_NOT_ALLOWED.
    PDFNATIVE_MCP_NETWORK_TIMEOUT_MS (v1.6.0)Network timeout for TSA/OCSP/CRL requests (1000–120000 ms, default 10000).
    +
    +

    Network charter (v1.6.0). The server still makes no outbound request by default. The only egress it can ever perform goes to the TSA / OCSP / CRL endpoints the operator configures via the variables above — URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard (allow-list, http(s) only, no credentials, no redirects, internal address literals rejected, size caps, timeouts).

    +
    +
    +

    Tool reference#

    +

    pdfnative-mcp exposes 28 tools:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ToolPurpose
    generate_basic_pdfMulti-page documents from structured blocks — all 13 block kinds since v1.6.0 (headings, paragraphs, lists, tables, images, links, TOC, barcodes, SVG, form fields, charts, page breaks, spacers). Accepts optional pdfA, layout options, build-time encrypt, and print-production fields.
    add_tableTabular PDF reports from column headers and data rows. Optional autoFitColumns and clipCells. Accepts pdfA.
    add_barcodeQR Code, Code 128, EAN-13, Data Matrix, PDF417 — embedded in a single-page PDF. Accepts pdfA.
    add_international_text25 lang font codes — the 22 writing systems plus latin, emoji and the explicit math script (Noto Sans Math, on-demand) — with BiDi & OpenType shaping. lang accepts string, string[], or comma-separated.
    add_formInteractive AcroForm PDFs with text fields, text areas, checkboxes, radio buttons, dropdowns, and list boxes (v1.6.0). Accepts pdfA.
    embed_imageEmbed a JPEG or PNG image (base64-encoded) into a titled PDF document, with align and alt (v1.6.0). Accepts pdfA.
    prepare_signature_placeholderCreate a PDF with a /Sig AcroForm placeholder ready to be signed; subFilter, reserveTimestamp and frozen signer metadata (v1.6.0). Accepts pdfA.
    sign_pdfPAdES CMS digital signatures — RSA-SHA256/384/512 and ECDSA-SHA256 P-256, profile: 'pades', RFC 3161 timestamp (B-T), certificate chains, named fields and multiple signatures (v1.6.0).
    add_ltv (v1.6.0)Embed long-term-validation material (/DSS + /VRI) into a signed PDF — PAdES B-LT — online through the operator-configured revocation provider or offline from caller-supplied material.
    timestamp_pdf (v1.6.0)Append a /DocTimeStamp (ETSI.RFC3161) through the operator TSA — PAdES B-LTA — with auto-suffixed field names for periodic re-timestamping.
    inspect_pdfRead-only inspection. Returns version, pageCount, encryption, pdfA, signatureCount, info, optional perPage, optional pageLabels[], and — (v1.6.0) — an optional signatures[] inventory, annotations[], page boxes + userUnit, dss / docTimestampCount / trapped, plus checks + checksPassed.
    inspect_layout (v1.6.0)Read-only pagination dry run — page count, page geometry and each block's position for a prospective document, with no PDF produced.
    validate_pdfRead-only PDF/UA structural validation (valid, errors, warnings).
    verify_pdfReal CMS/PKCS#7 signature verification — RSA & ECDSA, message digest, certificate chain. Since v1.6.0, /DocTimeStamp entries are verified as RFC 3161 tokens and ltv: true reports the achieved PAdES level (B-B → B-LTA).
    add_attachmentEmbed files (e.g. Factur-X / ZUGFeRD e-invoice XML) into PDF/A-3b output.
    extract_attachmentsExtract embedded files from an existing PDF (optionally metadata-only).
    extract_textExtract text content from an existing PDF via the native parser.
    merge_pdfsConcatenate 2–50 PDFs into one document via the page-tree API (drops signatures//AcroForm, keeps URI links).
    split_pdfSplit one PDF into one document per page range — multi-output { mode, count, totalBytes, parts[] }. In-memory assembly is capped by maxOutputSizeBytes, default 256 MiB; each emitted PDF is separately capped at 50 MiB.
    extract_pagesPull an arbitrary, order-preserving page subset (max 5000) into a single PDF.
    annotate_pdfOverlay markup annotations (text / highlight / underline / strikeout / squiggly / square / circle / line / freetext) on an existing PDF via incremental update. A visual review layer, not a redaction.
    draft_governance_issueAssemble a governance-compliant GitHub-issue draft plus a structured compliance report locally — network-free by construction; never submits.
    add_chart (v1.5.0)Render a chart as native PDF vector paths — nine types since v1.6.0 (bar, barH, stackedBar, stackedBarH, line, area, scatter, pie, donut), with a secondary axis, log and time scales, and data labels. Tagged as /Figure with alt text when pdfA is set.
    read_form_fields (v1.5.0)List an existing AcroForm's fields with their types, current values and available options — the read half of the fill round-trip. Accepts password.
    fill_form (v1.5.0)Fill AcroForm field values and optionally flatten them into static page content. Works on encrypted PDFs via incremental update.
    encrypt_pdf (v1.5.0)Re-secure an existing PDF with AES-128 or AES-256 — owner/user passwords and an explicit permission set.
    decrypt_pdf (v1.5.0)Remove encryption from a password-protected PDF in-server — RC4, AES-128 and AES-256 sources.
    update_metadata (v1.6.0)Rewrite an existing PDF's /Info dictionary (+ XMP) — title, author, subject, keywords, pinned modDate — as a non-destructive incremental update.
    +

    Every tool publishes an outputSchema advertised in tools/list. Since v1.6.0 the server speaks the MCP 2026-07-28 spec (stateless envelope, server/discover, resultType, cache hints) on SDK v2, while 2025-era clients (2025-11-25 / 2025-06-18 / 2025-03-26) keep working through the automatic legacy fallback.

    +
    +

    generate_basic_pdf#

    +

    Produces a multi-page document from a list of content blocks.

    +
    {
    +  "title": "Q1 2026 Report",
    +  "blocks": [
    +    { "type": "heading",   "text": "Executive Summary", "level": 1 },
    +    { "type": "paragraph", "text": "Revenue grew 24 % year over year." },
    +    { "type": "list",      "style": "bullet", "items": ["Strong APAC", "Stable EU", "Soft NA"] },
    +    { "type": "pageBreak" },
    +    { "type": "heading",   "text": "Details", "level": 2 }
    +  ],
    +  "footerText": "Confidential — Internal use only",
    +  "outputMode": "base64"
    +}
    +
    +

    Block types supported: all 13 engine block kinds since v1.6.0 — heading (levels 1–3), paragraph, list (bullet / numbered, nested items to depth 6), table, image, link (http: / https: / mailto: only), toc (a contents page with internal /GoTo links, pairs with outline: 'auto'), barcode, svg (a safe subset: paths, shapes and text — no XML parser, no external reference ever fetched), formField, chart (same body as add_chart), pageBreak, and spacer (height in points). The table / image / barcode / formField blocks share their body with the dedicated tools, so a standalone artefact and an inline block validate and render identically.

    +

    Layout options (v1.6.0), available on all nine document tools: pageSize (A4 default, Letter, Legal, A3, Tabloid), margins (all four sides, 0–200 pt), running headerTemplate / footerTemplate (left / center / right zones with {page} {pages} {title} {date} placeholders — a footerTemplate replaces the default footer, so footerText is then ignored), compress, and debug.

    +

    Build-time encrypt (v1.6.0), on generate_basic_pdf, add_table, add_form, add_international_text, embed_image, add_barcode and add_chart: AES-128 (default) or AES-256 with owner/user passwords and permissions — and unlike encrypt_pdf, it keeps the AcroForm, making encrypted fillable forms reachable. Exclusive with pdfA (ISO 19005-1 §6.3.2); never cached.

    +

    Print production (v1.6.0), on the nine document tools: print (TrimBox / BleedBox / ArtBox / CropBox, a bleed shorthand, crop + registration marks, /UserUnit), metadata (/Author, /Subject, /Keywords, /Trapped with XMP parity), and outputIntent (custom RGB ICC profile). viewerPreferences gains duplex, pickTrayByPDFSize, printPageRange (1-based), numCopies. Boxes survive merge_pdfs / split_pdf / extract_pages and are reported by inspect_pdf.

    +

    Honest PDF/A (v1.6.0): text rendered through the viewer's base-14 Helvetica is not embedded, so a PDF/A claim on such a file is rejected by veraPDF. embedFonts: true embeds Noto Sans Latin for a valid claim; strict: true fails instead of producing a non-conformant file; includeDiagnostics: true echoes the engine's diagnostics (PDFA_NO_FONT_ENTRIES, PDFA_UNEMBEDDED_FORM_FONT, PDFA_DEVICE_CMYK_IMAGE).

    +

    Reproducible output (v1.6.0): creationDate (ISO-8601) on all nine document tools pins /Info /CreationDate, the XMP dates and therefore the trailer /ID — byte-identical output on the same host time zone.

    +
    +

    add_table#

    +

    Generates a tabular report from column headers and rows.

    +
    {
    +  "title": "Monthly Sales",
    +  "headers": ["Region", "Units", "Revenue"],
    +  "rows": [
    +    ["APAC", "1 200", "$240,000"],
    +    ["EMEA", "800",   "$160,000"]
    +  ],
    +  "infoItems":      [{ "label": "Period", "value": "January 2026" }],
    +  "footerText":     "Internal use only",
    +  "autoFitColumns": true,
    +  "clipCells":      true,
    +  "pdfA":           "pdfa2b",
    +  "outputMode":     "base64"
    +}
    +
    +

    autoFitColumns and clipCells transparently switch to the document-block backend so cell content fits its column or is clipped at the boundary, leveraging pdfnative's TableBlock props. Optional pdfA produces an archive-grade variant.

    +
    +

    Smart-table fields (v1.0.0). add_table exposes the six pdfnative 1.2 TableBlock fields: wrap ('auto' | 'always' | 'never', default 'auto'), repeatHeader (default true), zebra, caption, minRowHeight, cellPadding. Multi-page tables reprint headers and wrap on overflow by default — agent-driven invoice/report workflows get multi-page-safe output out of the box. See the Smart tables guide for full semantics.

    +
    +
    +

    add_barcode#

    +
    {
    +  "format":     "qr",
    +  "data":       "https://pdfnative.dev",
    +  "caption":    "Scan to learn more",
    +  "ecLevel":    "H",
    +  "outputMode": "file",
    +  "outputPath": "tickets/event-42.pdf"
    +}
    +
    +

    Supported formats: qr, code128, ean13, datamatrix, pdf417.
    Error correction levels (QR only): L, M, Q, H.

    +
    +

    add_international_text#

    +
    {
    +  "title":      "مرحبا بالعالم 👋",
    +  "lang":       ["ar", "emoji"],
    +  "paragraphs": [
    +    "هذا اختبار للنص العربي مع تشكيل OpenType ومحارف ثنائية الاتجاه.",
    +    "Mixed content: العربية + English + emoji 🚀 ✓"
    +  ]
    +}
    +
    +

    Supported lang codes: ar (Arabic), he (Hebrew), th (Thai), ja (Japanese), zh (Chinese Simplified), ko (Korean), el (Greek), hi (Devanagari/Hindi), bn (Bengali), ta (Tamil), te (Telugu), si (Sinhala), bo (Tibetan), km (Khmer), my (Myanmar), am (Ethiopic), ru (Cyrillic/Russian), ka (Georgian), hy (Armenian), tr (Turkish), vi (Vietnamese), pl (Polish), plus latin (Noto Sans VF), emoji (Noto Emoji/COLRv1) and the explicit math symbols font (v1.4.0).

    +

    lang accepts string, string[], or a comma-separated value — e.g. "ar,emoji" or ["ar", "emoji"]. When pdfA is set on this tool, the latin font is auto-registered so curly quotes, em-dashes, and ellipses validate cleanly under PDF/A.

    +
    +

    add_form#

    +

    Creates an interactive AcroForm PDF.

    +
    {
    +  "title": "Employee Onboarding",
    +  "fields": [
    +    { "fieldType": "text",     "name": "fullName", "label": "Full Name",   "required": true },
    +    { "fieldType": "dropdown", "name": "dept",     "label": "Department",  "options": ["Engineering", "Sales", "HR"] },
    +    { "fieldType": "checkbox", "name": "agree",    "label": "I agree to the terms", "checked": false }
    +  ],
    +  "outputMode": "base64"
    +}
    +
    +

    Field types: text, textarea, checkbox, radio, dropdown, and listbox (v1.6.0). Text fields accept an optional placeholder (v1.6.0). Since v1.6.0, textarea maps to the engine's true multi-line field (/Ff 4096) — in v1.5.0 it rendered as a single-line widget.

    +
    +

    embed_image#

    +
    {
    +  "title":       "Product Photo",
    +  "imageBase64": "<base64-encoded JPEG bytes>",
    +  "mimeType":    "image/jpeg",
    +  "caption":     "Front view of Model X",
    +  "width":       400,
    +  "outputMode":  "base64"
    +}
    +
    +
    +

    Note: Alpha-channel PNGs (color type 6) are not supported. Pre-process such images to remove the alpha channel before embedding.

    +
    +

    Since v1.6.0 the tool also accepts align (horizontal placement) and alt (accessibility text written as /Figure /Alt in tagged output).

    +
    +

    prepare_signature_placeholder#

    +

    Creates a PDF pre-wired with an AcroForm /Sig field, ready to be signed by sign_pdf.

    +
    {
    +  "title":      "Service Agreement",
    +  "signerName": "Alice Dupont",
    +  "reason":     "Approved",
    +  "location":   "Paris, FR",
    +  "blocks": [
    +    { "type": "paragraph", "text": "By signing below, I accept the terms and conditions." }
    +  ],
    +  "outputMode": "base64"
    +}
    +
    +

    Since v1.6.0 the placeholder also takes subFilter, reserveTimestamp (extra /Contents room for a later RFC 3161 token), placeholderBytes, and signingTime — and the signer metadata (signerName, reason, location, contactInfo, signingTime) is baked into the /Sig dictionary at placeholder time, fixing a pre-1.7 engine bug where those values never reached the signed document.

    +
    +

    sign_pdf#

    +

    Signs any PDF. When the input already contains a /Sig placeholder (e.g. produced by prepare_signature_placeholder) it is signed in place; otherwise the placeholder is auto-injected first (autoInjectPlaceholder defaults to true; set it to false to require an existing placeholder).

    +
    {
    +  "pdfBase64":           "<base64 PDF bytes>",
    +  "algorithm":           "rsa-sha256",
    +  "certDerBase64":       "<base64 X.509 certificate in DER format>",
    +  "rsaKeyPkcs1DerBase64":"<base64 PKCS#1 RSAPrivateKey in DER format>",
    +  "signerName":          "Alice",
    +  "reason":              "Approval",
    +  "location":            "Paris, FR",
    +  "signingTime":         "2026-01-15T10:30:00Z"
    +}
    +
    +

    For ECDSA P-256: use "algorithm": "ecdsa-sha256" and supply either ecPrivateScalarHex (64 hex chars, raw scalar d) or ecPrivateKeyDerBase64 (SEC1 / PKCS#8 DER, base64 — signed through the constant-time node:crypto path) instead of rsaKeyPkcs1DerBase64. The two EC inputs are mutually exclusive.

    +

    PAdES and timestamps (v1.6.0):

    +
      +
    • algorithm also accepts rsa-sha384 and rsa-sha512.
    • +
    • profile: 'pades' produces an ETSI.CAdES.detached signature (PAdES B-B).
    • +
    • timestamp: true requests an RFC 3161 timestamp from the operator-configured TSA (PDFNATIVE_MCP_TSA_URL) and embeds it in the CMS — PAdES B-T. Fails with TSA_NOT_CONFIGURED when no TSA is set, TSA_REJECTED when the authority declines.
    • +
    • certChainDerBase64 embeds intermediate certificates alongside the signer certificate.
    • +
    • fieldName targets a specific /Sig field (SIGNATURE_FIELD_NOT_FOUND when absent, PLACEHOLDER_AMBIGUOUS when several placeholders exist and none is named); allowMultiple: true adds a signature next to existing ones instead of rejecting.
    • +
    • The default placeholder reservation is now max(16384, estimated CMS size) — plus 8192 bytes when timestamp: true — instead of a flat 16384 bytes. To pin it exactly, build the placeholder with prepare_signature_placeholder and its placeholderBytes input.
    • +
    +

    To climb the rest of the PAdES ladder, follow with add_ltv (B-LT) and timestamp_pdf (B-LTA), then check the achieved level with verify_pdf ltv: true. The server's pades_ladder prompt walks through the full recipe.

    +
    +

    inspect_pdf#

    +

    Read-only PDF inspection over openPdf(). Never modifies the input.

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "pages":     true,
    +  "check":     ["pdfa", "signed"]
    +}
    +
    +

    Inputs:

    +
      +
    • pdfBase64 — base64 PDF bytes (required).
    • +
    • password — password for an encrypted PDF (v1.5.0).
    • +
    • pages — when true, includes per-page index, width, height — and, since v1.6.0, the declared page boxes and userUnit.
    • +
    • signatures (v1.6.0) — when true, a per-signature inventory: subFilter, isDocTimestamp, isPlaceholder, byteRange, vriKey.
    • +
    • annotations (v1.6.0) — when true, an annotations[] list (0-based page, subtype, rect, and when present contents, title, color, quadPoints, link url) plus annotationCount.
    • +
    • check — array of CI assertions. Allowed values: pdfa, signed, encrypted, placeholder, attachments, and — (v1.6.0)dss, docTimestamp, trapped, annotations. The response includes checks (per-assertion result) and checksPassed (boolean AND). Since v1.6.0, checks contains only the requested keys.
    • +
    • verbosity'full' (default) or 'summary' (token-frugal scalar subset).
    • +
    • fields — optional dot-path projection of the result.
    • +
    +

    Outputs: version, pageCount, encryption ('none' / 'aes-128' / 'aes-256' / 'rc4' / 'unknown'), optional encryptionInfo ({ algorithm, revision, authenticatedAs }, present when the document is encrypted and opened successfully), pdfA (null or the detected claim string), signatureCount, hasSignaturePlaceholder, attachments[] (embedded-file summaries), info (decoded /Info entries), optional perPage[], optional pageLabels[] (when /PageLabels is declared), optional checks + checksPassed — plus, presence-gated since v1.6.0, dss, docTimestampCount and trapped.

    +

    Useful in CI as a final assertion step before publishing a PDF artifact:

    +
    { "tool": "inspect_pdf",
    +  "input": { "pdfBase64": "<...>", "check": ["pdfa", "signed"] } }
    +// → { ..., "checks": { "pdfa": true, "signed": true }, "checksPassed": true }
    +
    +
    +

    merge_pdfs#

    +

    Concatenates 2–50 PDFs into a single document via pdfnative's page-tree API. Encrypted sources are supported since v1.5.0 via password (a single password applied to every encrypted source), and the output can be re-encrypted via encrypt. Signatures and /AcroForm are dropped because page edits invalidate /ByteRange; self-contained URI /Link annotations are kept.

    +
    {
    +  "pdfsBase64": ["<base64 PDF 1>", "<base64 PDF 2>", "<base64 PDF 3>"],
    +  "outputMode": "base64"
    +}
    +
    +

    Errors: PASSWORD_REQUIRED / PASSWORD_INVALID (encrypted source without / with the wrong password), OUTPUT_TOO_LARGE (50 MiB per emitted PDF; assembly capped by maxOutputSizeBytes, default 256 MiB), PDF_PARSE_FAILED.

    +
    +

    split_pdf#

    +

    Splits one PDF into one document per page range, returning a multi-output shape. Ranges are 0-based and inclusive; end defaults to start (a single page).

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "ranges": [{ "start": 0, "end": 2 }, { "start": 3, "end": 9 }],
    +  "outputMode": "base64"
    +}
    +// → { "mode": "base64", "count": 2, "totalBytes": 123456, "parts": [ { "index": 0, "sizeBytes": 61000, "base64": "..." }, ... ] }
    +
    +

    Each part is capped at 50 MiB; the aggregate output is capped at 200 MiB.

    +
    +

    extract_pages#

    +

    Pulls an arbitrary, order-preserving page subset (max 5000 pages) into a single PDF.

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "pages": [0, 2, 4, 1],
    +  "outputMode": "base64"
    +}
    +
    +

    Page indices are 0-based; the output preserves the order you request.

    +

    annotate_pdf#

    +

    Overlays markup annotations on an existing PDF via incremental update, so the original bytes — and any existing signature — stay intact.

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "annotations": [
    +    { "type": "highlight", "page": 0, "rect": [72, 700, 520, 715], "color": "#ffe066", "contents": "Review this clause" },
    +    { "type": "text",      "page": 1, "rect": [80, 640, 100, 660], "contents": "Sticky note" }
    +  ],
    +  "outputMode": "base64"
    +}
    +
    +

    Types: text, highlight, underline, strikeout, squiggly, square, circle, line, freetext. Each takes a 0-based page, a rect: [x1, y1, x2, y2], and optional color / contents. Encrypted sources → ENCRYPTED_SOURCE; an out-of-range page → a validation error.

    +
    +

    Overlay, not redaction. annotate_pdf is a visual review layer; the underlying bytes remain. It does not remove or obscure content — see the deferred redact_pdf note under What's new in v1.4.0.

    +
    +

    draft_governance_issue#

    +

    Assembles a governance-compliant GitHub-issue draft plus a structured compliance report locally and returns them. It never submits and makes no network call — there is no HTTP client, no GitHub SDK, and no fetch anywhere in the server.

    +
    {
    +  "title": "SVG <text> word-wrap not supported",
    +  "issueType": "feature",
    +  "summary": "renderSvg() emits <text> on a single line with no automatic wrapping.",
    +  "reproduction": {
    +    "command": "node repro.mjs",
    +    "result": "Long <text> overflows the viewport instead of wrapping."
    +  },
    +  "expectedBehavior": "Optional word-wrap for SVG <text>.",
    +  "targetRepo": "pdfnative",
    +  "affectedPackages": ["pdfnative"],
    +  "duplicateSearchPerformed": true,
    +  "outputMode": "inline"
    +}
    +
    +

    duplicateSearchPerformed must be true. A draft that proposes a runtime dependency, omits a reproduction, or sets it to false is rejected with GOVERNANCE_VIOLATION. Present the returned draftMarkdown + compliance to the user; a human submits it. Read the governance_contract / draft_issue_workflow prompts first.

    +

    add_chart (v1.5.0)#

    +

    Renders a chart as native PDF vector paths — no rasterisation, no image round-trip. Multi-series bar/line, legends, gridlines and negative values are supported; the chart is tagged /Figure with /Alt (auto-generated when altText is omitted).

    +
    {
    +  "chartType": "bar",
    +  "title": "Revenue by quarter",
    +  "series": [{ "label": "2026", "values": [1.2, 1.5, 1.4, 1.9] }],
    +  "intro": "Quarterly revenue in $M.",
    +  "pdfA": "pdfa2b",
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: chartType (bar / barH / line / pie / donut, plus — (v1.6.0)stackedBar / stackedBarH / area / scatter) and series (required; pie/donut take exactly one series), plus title, legend ('bottom' / 'none'), markers (line series), colors (hex palette override), altText, intro (paragraph above the chart), pdfA, outputMode / outputPath. For a chart amongst headings/paragraphs/tables, use a chart block inside generate_basic_pdf — both build identical pdfnative blocks.

    +

    Charts v2 (v1.6.0): per-series xValues with xAxis.type: 'linear' | 'time' (UTC-deterministic time axes), a secondary right axis (axis2), axis.scale: 'log', dataLabels, and labelStride / labelRotation for crowded x labels. Engine cross-field rules surface as CHART_ERROR with a remedy.

    +

    read_form_fields (v1.5.0)#

    +

    Read-only enumeration of an existing PDF's AcroForm — the discovery half of the fill round-trip.

    +
    { "pdfBase64": "<base64 PDF bytes>", "verbosity": "full" }
    +
    +

    Inputs: pdfBase64 (required), password (encrypted sources), verbosity ('full' default / 'summary' = { fieldCount } only), fields (dot-path projection).
    Outputs: fieldCount and fields[] — each with name (fully-qualified), type (text / checkbox / radio / dropdown / listbox / button / signature / unknown), current value, readOnly / required / multiline flags, options[] ({ export, label } for choice fields), maxLen, onState, and widgets[] (0-based page + rect).

    +

    fill_form (v1.5.0)#

    +

    Fills (and optionally flattens) the AcroForm of an existing PDF via a non-destructive incremental update, so a prior signature stays valid for its revision. Works on encrypted documents via password (appended objects are encrypted under the document's existing scheme).

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "values": { "fullName": "Alice Dupont", "agree": true },
    +  "flatten": false,
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: pdfBase64 (required), values (name → string | boolean | string[]; omit with flatten: true for a pure flatten), flatten (default false), onUnknownField ('throw' default / 'ignore'), nonWinAnsi ('throw' default / 'needAppearances'), password, outputMode / outputPath.
    Errors: FORM_FIELD_NOT_FOUND, FORM_VALUE_TYPE_ERROR, FORM_UNSUPPORTED (signature fields cannot be filled/flattened), PASSWORD_REQUIRED / PASSWORD_INVALID.

    +

    encrypt_pdf (v1.5.0)#

    +

    Re-secures an existing PDF with AES-128 (default, widest compatibility) or AES-256 via the page-tree re-encryption path. An already-encrypted source can be rotated to a new password by supplying its current password.

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "ownerPassword": "s3cret-owner",
    +  "userPassword": "open-me",
    +  "algorithm": "aes256",
    +  "permissions": { "print": true, "copy": false, "modify": false, "extractText": false },
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: pdfBase64 and ownerPassword (required), userPassword (omitted/empty = opens without a prompt), algorithm ('aes128' default / 'aes256'), permissions ({ print?, copy?, modify?, extractText? }, each allowed when omitted), password (current password of an encrypted source), outputMode / outputPath.

    +
    +

    Like merge/split/extract, encryption rebuilds the page tree: existing signatures and /AcroForm are dropped, and only self-contained URI link annotations are kept. Encrypt before signing, not after. Excluded from the response cache.

    +
    +

    decrypt_pdf (v1.5.0)#

    +

    Opens an encrypted PDF (RC4, AES-128 or AES-256) and emits an unencrypted copy in-server — no external tool needed.

    +
    { "pdfBase64": "<base64 encrypted PDF>", "password": "open-me", "outputMode": "base64" }
    +
    +

    Inputs: pdfBase64 (required), password (user or owner; omit only for documents with an empty user password), outputMode / outputPath.

    +
    +

    The rebuild drops signatures and /AcroForm. To read an encrypted PDF without rebuilding it, pass password to inspect_pdf / extract_text / extract_attachments instead. Excluded from the response cache.

    +
    +

    add_ltv (v1.6.0)#

    +

    Embeds long-term-validation material — a /DSS (Document Security Store) with /VRI entries — into an already-signed PDF via incremental update, taking a PAdES B-T document to B-LT.

    +
    {
    +  "pdfBase64": "<base64 signed PDF>",
    +  "mode": "online",
    +  "preferOcsp": true,
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: pdfBase64 (required), mode ('online' default — fetches OCSP responses / CRLs through the operator-configured revocation provider; 'offline' — embeds parse-validated caller-supplied material), preferOcsp, certificatesDerBase64[] / ocspResponsesDerBase64[] / crlsDerBase64[] (offline material), outputMode / outputPath. The structured result includes a summary of what was embedded.

    +

    Errors: LTV_NO_SIGNATURE (nothing to validate), LTV_EMPTY (no material gathered), LTV_MATERIAL_INVALID, LTV_ERROR, REVOCATION_NOT_CONFIGURED (online mode without PDFNATIVE_MCP_REVOCATION), NETWORK_HOST_NOT_ALLOWED, NETWORK_ERROR, ENCRYPTED_SOURCE (decrypting would destroy the signatures — sign before encrypting).

    +
    +

    Online mode is the only network-touching path in the server besides the TSA, and it only ever contacts operator-allow-listed hosts. See Environment variables above.

    +
    +

    timestamp_pdf (v1.6.0)#

    +

    Appends a /DocTimeStamp signature field (ETSI.RFC3161) through the operator-configured TSA, taking a B-LT document to B-LTA. The token's status, message imprint and nonce are checked before embedding; its own CMS signature is verified by verify_pdf.

    +
    {
    +  "pdfBase64": "<base64 signed PDF>",
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: pdfBase64 (required), fieldName (auto-suffixed DocTimeStamp1, DocTimeStamp2, … when omitted — supporting periodic re-timestamping), placeholderBytes (4096–65536), outputMode / outputPath.

    +

    Errors: TSA_NOT_CONFIGURED, TSA_REJECTED, NETWORK_ERROR, ENCRYPTED_SOURCE.

    +

    update_metadata (v1.6.0)#

    +

    Rewrites an existing PDF's /Info dictionary — and its XMP packet when present — via a non-destructive incremental update, so earlier revisions stay a byte-exact prefix.

    +
    {
    +  "pdfBase64": "<base64 PDF bytes>",
    +  "title": "Quarterly Report — Final",
    +  "author": "Finance Team",
    +  "keywords": "finance, Q1, 2026",
    +  "modDate": "2026-08-25T09:00:00+02:00",
    +  "outputMode": "base64"
    +}
    +
    +

    Inputs: pdfBase64 (required), title, author, subject, keywords (≤ 1000 characters), modDate (ISO-8601 with offset — pinned for reproducible bytes), outputMode / outputPath.

    +

    Errors: ENCRYPTED_SOURCE (use decrypt_pdfupdate_metadataencrypt_pdf), METADATA_ERROR, PDF_PARSE_FAILED.

    +

    inspect_layout (v1.6.0)#

    +

    A read-only pagination dry run: measures how a prospective document would paginate — without producing a PDF and without consuming output tokens on base64 bytes. The seventh read tool with verbosity / fields.

    +
    {
    +  "title": "Q1 2026 Report",
    +  "blocks": [
    +    { "type": "heading", "text": "Executive Summary", "level": 1 },
    +    { "type": "paragraph", "text": "Revenue grew 24 % year over year." }
    +  ],
    +  "verbosity": "summary"
    +}
    +// → { "pageWidth": 595.28, "pageHeight": 841.89, "totalPages": 1, "blockCount": 2 }
    +
    +

    Inputs: title and blocks (required), plus every input that moves a block — footerText, pdfA, normalize, embedFonts, pageSize, margins, headerTemplate, footerTemplate — and verbosity / fields.

    +

    Outputs (full): pageWidth, pageHeight, margins, totalPages, and pages[].blocks[] with each block's type, page, x, top, width, height (2-decimal points).

    +
    +

    Known engine gap: a toc block is measured as 0 pt, so a document with a printed contents page may paginate one page later than previewed.

    +
    +
    +

    MCP prompts#

    +

    Since v1.4.0 the server advertises the MCP prompts capability; v1.6.0 grows it to six prompts — the two governance prompts plus four recipe prompts:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PromptPurpose
    governance_contractThe full AI-governance / Human-in-the-Loop contract.
    draft_issue_workflowThe step-by-step recipe for producing a compliant issue draft with draft_governance_issue.
    pades_ladder (v1.6.0)The B-B → B-T → B-LT → B-LTA recipe: sign_pdfadd_ltvtimestamp_pdf, verified with verify_pdf ltv: true.
    print_ready (v1.6.0)Producing press-ready output: bleed, printer's marks, custom OutputIntent.
    reproducible_output (v1.6.0)Byte-stable output via pinned creationDate / signingTime / modDate.
    pdfa_valid (v1.6.0)Producing a PDF/A file that veraPDF actually accepts (embedFonts, strict, diagnostics).
    +

    Error codes#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    codeRaised byMeaning / fix
    ENCRYPTED_SOURCEannotate_pdf, update_metadata, add_ltv, timestamp_pdfThe source PDF is encrypted and the tool has no password parameter. Remedies are tool-specific since v1.6.0: for annotate_pdf / update_metadata, decrypt_pdf → edit → encrypt_pdf; for add_ltv / timestamp_pdf, decrypting would destroy the signatures — sign before encrypting.
    PASSWORD_REQUIRED (v1.5.0)password-aware tools (inspect_pdf, verify_pdf, extract_text, extract_attachments, read_form_fields, fill_form, page-tree trio, encrypt_pdf, decrypt_pdf)The source is encrypted and no password was supplied.
    PASSWORD_INVALID (v1.5.0)password-aware toolsThe supplied password does not open the document.
    ENCRYPTION_UNSUPPORTED (v1.5.0)password-aware toolsThe document uses an encryption scheme the reader does not support.
    FORM_FIELD_NOT_FOUND (v1.5.0)fill_formA values key matches no field (with onUnknownField: 'throw', the default).
    FORM_VALUE_TYPE_ERROR (v1.5.0)fill_formA value's type does not match the field (e.g. a boolean for a text field).
    FORM_UNSUPPORTED (v1.5.0)fill_formThe field cannot be filled/flattened (e.g. a signature field).
    OUTPUT_TOO_LARGEPDF-emitting toolsAn emitted PDF exceeds 50 MiB (for split_pdf, also the 200 MiB aggregate), or assembly exceeds maxOutputSizeBytes (default 256 MiB).
    PDF_PARSE_FAILEDpage-tree toolsThe source bytes are not a parseable PDF.
    GOVERNANCE_VIOLATIONdraft_governance_issueThe draft breaks the AI-governance contract (proposes a runtime dependency, omits a reproduction, or duplicateSearchPerformed: false). Fix the draft and retry.
    TSA_NOT_CONFIGURED (v1.6.0)sign_pdf (timestamp: true), timestamp_pdfNo PDFNATIVE_MCP_TSA_URL is set. Configure the TSA in the server environment.
    TSA_REJECTED (v1.6.0)sign_pdf, timestamp_pdfThe timestamp authority declined the request.
    REVOCATION_NOT_CONFIGURED (v1.6.0)add_ltv (mode: 'online')No PDFNATIVE_MCP_REVOCATION sources are set. Configure them, or use mode: 'offline' with caller-supplied material.
    NETWORK_HOST_NOT_ALLOWED (v1.6.0)add_ltvA certificate-supplied OCSP/CRL URL points outside PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS.
    NETWORK_ERROR (v1.6.0)TSA/LTV toolsThe TSA/OCSP/CRL request failed (timeout, refusal, size cap).
    LTV_NO_SIGNATURE / LTV_EMPTY / LTV_MATERIAL_INVALID / LTV_ERROR (v1.6.0)add_ltvNo signature to validate / no material gathered / supplied DER material does not parse / embedding failed.
    METADATA_ERROR (v1.6.0)update_metadataThe incremental /Info + XMP rewrite failed.
    PRINT_ERROR (v1.6.0)document tools with printThe requested page boxes are inconsistent (e.g. do not fit pageSize).
    CHART_ERROR (v1.6.0)add_chart, chart blocksAn engine cross-field chart rule failed (the message carries the remedy).
    PLACEHOLDER_AMBIGUOUS (v1.6.0)sign_pdfSeveral unsigned placeholders exist and no fieldName was given.
    SIGNATURE_FIELD_NOT_FOUND (v1.6.0)sign_pdfThe named fieldName does not exist.
    CMS_PARSE_FAILED (v1.6.0)verify_pdfA CMS structure is shorter or more malformed than the parser expects.
    +
    +

    Protocol errors (v1.6.0): calling an unknown tool or prompt name is now a JSON-RPC -32602 error ([UNKNOWN_TOOL] / [UNKNOWN_PROMPT]), not an isError result. Likewise, an unknown or misspelt input key — top-level or nested — fails with VALIDATION_ERROR ("Unrecognized key") instead of being silently stripped.

    +
    +
    +

    The pdfA flag#

    +

    Every document tool (generate_basic_pdf, add_table, add_form, embed_image, add_barcode, prepare_signature_placeholder, add_international_text, add_chart (v1.5.0)) accepts an optional pdfA field. add_attachment is the special case: it always produces PDF/A-3b (the only conformance level that allows arbitrary embedded files), so it has no pdfA input.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    pdfA valuePDF versionNotes
    "pdfa1b"1.4Most conservative — no transparency, no AES
    "pdfa2b"1.7Default archive target
    "pdfa2u"1.72b + Unicode mapping for every glyph
    "pdfa3b"1.72b + arbitrary /EmbeddedFile attachments
    +

    When set on add_international_text, the latin font auto-registers so non-WinAnsi Latin characters validate cleanly. Mutually exclusive with the underlying pdfnative encryption layer (ISO 19005-1 §6.3.2).

    +
    +

    Output modes#

    +

    Every document-producing tool accepts an outputMode field. The seven read-only +tools (inspect_pdf, inspect_layout, validate_pdf, verify_pdf, extract_text, +extract_attachments, read_form_fields) return JSON only and have no +outputMode:

    + + + + + + + + + + + + + + + +
    ModeBehaviour
    "base64" (default)The PDF bytes are returned inline in the MCP response as a base64 string. Suitable for pipelines that immediately consume or display the bytes.
    "file"The PDF is written to the sandbox directory configured via PDFNATIVE_MCP_OUTPUT_DIR. An outputPath (relative, .pdf extension) is required. Disabled unless the environment variable is set.
    +
    +

    Exception: draft_governance_issue uses outputMode: "inline" | "file" (not base64) and writes a .md draft — not a PDF — when file mode is selected.

    +
    +
    +

    End-to-end example: signed document#

    +

    This workflow uses two tools in sequence:

    +
    // Step 1 — create the placeholder
    +{
    +  "tool": "prepare_signature_placeholder",
    +  "input": {
    +    "title":      "Purchase Order #42",
    +    "signerName": "Jane Smith",
    +    "reason":     "CFO approval",
    +    "location":   "London, UK",
    +    "blocks": [
    +      { "type": "paragraph", "text": "Total amount: $128,000" }
    +    ],
    +    "outputMode": "base64"
    +  }
    +}
    +
    +// Step 2 — sign the returned PDF
    +{
    +  "tool": "sign_pdf",
    +  "input": {
    +    "pdfBase64":            "<result from step 1>",
    +    "algorithm":            "rsa-sha256",
    +    "certDerBase64":        "<your DER certificate>",
    +    "rsaKeyPkcs1DerBase64": "<your PKCS#1 private key>",
    +    "signerName":           "Jane Smith",
    +    "reason":               "CFO approval",
    +    "location":             "London, UK",
    +    "signingTime":          "2026-04-26T09:00:00Z",
    +    "outputMode":           "base64"
    +  }
    +}
    +
    +
    +

    Security model#

    +

    pdfnative-mcp is designed to run safely inside your AI client:

    +
      +
    • No network access by default — the server opens no outbound connection unless the operator configures TSA/OCSP/CRL endpoints for the PAdES tools (see the network charter under Environment variables). URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard.
    • +
    • Sandboxed file writesfile output mode is gated by PDFNATIVE_MCP_OUTPUT_DIR. When unset, file writes are rejected with a SecurityError.
    • +
    • Path traversal protection — absolute paths, .. sequences, NUL bytes, and non-.pdf extensions are all rejected.
    • +
    • Output size cap — PDF output is capped at 50 MiB per call. split_pdf additionally enforces 50 MiB per part and a 200 MiB aggregate ceiling across its parts, returning OUTPUT_TOO_LARGE when exceeded.
    • +
    • HTTP transport hardening — when bound to PDFNATIVE_MCP_PORT, the HTTP transport is loopback-only and rejects foreign Host / Origin headers with 403 (DNS-rebinding protection). An opt-in bearer token (PDFNATIVE_MCP_HTTP_TOKEN) adds authentication (v1.6.0) — without it, the loopback endpoint has none.
    • +
    • Input validation — every tool validates inputs against strict JSON Schemas and Zod runtime checks at the boundary. Since v1.6.0 every schema is .strict() at every nesting level, so an unknown or misspelt key is a VALIDATION_ERROR instead of being silently stripped, and PEM armour where DER base64 is expected fails with the exact openssl … -outform DER remedy.
    • +
    • Decompression cap — the engine's 100 MiB per-stream inflate cap is operator-tunable via PDFNATIVE_MCP_MAX_INFLATE_BYTES (v1.6.0).
    • +
    +

    See SECURITY.md for responsible disclosure.

    +
    +

    Troubleshooting#

    +

    The server does not appear in my AI client.
    Verify that Node.js ≥ 22 is installed (node --version) and that the config file path is correct for your OS. Restart the client after any config change.

    +

    file output mode returns a SecurityError.
    Set the PDFNATIVE_MCP_OUTPUT_DIR environment variable to an existing absolute path in the client config.

    +

    add_international_text produces blank text.
    The Noto fonts ship bundled with pdfnative — no network access is needed. Blank text usually means an unsupported lang code or a corrupted install; check the lang value against the supported list and reinstall pdfnative if needed.

    +

    sign_pdf fails with "invalid placeholder".
    By default sign_pdf auto-injects a /Sig placeholder when the input has none (autoInjectPlaceholder: true), so this error normally only appears when autoInjectPlaceholder was set to false — either drop that flag or call prepare_signature_placeholder first, and make sure the placeholder PDF was not modified between creation and signing.

    +

    Output PDF exceeds 50 MB.
    Split the content across multiple tool calls or reduce image/barcode count.

    +
    +

    Release history#

    +

    The current release is v1.6.0 (28 tools, built on pdfnative 1.7.0 — see the header note). Per-release notes, oldest first:

    +

    What's new in v1.0.0#

    +

    v1.0.0 is the first stable release, built on pdfnative 1.2.0, and commits to API stability via a per-tool _meta.apiVersion field. New fields are optional and backward-compatible — omitting them produces byte-identical output. Tool count: 12 (was 9).

    +
      +
    • Three new tools:
        +
      • verify_pdf — read-only verification of every PAdES Baseline / adbe.pkcs7.detached signature: recomputes the ByteRange SHA-256, validates the CMS messageDigest, and verifies the signatureValue with the embedded signer certificate (RSA-SHA256 + ECDSA-SHA256 P-256). Optional trustedRootsDerBase64 enables chain trust.
      • +
      • add_attachment — generate a PDF/A-3 (ISO 19005-3) document with one or more embedded files. Primary use case: Factur-X / ZUGFeRD electronic invoices (XML payload with relationship: 'Source'). 8 MiB per-file cap.
      • +
      • extract_text — best-effort plain-text extraction from a non-encrypted PDF (operands of Tj / ' / " / TJ). Reports extractable: false when a page yields no text; encrypted PDFs are rejected.
      • +
      +
    • +
    • Smart-table fields on add_tablewrap, repeatHeader, zebra, caption, minRowHeight, cellPadding (pdfnative 1.2 TableBlock props). Multi-page tables reprint headers and wrap on overflow by default.
    • +
    • Signing ergonomicssign_pdf accepts ECDSA SEC1 / PKCS#8 DER keys and autoInjectPlaceholder: true (default) transparently calls addSignaturePlaceholder() when the input lacks a /Sig widget (one-call signing of any PDF).
    • +
    • inspect_pdf now reports hasSignaturePlaceholder and an attachments[] summary; two new check values: placeholder, attachments. The signed check is now signatureCount > 0 && !hasSignaturePlaceholder.
    • +
    • Opt-in result cache (PDFNATIVE_MCP_CACHE_DIR) — SHA-256 keyed over canonical JSON of {tool, apiVersion, input}, 1 h TTL, 256 MiB LRU. Skips outputMode: 'file' calls.
    • +
    • _meta.apiVersion = '1.0.0' and per-tool _meta.examples on every tool listing for AI-agent discovery — anchored to docs/API_STABILITY.md.
    • +
    • pdfA flag on every document tool — values pdfa1b, pdfa2b, pdfa2u, pdfa3b, mapping to pdfnative's tagged option (spread straight from pdfnative's exported PDF_A_CONFORMANCE_TARGETS).
    • +
    • Env-var rename: the canonical name is PDFNATIVE_MCP_OUTPUT_DIR (was the misspelt PDFNATIVE_MPC_OUTPUT_DIR, which still works as a deprecated alias with a one-shot stderr warning, scheduled for removal in v2.0.0).
    • +
    +

    Deferred to v1.1: merge_pdfs, split_pdf, redact_pdf — require pdfnative page-tree primitives not yet exported.

    +

    What's new in v1.1.0#

    +

    v1.1.0 upgrades the server to the pdfnative 1.3.0 engine and adds accessibility-first read tooling:

    +
      +
    • New tool: validate_pdf — read-only PDF/UA structural conformance checks (ISO 14289-1).
    • +
    • Six additional scripts in add_international_text: Telugu (te), Sinhala (si), Tibetan (bo), Khmer (km), Myanmar (my), Ethiopic (am).
    • +
    • COLRv1 colour emoji support through the upgraded engine.
    • +
    • Paragraph newline sanitization to avoid malformed text blocks when callers send embedded \n.
    • +
    +

    What's new in v1.2.0#

    +

    v1.2.0 keeps full backward compatibility and extends MCP ergonomics for AI workflows:

    +
      +
    • New tool: extract_attachments — read embedded files from PDF/A-3 documents (Factur-X / ZUGFeRD round-trip).
    • +
    • Watermark support on generate_basic_pdf and add_table (text, opacity, angle, color, position).
    • +
    • Opt-in Unicode normalization (normalize: NFC / NFD / NFKC / NFKD) on international/document flows.
    • +
    • Token-frugal read modes on read-only tools via verbosity: 'summary' and selective fields projection.
    • +
    • Base64 payload deduplication: generated bytes are returned once via resource in base64 mode.
    • +
    +

    What's new in v1.3.0#

    + +

    v1.3.0 upgrades the engine to pdfnative 1.4.0, adds a page-tree tool trio, and stays fully backward-compatible — tool count rises to 17:

    +
      +
    • Three new page-tree tools:
        +
      • merge_pdfs — concatenate 2–50 source PDFs into one document via pdfnative's page-tree API. Encrypted sources are rejected; signatures and /AcroForm are dropped (page edits invalidate /ByteRange); self-contained URI /Link annotations are preserved.
      • +
      • split_pdf — split one PDF into one document per page range, returning a multi-output shape { mode, count, totalBytes, parts[] }. Caps: 50 MiB per part, 200 MiB aggregate.
      • +
      • extract_pages — pull an arbitrary, order-preserving page subset (max 5000 pages) into a single PDF.
      • +
      +
    • +
    • Enriched authoring options (from pdfnative 1.4.0):
        +
      • generate_basic_pdf gains outline ('auto' or an explicit nested tree), pageLabels, nested list items (max depth 6), and viewerPreferences.
      • +
      • add_table gains cellBorders (top/right/bottom/left, color, width, style), cellVAlign ('top' / 'middle' / 'bottom'), and viewerPreferences.
      • +
      • add_international_text gains viewerPreferences.
      • +
      +
    • +
    • Constant-time signingsign_pdf now signs RSA and EC-DER keys through a per-call node:crypto provider (constant-time) with a transparent pure-JS fallback; pure-scalar ecPrivateScalarHex remains pure-JS.
    • +
    • HTTP transport hardening — the PDFNATIVE_MCP_PORT Streamable HTTP transport now rejects foreign Host / Origin headers with 403 (DNS-rebinding protection). serverInfo advertises a human-readable title + description (MCP 2025-11-25 alignment).
    • +
    • New error codes on the page-tree tools: ENCRYPTED_SOURCE, OUTPUT_TOO_LARGE, PDF_PARSE_FAILED.
    • +
    • Spec alignment — MCP SDK ^1.29 (2025-11-25 revision), JSON Schema 2020-12, and _meta.apiVersion bumped to 1.3.0 on every tool.
    • +
    +
    +

    What's new in v1.4.0#

    +

    v1.4.0 upgrades the engine to pdfnative 1.5.0, brings the pdfnative AI-governance / Human-in-the-Loop (HITL) system to the MCP surface, and adds markup annotations — taking the catalogue to 19 tools at that release. Fully backward-compatible: every v1.3.0 call works unchanged and default responses are byte-identical.

    +
      +
    • Two new tools:
        +
      • draft_governance_issue — an agent drafts a fully compliant GitHub issue locally (a draft .md plus a machine-readable compliance report) and stops. The agent is a draftsman, never an autonomous submitter: a human is the only gate, and — by construction, not just policy — the server makes zero GitHub writes and no outbound network call. A draft that proposes a runtime dependency, omits a reproduction, or sets duplicateSearchPerformed: false is rejected with the new GOVERNANCE_VIOLATION error.
      • +
      • annotate_pdf — overlay text, highlight, underline, strikeout, squiggly, square, circle, line, and freetext annotations on an existing PDF via incremental update. A visual review layer, not a redaction — the underlying bytes remain. Encrypted sources → ENCRYPTED_SOURCE.
      • +
      +
    • +
    • MCP prompts capability — the server now advertises two prompts: governance_contract (the full HITL contract) and draft_issue_workflow (the step-by-step recipe).
    • +
    • Page labels in inspect_pdf — read-only surfacing of /PageLabels ranges via a new optional pageLabels[] output field (present only when the PDF declares them).
    • +
    • Math / scientific scriptadd_international_text accepts the explicit math lang (Noto Sans Math), embedded on demand only (e.g. lang: ['latin', 'math']); there is no global auto-routing.
    • +
    • Engine upgrade — pdfnative ^1.4.0^1.5.0 (additive, no breaking changes); _meta.apiVersion bumped to 1.4.0 on every tool.
    • +
    +
    +

    redact_pdf is deferred by design. pdfnative's annotation writer can only overlay content; an overlay-only “redaction” would leave the original bytes intact and create false security. It is intentionally not shipped and is tracked as an upstream true content-removal request — a fitting first use of draft_governance_issue.

    +
    +

    What's new in v1.5.0#

    +

    v1.5.0 upgrades the engine to pdfnative 1.6.0 and takes the catalogue to 24 tools at that release, closing the read/modify loop on documents the assistant did not create.

    +
      +
    • Five new tools:
        +
      • add_chart — bar, horizontal-bar, line, pie and donut charts drawn as native PDF vector paths. No rasterisation and no image round-trip, so the output stays sharp at any zoom and tags as /Figure with alt text. If you were previously asking an agent to render a chart to PNG and push it through embed_image, stop — this replaces that.
      • +
      • read_form_fields — list an AcroForm's fields with types, current values and options.
      • +
      • fill_form — fill values and optionally flatten them into static page content. Works on encrypted documents via incremental update.
      • +
      • encrypt_pdf — re-secure with AES-128 or AES-256, setting owner/user passwords and a permission set.
      • +
      • decrypt_pdf — remove encryption in-server, for RC4, AES-128 and AES-256 sources.
      • +
      +
    • +
    • Passwords on the read tools. inspect_pdf, verify_pdf, extract_text and extract_attachments accept a password, as do the page-tree trio. Encrypted sources are no longer rejected outright.
    • +
    • New error codes: PASSWORD_REQUIRED, PASSWORD_INVALID, ENCRYPTION_UNSUPPORTED, FORM_FIELD_NOT_FOUND, FORM_VALUE_TYPE_ERROR, FORM_UNSUPPORTED.
    • +
    • Generated PDFs are exposed as MCP resources (pdfnative://output/{path}), so a client can list and re-read them without a second tool call. File-mode results carry a resource_link.
    • +
    • Tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are published in tools/list, letting a client reason about which calls are safe to retry.
    • +
    • The encryption tools are deliberately excluded from the response cache.
    • +
    +

    What's new in v1.6.0#

    +

    v1.6.0 aligns the server with the MCP 2026-07-28 specification and grows the catalogue to 28 tools, exposing the engine's full document model and completing the PAdES baseline ladder. No breaking changes: every v1.5.0 call keeps its inputs, error codes and response shape, and a superset gate against the published 1.5.0 catalogue proves it.

    +
      +
    • Four new tools:
        +
      • add_ltv — embed /DSS + /VRI long-term-validation material (PAdES B-LT), online through the operator-configured revocation provider or offline from caller-supplied material.
      • +
      • timestamp_pdf — append a /DocTimeStamp through the operator TSA (PAdES B-LTA), with auto-suffixed field names for periodic re-timestamping.
      • +
      • update_metadata — rewrite /Info (+ XMP) of an existing PDF as an incremental update.
      • +
      • inspect_layout — a read-only pagination dry run: page count and block positions with no PDF produced.
      • +
      +
    • +
    • Full engine coveragegenerate_basic_pdf composes all 13 block kinds (table, image, link, toc, barcode, svg, formField join the six existing ones); pageSize, margins, running headerTemplate / footerTemplate, compress and debug on the nine document tools; build-time encrypt that keeps the AcroForm; image watermarks with position.
    • +
    • PAdES signingsign_pdf gains profile: 'pades', timestamp: true (RFC 3161, B-T), RSA-SHA384/512, certChainDerBase64, fieldName / allowMultiple; verify_pdf ltv: true reports profile, timestamp, embedded revocation status and the achieved level; prepare_signature_placeholder gains subFilter / reserveTimestamp and freezes signer metadata into the /Sig dictionary (fixing a pre-1.7 engine bug where those values were silently dropped).
    • +
    • Network charter — still no outbound request by default; the single permitted egress class is the operator-configured TSA / OCSP / CRL endpoints, never a URL from a tool argument, behind an SSRF guard. Seven new environment variables configure the HTTP bearer token, the inflate cap and the network providers.
    • +
    • Print productionprint page boxes, bleed, printer's marks, /UserUnit, metadata with /Trapped, custom outputIntent; boxes survive the page-tree tools and are reported by inspect_pdf.
    • +
    • Charts v2stackedBar / stackedBarH / area / scatter, per-series xValues with linear or time axes, secondary axis2, axis.scale: 'log', dataLabels, labelStride / labelRotation.
    • +
    • Honest PDF/AembedFonts: true (embed Noto Sans Latin for a claim veraPDF accepts), strict: true (fail instead of emitting a non-conformant file), includeDiagnostics: true; two new diagnostics; an advisory veraPDF corpus runs locally and in CI.
    • +
    • MCP 2026-07-28 transport — SDK v2 (@modelcontextprotocol/server), server/discover, resultType, cache hints, per-result serverInfo, deterministic tools/list; 2025-era clients keep working via the automatic legacy fallback.
    • +
    • Four new recipe promptspades_ladder, print_ready, reproducible_output, pdfa_valid — taking the prompt catalogue to six.
    • +
    • Reproducible outputcreationDate on all nine document tools, signingTime on the placeholder, modDate on update_metadata.
    • +
    +

    Migrating from v1.5.0 — six behaviour changes, all on error paths or on inputs the published schema already declared invalid:

    +
      +
    1. Stray keys fail. An unknown or misspelt key, top-level or nested, is now VALIDATION_ERROR ("Unrecognized key") instead of being silently stripped — the schemas always declared additionalProperties: false.
    2. +
    3. Page-index mistakes are VALIDATION_ERROR. Out-of-range pages / ranges on merge_pdfs / split_pdf / extract_pages were PDF_PARSE_FAILED; the message now carries a 0-based hint.
    4. +
    5. validate_pdf on unparsable bytes is an error, not a verdict. v1.5.0 returned { valid: false }; v1.6.0 returns isError: true [PDF_PARSE_FAILED] — a parse failure is not a PDF/UA verdict.
    6. +
    7. Unknown tool or prompt names are protocol errors — JSON-RPC -32602 ([UNKNOWN_TOOL] / [UNKNOWN_PROMPT]), no isError result.
    8. +
    9. inspect_pdf.checks holds only the keys you asked for — read checksPassed or the requested key, never an absent one.
    10. +
    11. add_form text areas change bytes. fieldType: 'textarea' now produces a real multi-line field (/Ff 4096).
    12. +
    +
    +

    Further reading#

    + +
    diff --git a/docs/guides/onboarding.html b/docs/guides/onboarding.html index d09060d..1d1c53f 100644 --- a/docs/guides/onboarding.html +++ b/docs/guides/onboarding.html @@ -1,87 +1,198 @@ - - - - - - Onboarding — pdfnative, CLI, MCP & React - - - - + + + + + + Onboarding — pdfnative, CLI, MCP & React + + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Onboarding

    - -
    - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Onboarding

    +
    + +

    Onboarding — the pdfnative ecosystem in 90 seconds

    +
    +

    Pick your entry point: the library for code, the CLI for shell scripts, MCP for AI assistants, and React for declarative JSX. They all produce the same ISO 32000-1 / PDF/A-conformant PDFs from the same zero-dependency engine. Live versions for every package are shown at the top of the documentation home.

    +
    +
    +

    1. Library (Node, browser, Deno, Bun) — 30 seconds#

    +
    npm install pdfnative
    +
    +
    import { buildDocumentPDFBytes, registerFont, loadFontData } from 'pdfnative';
    +
    +// Optional: enable a non-Latin script — register under the script code ('ar'),
    +// then load the data and pass it via fontEntries (registration alone is a no-op):
    +registerFont('ar', () => import('pdfnative/fonts/noto-arabic-data.js'));
    +const ar = await loadFontData('ar');
    +if (!ar) throw new Error('Arabic font failed to load');
    +
    +// Synchronous — returns a Uint8Array, not a Promise.
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Hello pdfnative',              // top-level, not inside metadata
    +  metadata: { author: 'Me' },            // author / subject / keywords only
    +  blocks: [
    +    { type: 'heading', text: 'Hello pdfnative', level: 1 },
    +    { type: 'paragraph', text: 'Pure native PDF, zero runtime dependencies.' },
    +  ],
    +  fontEntries: [{ fontData: ar, fontRef: '/F3', lang: 'ar' }], // /F1 and /F2 are reserved
    +  layout: { tagged: 'pdfa2b' },          // optional PDF/A-2b
    +});
    +
    +// In Node: await fs.writeFile('out.pdf', bytes);
    +// In browser: new Blob([bytes], { type: 'application/pdf' });
    +
    +

    Next: Quick Start → · Architecture → · PDF/A conformance →

    +
    +

    2. CLI — 30 seconds#

    +
    npm install -g pdfnative-cli   # or: npx pdfnative-cli ...
    +
    +
    +

    The package is named pdfnative-cli, but the binary it puts on your PATH is +pdfnative. Under npx, either name works.

    +
    +
    # Check the environment first — the fastest way to confirm the install worked
    +pdfnative doctor
    +
    +# Render a JSON document → PDF. Input and output are flags, not positionals;
    +# with neither, render reads stdin and writes stdout.
    +pdfnative render --input doc.json --output out.pdf --tagged pdfa2b
    +
    +# Sign it (auto-injects a signature placeholder if needed)
    +pdfnative sign --input out.pdf --output signed.pdf \
    +  --key signer.key --cert signer.crt --algorithm rsa-sha256
    +
    +# Verify the embedded CMS signature
    +pdfnative verify --input signed.pdf --json
    +
    +

    Iteration helpers: --watch re-renders on save, --template injects variables, --font enables any of the 22 bundled scripts + colour emoji + the math font. v1.1.0 added --stream-true, inspect --pdfua (accessibility gate), and an agent-native --json/E_*/--dry-run contract; v1.2.0 added page-tree merge / split / extract, markup annotate, an AI-governance govern gate, and render --outline / --font math / --inspect-layout; v1.3.0 adds fill, encrypt, decrypt, extract-text and doctor, native chart blocks in render, passwords on the page-tree commands, and PowerShell completion.

    +
    +

    Upgrading from v1.2.0? render --encrypt was a silent no-op in that +release — documents you thought were encrypted were written in the clear. +v1.3.0 fixes it and unifies the flags under --encrypt / --owner-password / +--user-password / --permissions. Re-run any affected job.

    +
    +

    Next: CLI guide → · CLI playground →

    +
    +

    3. MCP (Claude Desktop, Cursor, Continue, Zed) — 30 seconds#

    +
    npm install -g pdfnative-mcp
    +
    +

    Add the server to your client config — Claude Desktop example (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

    +
    {
    +  "mcpServers": {
    +    "pdfnative": {
    +      "command": "npx",
    +      "args": ["-y", "pdfnative-mcp"]
    +    }
    +  }
    +}
    +
    +

    Then prompt your assistant:

    +
    +

    Create a PDF/A-2b invoice for ACME Inc, add a multilingual paragraph in Arabic, and sign it with my key.

    +
    +

    The assistant calls generate_basic_pdf (with pdfA: "pdfa2b"), then add_international_text (with lang: ["ar", "emoji"]), add_table, sign_pdf, and finally inspect_pdf — confirming the result. v1.6.0 ships 28 tools, including the complete PAdES ladder (sign_pdf with RFC 3161 timestamps, add_ltv, timestamp_pdf), the page-tree trio merge_pdfs, split_pdf, extract_pages, markup annotate_pdf, update_metadata, the read-only inspect_layout pagination preview, the network-free draft_governance_issue, plus validate_pdf, verify_pdf, add_attachment, extract_attachments, and extract_text.

    +

    Next: MCP guide → · MCP playground →

    +
    +

    4. React (Next.js, Remix, any React 19 app) — 30 seconds#

    +
    npm install pdfnative-react pdfnative react
    +
    +
    import { Document, Heading, Text, Table, renderToBytes } from 'pdfnative-react';
    +
    +const bytes = renderToBytes(
    +  <Document title="Invoice #1024" footerText="Acme Inc">
    +    <Heading level={1}>Invoice #1024</Heading>
    +    <Text>Thank you for your business.</Text>
    +    <Table headers={['Item', 'Total']} rows={[{ cells: ['Pro plan', '$49.00'], type: 'default', pointed: false }]} zebra />
    +  </Document>,
    +); // → Uint8Array, a valid PDF
    +
    +

    A custom React reconciler compiles your JSX to pdfnative blocks on-device — no DOM, no headless browser. Preview live with the usePdf hook / PDFViewer, or let AI agents author with the token-frugal DocSpec. React 19 is a peer dependency of pdfnative-react only — the engine stays zero-dependency.

    +

    Next: React guide → · React playground →

    +
    + + +

    If pdfnative saved you time, a ⭐ on GitHub helps others find it. Thanks!

    + +
    +
    + + + + + + + + + + + + diff --git a/docs/guides/outlines.html b/docs/guides/outlines.html index 911858e..e86f3d3 100644 --- a/docs/guides/outlines.html +++ b/docs/guides/outlines.html @@ -1,85 +1,286 @@ - - - - - - Outlines & page labels — pdfnative - - - + + + + + + Outlines & page labels — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Outlines & page labels

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Outlines & page labels

    +
    + +

    Outlines & page labels

    +
    +

    New in v1.4.0. Give long documents a navigable bookmark tree +(/Outlines) and logical page numbering (/PageLabels) — roman-numbered +front matter, prefixed appendices, custom starting numbers. Both are opt-in +DocumentParams fields and add zero overhead when unused.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Annual Report',
    +  outline: 'auto',                       // bookmarks derived from headings
    +  pageLabels: [
    +    { startPage: 0, style: 'roman' },    // startPage is 0-based — i, ii, iii … (front matter)
    +    { startPage: 3, style: 'decimal' },  // 1, 2, 3 … (body, from the 4th page)
    +  ],
    +  blocks: [
    +    { type: 'heading', level: 1, text: 'Overview' },
    +    { type: 'paragraph', text: '…' },
    +    { type: 'heading', level: 2, text: 'Highlights' },
    +    { type: 'paragraph', text: '…' },
    +  ],
    +});
    +
    +

    Document outline (bookmarks)#

    +

    The outline appears in the viewer's sidebar / bookmarks panel and lets readers +jump straight to a section. Set DocumentParams.outline to either 'auto' or +an explicit nested tree.

    +

    Automatic outline from headings#

    +
    outline: 'auto'
    +
    +

    pdfnative walks your HeadingBlocks and builds a nested tree by heading level +(level: 1 → top level, level: 2 → child, …), each bookmark linking to the +page the heading lands on. This is the zero-effort option for structured +reports.

    +

    Explicit outline#

    +

    For full control over titles, nesting, ordering, styling, and destinations, +pass an array of OutlineItem:

    +
    import type { OutlineItem } from 'pdfnative';
    +
    +const outline: OutlineItem[] = [
    +  {
    +    title: 'Part I — Introduction',
    +    pageIndex: 0,       // 0-based page index
    +    bold: true,
    +    children: [
    +      { title: 'Background', pageIndex: 0 },
    +      { title: 'Scope',      pageIndex: 1 },
    +    ],
    +  },
    +  {
    +    title: 'Part II — Results',
    +    pageIndex: 2,
    +    color: [0.1, 0.3, 0.9], // RGB 0–1; also accepts '#1a4fd6'
    +    children: [
    +      { title: 'Findings', pageIndex: 2, italic: true },
    +    ],
    +  },
    +];
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OutlineItem fieldTypeDescription
    titlestringBookmark label (encoded as PDF text, UTF-16BE when needed)
    pageIndexnumber0-based page index to jump to
    ynumber?Optional vertical destination (PDF user units from the bottom); defaults to the top of the page
    boldboolean?Render the label bold (/F flag 2)
    italicboolean?Render the label italic (/F flag 1)
    colorPdfColor?Label colour (/C) — [r,g,b] 0–1 or a hex string
    openboolean?Initial expansion state. true (default) renders the bookmark expanded; false renders it collapsed (negative /Count), hiding its children until the reader expands it. Only meaningful with children.
    childrenOutlineItem[]?Nested bookmarks
    +

    Destinations use /XYZ with the page's top-left as the default anchor, so the +viewer scrolls the target page into view at 100 % zoom.

    +

    Collapsed bookmarks#

    +

    Deep hierarchies read better when some branches start collapsed. Set +open: false on any item with children:

    +
    const outline: OutlineItem[] = [
    +  { title: 'Front matter', pageIndex: 0 },
    +  {
    +    title: 'Appendices',
    +    pageIndex: 12,
    +    open: false,            // collapsed on open — children hidden until expanded
    +    children: [
    +      { title: 'Appendix A', pageIndex: 12 },
    +      { title: 'Appendix B', pageIndex: 18 },
    +    ],
    +  },
    +];
    +
    +

    pdfnative emits the spec-correct signed /Count (ISO 32000-1 §12.3.3): a +positive count for open items, a negative count for collapsed ones, and a +collapsed node contributes only itself — not its hidden descendants — to its +ancestors' visible counts.

    +

    Page labels#

    +

    By default a viewer numbers pages 1, 2, 3 …. /PageLabels overrides that with +logical numbering — front matter in lowercase roman, the body in decimal, +appendices with an A- prefix, and so on. The labels show in the viewer's page +thumbnail / "go to page" box and in printed page references.

    +
    import type { PageLabelRange } from 'pdfnative';
    +
    +const pageLabels: PageLabelRange[] = [
    +  { startPage: 0, style: 'roman' },                 // i, ii, iii
    +  { startPage: 3, style: 'decimal' },               // 1, 2, 3
    +  { startPage: 20, style: 'decimal', prefix: 'A-', start: 1 }, // A-1, A-2
    +];
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PageLabelRange fieldTypeDescription
    startPagenumber0-based page index where this range begins
    stylePageLabelStyle?'decimal' · 'roman' (i, ii) · 'Roman' (I, II) · 'alpha' (a, b) · 'Alpha' (A, B) · 'none' (label is the prefix only)
    prefixstring?Text prepended to each label (e.g. 'A-')
    startnumber?First number in the range (default 1)
    +

    Ranges must be ordered by startPage and stay within the document's page +count — both are validated at the boundary with a descriptive error.

    +

    How it works#

    +
      +
    • OutlinebuildOutlineObjects() +(src/core/pdf-outline.ts) +emits the /Outlines dictionary plus one indirect object per bookmark, wired +with /First /Last /Next /Prev /Parent /Count. The objects are appended as +trailing indirect objects and the catalog gains /Outlines N 0 R.
    • +
    • Page labelsbuildPageLabelsDict() +(src/core/pdf-page-labels.ts) +emits an inline /PageLabels << /Nums [...] >> number tree in the catalog, so +it adds no indirect objects.
    • +
    +

    Both features are fully additive: a document with neither field is byte-identical +to the pre-v1.4.0 output.

    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/pdf-manipulation.html b/docs/guides/pdf-manipulation.html index f6463a1..6804eaf 100644 --- a/docs/guides/pdf-manipulation.html +++ b/docs/guides/pdf-manipulation.html @@ -1,85 +1,345 @@ - - - - - - PDF manipulation — pdfnative - - - + + + + + + PDF manipulation — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  PDF manipulation

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  PDF manipulation

    +
    + +

    PDF manipulation (merge / split / extract)

    +
    +

    New in v1.4.0. Combine, slice, and reorder existing PDFs with a +production-safe page-tree API. Each operation rebuilds a clean object graph +rather than patching bytes in place — inherited page attributes are resolved, +dangling references are pruned, and the result is a fresh, well-formed PDF.

    +
    +

    TL;DR#

    +
    import { mergePdfs, splitPdf, extractPages } from 'pdfnative';
    +import { readFileSync, writeFileSync } from 'node:fs';
    +
    +const a = readFileSync('cover.pdf');
    +const b = readFileSync('body.pdf');
    +
    +// Merge
    +writeFileSync('combined.pdf', mergePdfs([a, b]));
    +
    +// Split into page ranges (0-based, end inclusive; end defaults to start)
    +const [intro, rest] = splitPdf(b, [
    +  { start: 0, end: 1 },   // pages 0–1
    +  { start: 2, end: 9 },   // pages 2–9
    +]);
    +
    +// Extract specific pages (0-based)
    +writeFileSync('selected.pdf', extractPages(b, [0, 3, 7]));
    +
    +

    All three accept and return Uint8Array PDF bytes.

    +
    +

    New in v1.6.0. Encrypted sources are now decrypted on ingest — pass a +password (see Encrypted sources) — the rebuilt output +can be re-encrypted with encrypt (see +Re-encrypting the output), and there are +constant-memory streaming variants (streamMergedPdfs / streamSplitPdf / +streamExtractPages, see Streaming merge & split).

    +
    +

    mergePdfs(sources, options?)#

    +

    Concatenates multiple PDFs into one, in order.

    +
    function mergePdfs(
    +  sources: readonly PdfSourceInput[],
    +  options?: MergeOptions,
    +): Uint8Array;
    +
    +// Raw bytes, or bytes + password for an encrypted source (v1.6.0):
    +type PdfSourceInput = Uint8Array | { bytes: Uint8Array; password?: string };
    +
    +interface MergeOptions {
    +  /** Strip digital-signature widgets/fields from the result. Default false. */
    +  dropSignatures?: boolean;
    +  /** Strip all annotations (links, comments, …) from the result. Default false. */
    +  dropAnnotations?: boolean;
    +  /** Password used to decrypt encrypted sources (default for every source). v1.6.0 */
    +  password?: string;
    +  /**
    +   * Maximum size, in bytes, of the assembled output. The operation throws as
    +   * soon as the copied object graph would exceed this limit — even mid-copy,
    +   * before an oversized stream is materialised — so a malicious or accidentally
    +   * huge source cannot exhaust process memory. Defaults to **256 MiB**; pass
    +   * `Infinity` to disable (not recommended for untrusted input).
    +   */
    +  maxOutputSize?: number;
    +}
    +
    +
      +
    • Up to 50 source documents per call (MAX_MERGE_SOURCES).
    • +
    • Output is hard-capped at 256 MiB by default (maxOutputSize) so a hostile +source cannot OOM the process; raise it for legitimately large merges.
    • +
    • Page resources (/Font, /XObject, …) are deep-copied into a fresh +object-number space, so there are no collisions between sources.
    • +
    • Inherited attributes (/MediaBox, /CropBox, /Rotate, /Resources) are +resolved from each page's ancestors and folded onto the page, so pages keep +their geometry even when the original relied on inheritance.
    • +
    +
    +

    Merging a signed PDF invalidates its signature (the bytes change). Pass +dropSignatures: true to remove the now-meaningless signature fields.

    +
    +

    splitPdf(source, ranges)#

    +

    Splits one PDF into several, one output per range.

    +
    function splitPdf(
    +  source: Uint8Array,
    +  ranges: readonly PageRange[],
    +  options?: MergeOptions,
    +): Uint8Array[];
    +
    +interface PageRange {
    +  /** 0-based first page (inclusive). */
    +  start: number;
    +  /** 0-based last page (inclusive). Defaults to `start` (single page). */
    +  end?: number;
    +}
    +
    +

    Ranges may overlap and need not be contiguous. Each output is an independent, +fully-formed PDF. options (including maxOutputSize) applies to every emitted +document.

    +

    extractPages(source, indices)#

    +

    Builds a new PDF from an explicit list of 0-based page indices, in the order +given — handy for reordering or cherry-picking.

    +
    function extractPages(
    +  source: Uint8Array,
    +  indices: readonly number[],
    +  options?: MergeOptions,
    +): Uint8Array;
    +
    +extractPages(pdf, [4, 0, 1]); // page 5 first, then 1, then 2
    +
    +

    options (including maxOutputSize and dropAnnotations) is honoured here too.

    +

    Encrypted sources#

    +

    Since v1.6.0, mergePdfs / splitPdf / extractPages decrypt encrypted +sources transparently (Standard Security Handler — RC4, AES-128, AES-256). Give +the password either per-source or as a shared default:

    +
    // Per-source password (only that source is encrypted):
    +mergePdfs([cover, { bytes: encryptedBody, password: 'secret' }]);
    +
    +// Shared password for every source, via options:
    +mergePdfs([a, b], { password: 'secret' });
    +
    +// splitPdf / extractPages take the password on options:
    +splitPdf(encrypted, [{ start: 0, end: 2 }], { password: 'secret' });
    +
    +

    A wrong or missing password throws PdfPasswordError; an unsupported handler +(e.g. public-key) throws PdfEncryptionUnsupportedError. The rebuilt output is +unencrypted unless you set encrypt (below). See the +reader guide for openPdf(bytes, { password }).

    +

    Re-encrypting the output#

    +

    Since v1.6.0, MergeOptions.encrypt re-encrypts the rebuilt document — +closing the round trip: open encrypted → edit → re-secure. It takes the same +shape as the document builder's encryption option:

    +
    import { mergePdfs, splitPdf } from 'pdfnative';
    +
    +// Merge, then protect the result (AES-256):
    +const secured = mergePdfs([a, b], {
    +  encrypt: {
    +    ownerPassword: 'owner-secret',      // required, non-empty
    +    userPassword: 'user-secret',        // optional (empty = opens freely)
    +    algorithm: 'aes256',                // 'aes128' (V4/R4, default) | 'aes256' (V5/R6)
    +    permissions: { print: true, copy: false, modify: false },
    +  },
    +});
    +
    +// Change a document's password: decrypt on ingest, re-encrypt on output.
    +const rekeyed = mergePdfs(
    +  [{ bytes: oldPdf, password: 'old-password' }],
    +  { encrypt: { ownerPassword: 'new-password', algorithm: 'aes256' } },
    +);
    +
    +// Works identically on splitPdf / extractPages and the streaming variants.
    +splitPdf(src, [{ start: 0, end: 4 }], { encrypt: { ownerPassword: 'o' } });
    +
    +

    Notes:

    +
      +
    • AES only — new output is never RC4-encrypted (legacy RC4 is read-only).
    • +
    • CSPRNG required — a missing Web Crypto secure random source throws +before any copying starts (encryption keys are never derived from +Math.random()).
    • +
    • Fresh keys — no key material, permissions, or passwords from any source +document are reused; the caller states the new protection explicitly.
    • +
    • Non-deterministic output — random IVs/salts and a random document /ID +mean encrypted output is not byte-reproducible (the unencrypted path keeps +its content-addressed deterministic /ID).
    • +
    +

    Streaming merge & split#

    +

    For large documents, the streaming variants emit the result as fixed-size +chunks while holding only the cross-reference offsets and small object dicts in +memory — stream payloads flow straight from the (in-memory) source bytes, so the +fully-joined document is never materialised. Each is byte-identical to its +buffered counterpart (except with encrypt, where fresh random IVs make each +invocation structurally — not byte — identical) and composes with +streamToFile:

    +
    import { streamMergedPdfs, streamSplitPdf, streamToFile } from 'pdfnative';
    +
    +// Constant-memory merge straight to disk:
    +await streamToFile(streamMergedPdfs([a, b]), 'combined.pdf');
    +
    +// Split: one output stream per range (drain each fully before advancing):
    +for await (const part of streamSplitPdf(body, [{ start: 0, end: 1 }, { start: 2, end: 9 }])) {
    +  await streamToFile(part.pdf, `part-${part.index}.pdf`);
    +}
    +
    +

    StreamMergeOptions adds chunkSize (1 KiB–16 MiB, default 64 KiB) on top of +MergeOptions. For multi-gigabyte merges pass maxOutputSize: Infinity — safe +with streaming because output bytes are never buffered (the sources themselves +are still in-memory Uint8Arrays).

    +

    For freshly built (not merged) documents, combine the true streaming builders +with streamToFile so the binary never fully materialises:

    +
    import { buildDocumentPDFStreamTrue, streamToFile } from 'pdfnative';
    +
    +await streamToFile(buildDocumentPDFStreamTrue(params), 'report.pdf');
    +
    +

    Updating metadata in place#

    +
    +

    New in v1.7.0. PdfModifier.updateMetadata() rewrites a document's +metadata as a non-destructive incremental revision — no re-serialisation, +existing signatures over earlier revisions stay intact.

    +
    +
    import { openPdf, createModifier } from 'pdfnative';
    +
    +const modifier = createModifier(openPdf(bytes));
    +modifier.updateMetadata({
    +  title: 'Q3 report (final)',
    +  author: 'Finance',
    +  keywords: 'quarterly, revenue',
    +  // modDate: new Date('2026-08-21T00:00:00Z'),  // pin for reproducible bytes
    +});
    +const updated = modifier.save();
    +
    +

    Only the fields you pass change; the rest of /Info is preserved. /ModDate +is always refreshed (pass modDate to pin it). When the document carries an +XMP packet, it is resynchronised in the same revision — dc:title, +dc:creator, dc:description, pdf:Keywords, xmp:ModifyDate and +xmp:MetadataDate — while xmp:CreateDate and any pdfaid:* conformance +claim are preserved, so Info↔XMP parity holds for PDF/A documents.

    +

    Safety & limits#

    +
      +
    • Encrypted input is decrypted on ingest (v1.6.0) when a valid password is +supplied; a wrong/missing password throws PdfPasswordError. Output is +unencrypted unless encrypt re-protects it (AES-128/AES-256, CSPRNG +required, fresh keys only).
    • +
    • Annotations are filtered to URI /Link only during the rebuild (plus the +full strip when dropAnnotations is set), so interactive form/JS annotations +don't leak across documents.
    • +
    • Bounded-depth copy. The object-graph copy is capped at a fixed recursion +depth, so a pathologically nested or adversarial source can never overflow the +stack — it throws a descriptive error instead.
    • +
    • Bounded output size. Cumulative output is capped at 256 MiB by default +(maxOutputSize), checked before each stream is materialised, so a source +full of multi-gigabyte objects is rejected rather than allowed to exhaust +memory. Tune or disable (Infinity) per call.
    • +
    • Deterministic output. Every unencrypted result carries a +content-addressed trailer /ID (ISO 32000-1 §7.5.5) derived from the +assembled bytes, so the same inputs always produce a byte-identical PDF — +friendly to caching, diffing, and reproducible builds. (With encrypt, the +/ID is the encryption state's random document ID, as the file key is bound +to it.)
    • +
    • Full rebuild, not in-place surgery. The clean-graph approach trades a +little speed for correctness and is safe to run on third-party PDFs.
    • +
    • Page boxes survive (v1.7.0). Declared /TrimBox, /BleedBox, +/ArtBox, /CropBox and /UserUnit entries are preserved through +mergePdfs / splitPdf / extractPages (they were dropped by the rebuild +before 1.7.0) — so print-ready sources keep their geometry. See the +print production guide.
    • +
    +

    How it works#

    +

    src/parser/pdf-pagetree.ts +opens each source with the built-in PDF reader, walks the +page tree, and deep-copies every kept page plus its transitive object graph into +a new document (obj 1 = Catalog, obj 2 = Pages root, obj 3+ = the copied +graph). The copy is memoised per reader and cycle-safe, and all values +are serialised binary-safe (Latin-1) so embedded fonts and image streams survive +intact.

    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/pdfa.html b/docs/guides/pdfa.html index ae94a14..a12642c 100644 --- a/docs/guides/pdfa.html +++ b/docs/guides/pdfa.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,381 @@

    Home  ›  Guides  ›  PDF/A

    -
    -

    Loading…

    - +
    + +

    PDF/A conformance in pdfnative

    +

    PDF/A is the ISO archival profile for PDF (ISO 19005). pdfnative supports +PDF/A-1b, 2b, 2u, and 3b via the tagged build option. This guide +explains what works today, what's still in flight, and how to validate +your output against the official reference validator.

    +

    TL;DR#

    +
    import { buildPDFBytes } from 'pdfnative';
    +
    +const pdf = buildPDFBytes(params, { tagged: true });        // PDF/A-2b (default)
    +const pdf1b = buildPDFBytes(params, { tagged: 'pdfa1b' });  // PDF/A-1b
    +const pdf2u = buildPDFBytes(params, { tagged: 'pdfa2u' });  // PDF/A-2u
    +const pdf3b = buildPDFBytes(params, { tagged: 'pdfa3b' });  // PDF/A-3b + attachments
    +
    +
    +

    tagged alone is not enough: embed a font#

    +

    ISO 19005 requires every font in the file to be embedded. tagged writes the +XMP conformance declaration, the structure tree and the output intent — but it does +not embed a font for you. If your document uses only the viewer's built-in fonts +(which is what happens when you pass no fontEntries), pdfnative writes a file that +claims PDF/A while referencing non-embedded Helvetica, and veraPDF will reject it.

    +

    Pass at least a Latin font whenever you set tagged:

    +
    import { buildDocumentPDFBytes, registerFonts, loadFontData } from 'pdfnative';
    +
    +registerFonts({ latin: () => import('pdfnative/fonts/noto-sans-data.js') });
    +const fontData = await loadFontData('latin');
    +
    +// fontRef becomes a PDF resource name, so it needs the leading slash.
    +// /F1 and /F2 are reserved by the engine — start at /F3.
    +
    +const pdf = buildDocumentPDFBytes(
    +  { title: 'Archival', blocks, fontEntries: [{ fontData, fontRef: '/F3', lang: 'latin' }] },
    +  { tagged: 'pdfa2b' },
    +);
    +
    +

    Since v1.7.0 the builders guard this declaration themselves: requesting any +'pdfa*' level (or tagged: true) with no fontEntries emits the +PDFA_NO_FONT_ENTRIES diagnostic — a console.warn by default, or a thrown +Error under strict: true, before any output bytes are produced. See +Conformance diagnostics below.

    +

    The sample generators in scripts/generators/ all do this, which is why the veraPDF +CI job passes. If you author through pdfnative-react, its lintDocument() rule +L_TAGGED_NO_FONTS still catches the same trap earlier, at the document-model +level — a complementary check to the core diagnostic.

    +
    +

    Every output written with tagged set ships:

    +
      +
    • A full structure tree (/Document → /Table → /TR → /TH|/TD, /H1–H3, +/P, /L → /LI, /Figure, /Link).
    • +
    • /ActualText UTF-16BE on every marked content /Span.
    • +
    • An XMP metadata stream with pdfaid:part and pdfaid:conformance.
    • +
    • An sRGB ICC OutputIntent (GTS_PDFA1).
    • +
    • /MarkInfo << /Marked true >> on the catalog.
    • +
    • A trailer /ID derived deterministically from the document title and +creation timestamp.
    • +
    • /Info CreationDate byte-equivalent to xmp:CreateDate, both with +timezone offsets.
    • +
    +

    Conformance diagnostics (v1.7.0)#

    +

    Configurations that produce a PDF/A claim veraPDF would reject no longer fail +silently. Both builders (buildPDFBytes and buildDocumentPDFBytes) surface +them through a single diagnostics channel:

    + + + + + + + + + + + + + + + + + + + +
    CodeTrigger
    PDFA_NO_FONT_ENTRIESA 'pdfa*' level (or tagged: true) requested with no fontEntries — the file would claim PDF/A while referencing unembedded standard-14 Helvetica (ISO 19005 §6.2.11.4.1).
    PDFA_DEVICE_CMYK_IMAGEA DeviceCMYK image embedded under a PDF/A claim with an sRGB OutputIntent (ISO 19005-2 §6.2.4.3).
    PDFA_UNEMBEDDED_FORM_FONTAcroForm fields under a PDF/A claim — form appearances render through an unembedded base-14 /Helv font (same §6.2.11.4.1 rule). Flatten the form or drop the level.
    +

    By default each diagnostic is a console.warn, deduplicated once per code +per build. Two layout options change that:

    +
    import { buildPDFBytes, type PdfDiagnostic } from 'pdfnative';
    +
    +// CI / tests: escalate to a thrown Error, before any bytes are produced.
    +buildPDFBytes(params, { tagged: 'pdfa2b', strict: true });
    +
    +// Custom sink — receives every diagnostic (no deduplication).
    +// Pass () => {} to silence entirely.
    +const diagnostics: PdfDiagnostic[] = [];
    +const pdf = buildPDFBytes(params, {
    +  tagged: 'pdfa2b',
    +  onDiagnostic: (d) => diagnostics.push(d),
    +});
    +
    +

    Each PdfDiagnostic carries a machine-readable code, a severity +('warning'), and an actionable message that includes the remedy. +onDiagnostic is ignored when strict is set — diagnostics throw instead. +The code list is a stable, additions-only union (PdfDiagnosticCode), so a +sink written today keeps compiling as future codes are added.

    +
    +

    On the MCP surface (pdfnative-mcp 1.6.0), the same honesty is exposed +as three opt-in inputs on every document tool: embedFonts: true embeds +Noto Sans Latin so a PDF/A claim on base-14 text is actually accepted by +veraPDF, strict: true fails instead of producing a non-conformant file, +and includeDiagnostics: true echoes the engine's diagnostics +(PDFA_NO_FONT_ENTRIES, PDFA_UNEMBEDDED_FORM_FONT, +PDFA_DEVICE_CMYK_IMAGE) in the tool result. Known engine limitation: +add_form (or a formField block) under a PDF/A claim still fails +veraPDF even with embedFonts — the AcroForm /DR /Helv font is an +unembedded Type1 (ISO 19005-2 §6.2.11.4.1).

    +
    +

    v1.1.0 status — fully validated#

    +

    v1.1.0 ships full PDF/A-1b / 2b / 2u / 3b conformance against the +official veraPDF reference validator. The validator runs as a +blocking check on every PR (see +.github/workflows/verapdf.yml).

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RuleStatusFixed in
    ISO 19005-1 §6.1.3 — trailer /ID always presentv1.0.4
    veraPDF 6.7.3 t1 — CreationDatexmp:CreateDate parityv1.0.4
    veraPDF 6.7.3 t1 — dc:title/Info /Title parityv1.1.0
    veraPDF 6.7.3 t4 — dc:description/Info /Subject parityv1.1.0
    veraPDF 6.7.3 t5 — pdf:Keywords/Info /Keywords parityv1.1.0
    veraPDF 6.7.3 — dc:creator/Info /Author parityv1.0.4
    ISO 19005-1 §6.3.4 — Latin font embeddingv1.1.0
    ISO 19005-2 §6.2.11.4.1 — Type0 font referencesv1.1.0
    veraPDF 6.2.3.3 — DeviceRGB OutputIntentv1.0.4
    +

    To produce a strictly veraPDF-compliant PDF/A document, register the Latin +font module and resolve it. Registering alone is not enough: the builders +are synchronous and cannot await a loader, so the font never reaches the +document and you get an unembedded Helvetica reference — verified: 0 embedded +fonts.

    +
    import { registerFonts, loadFontData, buildPDFBytes } from 'pdfnative';
    +
    +registerFonts({ latin: () => import('pdfnative/fonts/noto-sans-data.js') });
    +const fontData = await loadFontData('latin');
    +
    +const pdf = buildPDFBytes(
    +  { ...params, fontEntries: [{ fontData, fontRef: '/F3', lang: 'latin' }] },
    +  { tagged: true },
    +);
    +
    +

    Without the 'latin' font registered, pdfnative falls back to the +unembedded Helvetica standard-14 references for byte-stable v1.0.x +output — convenient for non-archival rendering but invalid under +PDF/A.

    +

    Validating your output#

    +

    pdfnative ships a thin wrapper around the official veraPDF CLI:

    +
    # 1. Generate the sample suite (writes test-output/)
    +npm run test:generate
    +
    +# 2. Run veraPDF against every PDF/A-claiming sample
    +npm run validate:pdfa
    +
    +

    The script auto-detects veraPDF on $PATH or via the VERAPDF_HOME +env var. If veraPDF is not installed it exits 0 with install +instructions — local development never blocks. CI installs veraPDF +deterministically (pinned to 1.30.2) and runs the same script on every +engine PR — see .github/workflows/verapdf.yml — +and again as a blocking gate before every npm publish.

    +

    Detection is automatic and guarded: every sample declaring +pdfaid:part — currently the 18 PDF/A-claiming samples — is validated +without any registration, and a coverage canary fails the run if the +detected count drifts from declared.pdfaSamples in +docs/assets/ecosystem.json (bump it when adding or removing a +claiming sample; a mismatch with no sample change means detection or +generation regressed).

    +
    +

    Which samples are subject to PDF/A validation? Only files that +declare a conformance level in their XMP (pdfaid:part) — the ones +built with tagged: 'pdfa…', e.g. the pdfa/ and pdfa-latin/ sample +categories. The script skips everything else. Most showcase samples +(bidi/, alphabet/, charts/, …) intentionally make no PDF/A +claim: they have no XMP packet at all, so force-validating one against a +PDF/A profile in the veraPDF GUI fails by construction (missing +/Metadata, uncalibrated DeviceRGB, …) — that is expected and not a +defect.

    +
    +

    veraPDF is invoked as an external Java tool. pdfnative remains a +zero-runtime-dependency library; veraPDF is never bundled, linked, or +required by consumers of the npm package.

    +

    Installing veraPDF locally#

    +

    veraPDF is a Java application. Pick whichever path matches your OS. +After install, either expose verapdf on $PATH or set the +VERAPDF_HOME environment variable to the install directory.

    +

    macOS — Homebrew cask:

    +
    brew install --cask verapdf
    +verapdf --version
    +
    +

    Linux — official installer (headless):

    +
    curl -fsSL -o verapdf-installer.zip https://software.verapdf.org/rel/verapdf-installer.zip
    +unzip verapdf-installer.zip
    +java -jar verapdf-izpack-installer-*.jar -console
    +# follow the prompts; defaults are sane
    +export VERAPDF_HOME="$HOME/verapdf"
    +export PATH="$VERAPDF_HOME:$PATH"
    +
    +

    Windows — official GUI installer from +https://docs.verapdf.org/install/, or the same headless install CI +uses (download the pinned zip, run +java -jar verapdf-izpack-installer-*.jar auto-install.xml — the +descriptor is shown in CONTRIBUTING). +After install, either add the install directory to PATH or:

    +
    $env:VERAPDF_HOME = "C:\Program Files\verapdf"
    +$env:Path += ";$env:VERAPDF_HOME"
    +verapdf.bat --version
    +
    +

    The .bat launcher is fully supported by npm run validate:pdfa +since v1.7.0 (it is spawned through a shell, as recent Node versions +require for batch files).

    +

    No install at all? Drop the file into the official online demo +at https://demo.verapdf.org. It validates against the same engine, +which is convenient for ad-hoc checks but does not scale to a CI +suite.

    +

    Troubleshooting#

    +

    "My PDF fails veraPDF for missing XMP / DeviceRGB / unembedded +font, but I never asked for PDF/A."

    +

    If the file was generated without tagged: true, it is a plain +ISO 32000-1 document and should not be validated against any PDF/A +profile. The veraPDF online demo lets you pick a profile manually, +which will then surface failures by design — every PDF/A rule about +metadata, output intents, font embedding, transparency, and color +spaces will fire because the file never claimed any of those things.

    +

    The npm run validate:pdfa wrapper avoids this trap: it scans each +PDF for a pdfaid:part declaration in the XMP packet and skips +files that don't claim PDF/A. The summary line reports how many were +skipped:

    +
    Scanned 242 PDF(s); 18 claim PDF/A, 224 skipped (not PDF/A).
    +
    +

    If you want a file to be validated, generate it with tagged: true +(or any 'pdfa*' value).

    +

    "My tagged file still fails rule 6.3.4 (font embedding)."

    +

    Register the Latin font module and pass the resolved data as fontEntries +(see the TL;DR above). Registration on its own does nothing for the synchronous +builders. Without it, pdfnative emits Helvetica as an unembedded standard-14 +reference for byte-stable v1.0.x output. With it, every glyph used +in the document is embedded as CIDFontType2 / FontFile2 — see +the v1.1.0 status table above. Since v1.7.0 this configuration also emits the +PDFA_NO_FONT_ENTRIES diagnostic at build time (a thrown error under +strict: true), so the failure surfaces before veraPDF ever runs.

    +

    Output bytes change in v1.0.4#

    +

    v1.0.4 has no public API break, but PDF outputs differ byte-for-byte +from v1.0.3:

    +
      +
    • Trailer /ID array is now always present.
    • +
    • /Info CreationDate and xmp:CreateDate carry timezone offsets.
    • +
    • dc:creator is emitted only when an author is provided.
    • +
    +

    If your test fixtures snapshot full PDF bytes, regenerate them.

    +

    PDF/A vs encryption#

    +

    ISO 19005-1 §6.3.2 forbids combining PDF/A with PDF encryption. +pdfnative validates this at the build boundary — passing both +tagged: … and encryption: … in the same call throws.

    +

    Choosing a flavour#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlavourBase PDFNotes
    PDF/A-1b ('pdfa1b')PDF 1.4Most conservative — required by some legacy archival systems. No transparency, no JPEG2000, no AES.
    PDF/A-2b (true / 'pdfa2b')PDF 1.7Default. Allows transparency, layers, embedded TrueType.
    PDF/A-2u ('pdfa2u')PDF 1.72b + Unicode mapping for every glyph. Required when /ActualText and ToUnicode CMap completeness matter (recommended for accessibility).
    PDF/A-3b ('pdfa3b')PDF 1.72b + arbitrary /EmbeddedFile attachments (XML, source data, etc.).
    +

    All four flavours share the same XMP / OutputIntent / structure-tree +infrastructure — pdfnative only varies the PDF version, the +pdfaid:part, and the pdfaid:conformance value.

    +

    Canonical list for tooling#

    +

    The four legal tagged strings are also exposed as a +typed constant for tooling that needs to populate a JSON-schema +enum: (pdfnative-mcp does this for its MCP tool descriptions so +Gemini-CLI and other agents can autocomplete the right value):

    +
    import { PDF_A_CONFORMANCE_TARGETS, type PdfAConformanceTarget } from 'pdfnative';
    +
    +PDF_A_CONFORMANCE_TARGETS;
    +// → readonly ['pdfa1b', 'pdfa2b', 'pdfa2u', 'pdfa3b']
    +
    +function isValidTarget(s: string): s is PdfAConformanceTarget {
    +  return (PDF_A_CONFORMANCE_TARGETS as readonly string[]).includes(s);
    +}
    +
    +

    The constant is the single source of truth — adding a new target in a +future minor release will surface automatically in every downstream +consumer that imports it.

    +

    Hard invariants for contributors#

    +

    These rules are documented in the contributor instruction file +.github/instructions/pdfa-conformance.instructions.md:

    +
      +
    • /Info CreationDate and xmp:CreateDate come from the same +buildPdfMetadata() call. Never inline new Date() in the builders.
    • +
    • The unencrypted trailer /ID is derived deterministically from +MD5(title + creationDate + objectCount). Never randomize — it +breaks buildPDFBytes(params) byte-equality tests.
    • +
    • dc:creator is emitted only when an author is provided and is +XML-escaped.
    • +
    • XMP metadata streams are never compressed.
    • +
    • Compression always happens before encryption (ISO 32000-1 +§7.3.8).
    • +
    +

    See also#

    + +
    diff --git a/docs/guides/print.html b/docs/guides/print.html index d326a3d..8502ca0 100644 --- a/docs/guides/print.html +++ b/docs/guides/print.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,148 @@

    Home  ›  Guides  ›  Print production

    -
    -

    Loading…

    - +
    + +

    Print production — bleed, trim & printer's marks

    +
    +

    New in v1.7.0. Print-ready PDFs with zero dependencies: page geometry boxes (/TrimBox, /BleedBox, /ArtBox, /CropBox), crop and registration marks drawn as pure vector operators, /Trapped metadata with XMP parity, print-dialog defaults (duplex, tray, page range, copies), a caller-supplied OutputIntent ICC profile, and large-format /UserUnit. Everything is opt-in — output is byte-identical when unused.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes, PAGE_SIZES } from 'pdfnative';
    +
    +const BLEED = 8.5; // 3 mm in points
    +
    +const pdf = buildDocumentPDFBytes(params, {
    +  // Design the page at trim size + bleed on every side…
    +  pageWidth:  PAGE_SIZES.A4.width  + 2 * BLEED,
    +  pageHeight: PAGE_SIZES.A4.height + 2 * BLEED,
    +  margins: { t: 36 + BLEED, r: 36 + BLEED, b: 36 + BLEED, l: 36 + BLEED },
    +  // …declare the geometry and draw the marks:
    +  print: { bleed: BLEED, marks: true },
    +  viewerPreferences: { duplex: 'duplexFlipLongEdge', pickTrayByPDFSize: true },
    +});
    +
    +

    Open the result in Acrobat with Preferences → Page Display → Show art, trim & bleed boxes to see the geometry.

    +

    The box model (ISO 32000-1 §14.11.2)#

    +
    ┌─────────────────────────────┐  MediaBox — the physical page (sheet)
    +│  ┌───────────────────────┐  │  BleedBox — content clipped in production
    +│  │ ┌───────────────────┐ │  │  TrimBox — the finished page after cutting
    +│  │ │      ArtBox       │ │  │  ArtBox — meaningful-content extent
    +│  │ └───────────────────┘ │  │
    +│  │  backgrounds run here │  │  ← bleed zone: extend backgrounds into it
    +│  └───────────────────────┘  │
    +└─────────────────────────────┘
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptionPDF keyMeaning
    print.bleedderives bothShorthand: TrimBox = MediaBox inset by the bleed, BleedBox = MediaBox
    print.trimBox/TrimBoxFinished page size after cutting
    print.bleedBox/BleedBoxClipping extent in production
    print.artBox/ArtBoxMeaningful-content extent
    print.cropBox/CropBoxRegion viewers display/print
    +

    Boxes are validated (within the MediaBox, trim within bleed) and are pure page-dictionary metadata — the layout engine is untouched, so design the page at trim + 2×bleed and enlarge the margins by the bleed, letting backgrounds run to the page edge.

    +

    mergePdfs / splitPdf / extractPages preserve all four boxes (and /UserUnit).

    +

    Printer's marks (§14.11.3)#

    +

    print.marks: true draws, on every page, strictly outside the TrimBox:

    +
      +
    • Crop marks — 8 corner hairlines (default 0.25 pt, 14 pt long, 5 pt clear of the trim edge) showing where to cut.
    • +
    • Registration targets — circle-and-cross targets on the four edge midpoints, used to align separations.
    • +
    +

    Fine-tune with an object: marks: { crop, registration, length, offset, weight }.

    +
    +

    Marks are stroked in RGB black. A true all-separation registration colour requires CMYK content support, which is on the roadmap together with PDF/X — see below.

    +
    +

    /Trapped and prepress metadata#

    +
    buildDocumentPDFBytes({ ...params, metadata: { trapped: 'False' } }, { tagged: 'pdfa2b' });
    +
    +

    Writes /Info /Trapped /False and mirrors it as pdf:Trapped in the XMP packet, telling the RIP whether trapping has been applied. Because pdf:Trapped is not part of the XMP-2005 Adobe PDF schema that PDF/A pins, pdfnative also emits the required PDF/A extension schema declaring the property (ISO 19005 §6.6.2.3.2) — the document stays veraPDF-compliant. Per ISO 32000-1 Table 317, trapped: 'Unknown' is written to /Info only: unknown maps to the absence of pdf:Trapped in XMP.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptionPDF keyValues
    duplex/Duplex'simplex', 'duplexFlipShortEdge', 'duplexFlipLongEdge'
    pickTrayByPDFSize/PickTrayByPDFSizeboolean (Windows viewers)
    printPageRange/PrintPageRange1-based [first, last] pairs, e.g. [[1, 4], [7, 7]]
    numCopies/NumCopiespositive integer
    +

    These join the existing v1.4.0 viewer preferences and remain PDF/A-safe metadata.

    +

    Custom OutputIntent (tagged/PDF-A)#

    +

    Replace the built-in minimal sRGB profile with a real ICC profile:

    +
    import { readFileSync } from 'node:fs';
    +
    +buildDocumentPDFBytes(params, {
    +  tagged: 'pdfa2b',
    +  outputIntent: {
    +    iccProfile: new Uint8Array(readFileSync('sRGB-IEC61966-2.1.icc')),
    +    outputConditionIdentifier: 'sRGB IEC61966-2.1',
    +    outputCondition: 'sRGB display',
    +    info: 'IEC 61966-2.1 reference profile',
    +  },
    +});
    +
    +

    RGB profiles only — pdfnative emits RGB content, and a mismatched intent fails PDF/A validation (a CMYK profile throws with an actionable message). Omitted, the historical built-in profile is used byte-identically.

    +

    Large formats — /UserUnit#

    +

    PDF user space caps pages at 14 400 units (200 in). For banners and plans, print.userUnit scales the unit (1 unit = userUnit/72 inch, up to 75 000):

    +
    // A 5 m × 1 m banner: 1417 × 283 units at 10/72 inch per unit.
    +buildDocumentPDFBytes(params, { pageWidth: 1417, pageHeight: 283, print: { userUnit: 10 } });
    +
    +

    /UserUnit needs PDF 1.6+, so the header is raised to %PDF-1.7 when the option is set — and it is rejected under tagged: 'pdfa1b' (PDF/A-1 is PDF 1.4; use pdfa2b or later).

    +

    Limits & scope (v1.7.0)#

    +
      +
    • Marks are RGB black; no CMYK content, spot colours, colour bars or PDF/X conformance claims yet — all on the roadmap as one coherent CMYK workstream.
    • +
    • One geometry per document (pages share the same boxes), matching the single-page-size layout model.
    • +
    • The OutputIntent (custom or built-in) is emitted under tagged modes only.
    • +
    +

    See the print samples: print/print-bleed-marks.pdf, print/print-explicit-boxes.pdf, print/print-large-format.pdf.

    +
    diff --git a/docs/guides/quickstart.html b/docs/guides/quickstart.html index d8d9b14..ef8dba1 100644 --- a/docs/guides/quickstart.html +++ b/docs/guides/quickstart.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,188 @@

    Home  ›  Guides  ›  Quick Start

    -
    -

    Loading…

    - +
    + +

    Quick Start

    +

    Generate your first PDF in under a minute.

    +

    Install#

    +
    npm install pdfnative
    +
    +
    +

    Requirements: Node.js ≥ 22 · modern browsers · Deno · Bun. +Zero runtime dependencies are installed.

    +
    +

    Node.js#

    +
    import { writeFileSync } from 'node:fs';
    +import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const pdf = buildDocumentPDFBytes({
    +  title: 'Hello',
    +  blocks: [
    +    { type: 'heading',   text: 'Hello, pdfnative', level: 1 },
    +    { type: 'paragraph', text: 'Pure native PDF — zero dependencies, ISO 32000-1.' },
    +  ],
    +});
    +
    +writeFileSync('hello.pdf', pdf);
    +
    +

    For optimal compression on Node.js, enable native zlib once at startup:

    +
    import { initNodeCompression, buildDocumentPDFBytes } from 'pdfnative';
    +
    +await initNodeCompression();
    +
    +const pdf = buildDocumentPDFBytes(params, { compress: true });
    +
    +

    Browser#

    +
    <script type="module">
    +  import { buildDocumentPDFBytes, downloadBlob } from 'https://esm.sh/pdfnative@1.7.0';
    +
    +  document.getElementById('go').addEventListener('click', () => {
    +    const pdf = buildDocumentPDFBytes({
    +      title: 'Hello',
    +      blocks: [
    +        { type: 'heading', text: 'Hello from the browser', level: 1 },
    +      ],
    +    });
    +    downloadBlob(pdf, 'hello.pdf');
    +  });
    +</script>
    +
    +

    A stored-block compression fallback works automatically — no native zlib needed in the browser.

    +

    Tabular reports — buildPDFBytes#

    +

    For bank statements, invoices, and any single-table report:

    +
    import { buildPDFBytes } from 'pdfnative';
    +
    +const pdf = buildPDFBytes({
    +  title: 'Monthly Report',
    +  infoItems: [
    +    { label: 'Period',  value: 'January 2026' },
    +    { label: 'Account', value: 'Main' },
    +  ],
    +  balanceText: 'Balance: $1,234.56',
    +  countText: '42 transactions',
    +  headers: ['Date', 'Description', 'Amount'],
    +  rows: [
    +    { cells: ['01/15', 'Grocery', '-$45.00'],   type: 'debit',  pointed: false },
    +    { cells: ['01/16', 'Salary',  '+$3,000.00'], type: 'credit', pointed: true  },
    +  ],
    +  footerText: 'Generated by MyApp',
    +});
    +
    +

    Free-form documents — buildDocumentPDFBytes#

    +

    For mixed content (manuals, articles, multi-section reports):

    +
    const pdf = buildDocumentPDFBytes({
    +  title: 'Project Report',
    +  blocks: [
    +    { type: 'toc' },
    +    { type: 'heading',   text: 'Executive Summary', level: 1 },
    +    { type: 'paragraph', text: 'This quarter saw strong growth.' },
    +    { type: 'list', style: 'bullet', items: ['Revenue +15%', 'Costs −8%'] },
    +    { type: 'image', data: chartBytes, width: 400, alt: 'Revenue chart' },
    +    { type: 'barcode', format: 'qr', data: 'https://example.com', width: 80 },
    +    { type: 'pageBreak' },
    +    { type: 'heading',   text: 'Details', level: 1 },
    +    { type: 'table', headers: ['Q', 'Revenue'], rows: [
    +      { cells: ['Q1', '$1.2M'], type: '', pointed: false },
    +      { cells: ['Q2', '$1.4M'], type: '', pointed: false },
    +    ] },
    +  ],
    +  footerText: 'Confidential',
    +});
    +
    +

    13 block types are available: heading, paragraph, list, table, image, link, spacer, pageBreak, toc, barcode, svg, formField, chart.

    +

    Multi-language#

    +
    import { registerFonts, loadFontData, buildDocumentPDFBytes } from 'pdfnative';
    +
    +registerFonts({
    +  th: () => import('pdfnative/fonts/noto-thai-data.js'),
    +  ar: () => import('pdfnative/fonts/noto-arabic-data.js'),
    +});
    +
    +const fontEntries = await Promise.all([
    +  loadFontData('th').then(fd => ({ fontData: fd!, fontRef: '/F3', lang: 'th' })),
    +  loadFontData('ar').then(fd => ({ fontData: fd!, fontRef: '/F4', lang: 'ar' })),
    +]);
    +
    +const pdf = buildDocumentPDFBytes({
    +  blocks: [{ type: 'paragraph', text: 'สวัสดี — مرحبا' }],
    +  fontEntries,
    +});
    +
    +

    Note that lang is a property of each font entry, not of a block: pdfnative detects the script per character and routes to the matching font, so a single paragraph can mix Thai, Arabic and Latin. BiDi resolution and OpenType shaping follow from that routing. See the FAQ → Fonts and Unicode for the full list of codes.

    +

    Web Worker#

    +

    createPDF is the recommended entry point — it decides between the main thread and a worker for you:

    +
    import { createPDF } from 'pdfnative';
    +
    +const pdf = await createPDF(params, {
    +  workerUrl: new URL('./pdf-worker.js', import.meta.url), // your worker script
    +  threshold: 500,   // rows above this go to the worker (default WORKER_THRESHOLD = 500)
    +  onProgress: (p) => console.log(`${p}%`),
    +});
    +
    +

    At or below the threshold (or when Worker / workerUrl is unavailable) the PDF is generated on the main thread; above it, the worker at workerUrl is spawned, with an automatic main-thread fallback if the worker fails.

    +

    To drive a worker directly, use generatePDFInWorker(workerUrl, params, { timeout, onProgress }) — note the worker URL is the first argument, and the options are timeout (ms, default 60 000) and onProgress (there is no threshold at this level).

    +

    Streaming#

    +
    import { buildDocumentPDFStream, concatChunks } from 'pdfnative';
    +
    +// buildDocumentPDFStream(params, layoutOptions?, streamOptions?)
    +// chunkSize lives in the 3rd argument (StreamOptions)
    +const chunks: Uint8Array[] = [];
    +
    +for await (const chunk of buildDocumentPDFStream(
    +  params,
    +  {},                        // layoutOptions
    +  { chunkSize: 65536 },      // streamOptions
    +)) {
    +  chunks.push(chunk);
    +}
    +
    +const pdf = concatChunks(chunks);
    +
    +

    The async iterable yields Uint8Array chunks as the PDF is produced — no full-document buffering. The three-argument API (params, layoutOptions, streamOptions) keeps layout concerns (tagged, compress, watermark) and streaming concerns (chunk size) separate.

    +

    Playgrounds#

    +

    The interactive playgrounds on pdfnative.dev run entirely in the browser:

    +
      +
    • Extreme scripts — live BiDi, Tamil, Bengali + Devanagari, Arabic harakat
    • +
    • Scale — 1,000 to 100,000 pages, Web Worker + true streaming
    • +
    +
    +

    Local testing: opening the playgrounds as file:// disables the Web Worker +(browsers block cross-origin Worker imports from file: origins). +Serve the docs directory instead:

    +
    npm run docs:serve   # → http://localhost:5000
    +
    +
    +

    Command line — pdfnative-cli#

    +

    Prefer the terminal? pdfnative-cli wraps the same library with 17 commands — including render, fill, sign, verify, encrypt, decrypt, merge, split, extract and extract-text:

    +
    # Install once
    +npm install --global pdfnative-cli
    +
    +# Render a JSON document to a PDF
    +pdfnative render --input report.json --output report.pdf
    +
    +# Sign with an RSA key (loaded from env var, never logged)
    +export PDFNATIVE_SIGN_KEY="$(cat private.pem)"
    +export PDFNATIVE_SIGN_CERT="$(cat cert.pem)"
    +pdfnative sign --input report.pdf --output report.signed.pdf
    +
    +# Inspect any PDF (encryption, signatures, PDF/A, metadata)
    +pdfnative inspect --input report.signed.pdf --format text
    +
    +

    Stdin/stdout makes it composable in shell pipelines:

    +
    cat report.json | pdfnative render | pdfnative sign | pdfnative inspect --format text
    +
    +

    See the dedicated CLI guide for the full command reference, security model, and CI/CD recipes.

    +

    Next steps#

    +
      +
    • Architecture — modules, builders, generation pipeline.
    • +
    • CLI — pdfnative-cli command-line interface.
    • +
    • Accessibility — tagged PDF, PDF/UA, PDF/A.
    • +
    • FAQ — fonts, encryption, signatures, comparisons.
    • +
    • Troubleshooting — common pitfalls and fixes.
    • +
    • Sample generators — 242 PDFs across 37 categories (48 generators).
    • +
    +
    diff --git a/docs/guides/react.html b/docs/guides/react.html index 6e0f116..51aa2d7 100644 --- a/docs/guides/react.html +++ b/docs/guides/react.html @@ -10,6 +10,7 @@ + @@ -27,6 +28,9 @@ + + + @@ -58,9 +62,380 @@

    Home  ›  Guides  ›  React

    -
    -

    Loading…

    - +
    + +

    pdfnative-react — Declarative JSX Renderer Guide

    +
    +

    Tracks the latest published pdfnative-react (v1.1.0, built on the pdfnative 1.6 engine), with React 19 and pdfnative ^1.6.0 as peer dependencies (semver-accepts 1.7.0). Live package versions — and the pdfnative version each one is built on — are shown at the top of the documentation home. Full history: pdfnative-react releases.

    +
    +

    pdfnative-react turns declarative JSX into real, on-device PDFs powered by the zero-dependency pdfnative engine — no DOM, no headless browser, no SaaS round-trips. Your documents never leave the process.

    +
    +

    Why a React renderer? Front-end teams already think in components. pdfnative-react lets you author a PDF the same way you author a UI — with familiar @react-pdf/renderer-style ergonomics (Document, Page, Text, usePdf, PDFViewer) — while the actual bytes are produced locally by pdfnative. It is the frontend gateway to the pdfnative ecosystem.

    +
    +
    import { Document, Heading, Text, Table, renderToBytes } from 'pdfnative-react';
    +
    +const bytes = renderToBytes(
    +  <Document title="Invoice #1024" footerText="Acme Inc">
    +    <Heading level={1}>Invoice #1024</Heading>
    +    <Text>Thank you for your business.</Text>
    +    <Table
    +      headers={['Item', 'Qty', 'Total']}
    +      rows={[{ cells: ['Pro plan', '1', '$49.00'], type: 'default', pointed: false }]}
    +      zebra
    +    />
    +  </Document>,
    +); // → Uint8Array, a valid PDF (%PDF-… …%%EOF)
    +
    +
    +

    How it works#

    +

    A custom React reconciler compiles your component tree — synchronously, with no DOM — into the pdfnative DocumentParams model, which the engine then renders to bytes. There is no CSS/flexbox engine and no <View>: it is an honest, declarative block flow where every component maps 1:1 onto a pdfnative block.

    +
    [your JSX tree]
    +      │ custom react-reconciler (synchronous, no DOM)
    +┌──────────────────────────┐
    +│   pdfnative-react (npm)  │  ← components compile to pdfnative blocks
    +└──────────────────────────┘
    +      │ import { buildDocumentPDFBytes, … } from 'pdfnative'
    +┌──────────────────────────┐
    +│      pdfnative (npm)     │  ← zero-dependency PDF engine
    +└──────────────────────────┘
    +
    +
    +

    Zero-dependency invariant preserved. React 19 is a peer dependency of pdfnative-react only. The core pdfnative library remains zero-dependency — adding the React renderer to your app never adds a dependency to the engine itself.

    +
    +
    +

    Installation#

    +
    npm install pdfnative-react pdfnative react
    +
    +

    Requirements: React 19 and pdfnative ^1.6.0 (both peer dependencies) · Node.js ≥ 22. The package adds one runtime dependency of its own, react-reconciler. Works in Node, browsers and SSR frameworks.

    +
    +

    Next.js and other React Server Component setups. The root barrel is deliberately not marked 'use client', and importing it from a Server Component or a 'use server' file fails — the reconciler needs createContext, which is unavailable under React's react-server condition. Render from a Route Handler instead (see Server rendering below). The hooks and viewer components carry the directive and are published separately at pdfnative-react/client; import them from there in an app that mixes server and client components, because the directive does not survive bundling in the root barrel.

    +
    +

    The package ships NPM provenance — verify the published artifact with npm audit signatures or on npmjs.com.

    +
    +

    When to use the React renderer#

    + + + + + + + + + + + + + + + + + + + + + + + +
    Use pdfnative-react when…Use the library / CLI when…
    You already build UIs in React and want PDFs the same wayYou write a non-React Node service → use pdfnative directly
    You want a live <iframe> preview in the browser (usePdf / PDFViewer)You operate from shell scripts or CI → use pdfnative-cli
    You are migrating from @react-pdf/rendererYou drive PDFs from an AI assistant → use pdfnative-mcp
    You want AI agents to author with the token-frugal DocSpecYou need Web Worker offloading or 100 % programmatic control
    +

    The packages are complementary and all sit on the same engine, so a PDF authored in any of them is byte-compatible with the others.

    +
    +

    Components#

    +

    Every component maps 1:1 onto a pdfnative block (Section being the one intentional composite).

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ComponentRenders
    DocumentThe required root (title, footerText, metadata, fontEntries, layout).
    PageAn explicit page boundary (content auto-paginates otherwise).
    HeadingA section heading (level 1–3); feeds the auto TableOfContents.
    Paragraph / TextA wrapping paragraph (fontSize, lineHeight, align, indent, color).
    List / ItemA bullet or numbered (ordered) list.
    Table / Row / CellA data table (data-driven headers/rows, or JSX <Row>/<Cell>).
    ImageAn embedded JPEG/PNG (data: Uint8Array).
    LinkA clickable hyperlink (url/href).
    SpacerVertical whitespace (height).
    PageBreakA hard page break.
    TableOfContents / TocAn auto-generated TOC built from headings.
    BarcodeQR, Code 128, EAN-13, PDF417, Data Matrix (format, data).
    SvgInline vector graphics (path data or markup).
    FormFieldInteractive AcroForm widgets (fieldType, name).
    ChartA native vector chart (chartType, series, categories, altText, …) — see Charts.
    SectionComposite (the one exception to the 1:1 mapping): expands to an optional PageBreak + a Heading + its children before the reconciler runs. Props: title, level (default 2), color, break.
    +
    +

    Server rendering#

    +

    renderToResponse returns a web-standard Response, so the same call works in a +Next.js Route Handler, a Remix loader, Deno, Bun, Cloudflare Workers and any +other runtime with the Fetch API. This is the supported way to render from a +server framework.

    +
    // app/invoice/route.tsx — Next.js Route Handler
    +import { renderToResponse } from 'pdfnative-react';
    +
    +export async function GET() {
    +  return renderToResponse(<Invoice />, {
    +    fileName: 'invoice.pdf',
    +  });
    +}
    +
    +

    renderSpecToResponse does the same from a DocSpec object rather than JSX, +which is the shape an AI agent is most likely to produce.

    +

    Charts#

    +

    <Chart> compiles to the engine's native chart block — vector path operators, +no rasterisation, and /Figure tagging with alt text.

    +
    <Chart
    +  chartType="bar"            // bar | barH | line | pie | donut
    +  title="Quarterly revenue"
    +  categories={['Q1', 'Q2', 'Q3', 'Q4']}
    +  series={[{ label: 'Revenue', values: [50, 62, 70, 81] }]}
    +  altText="Bar chart of quarterly revenue, rising each quarter"
    +/>
    +
    +

    This is why the pdfnative peer floor is ^1.6.0: a 1.5 engine receives an +unknown block and drops it silently.

    +

    Linting#

    +

    lintDocument runs 18 deterministic rules over a compiled tree — no I/O, so it +is safe in a test or a CI step. It catches the classes of mistake a type system +cannot, including L_TAGGED_NO_FONTS: declaring PDF/A without embedding a font, +which produces a file that claims conformance it does not have.

    +
    import { lintDocument, LINT_RULES } from 'pdfnative-react';
    +
    +const report = lintDocument(<Invoice />);
    +if (!report.ok) {
    +  const first = report.findings.find((f) => f.severity === 'error');
    +  throw new Error(first?.message ?? 'lint failed');
    +}
    +
    +

    Agent surface#

    +

    capabilityManifest() and doctor() return plain JSON describing what the +package can do and whether the environment supports it — the discovery pair an +autonomous agent should call before planning work. validateSpec, schema(subject?) +and SCHEMA_SUBJECTS cover DocSpec validation; aiGovernancePolicy(), +agentRulesText() and validateIssueDraft() expose the human-in-the-loop contract.

    +

    Rendering#

    +
    import {
    +  renderToBytes,   // (node, options?) => Uint8Array
    +  renderToBlob,    // (node, options?) => Blob (application/pdf)
    +  renderToStream,  // (node, options?) => AsyncGenerator<Uint8Array>
    +  renderToFile,    // (node, path, options?) => Promise<void> (Node only)
    +  renderToResponse,// (node, options?) => Promise<Response> (web standard)
    +  renderToFileStream, // (node, path, options?) => Promise<StreamToFileResult> — { bytesWritten, path } (Node only)
    +  inspectDocument, // (node) => layout diagnostics, no render
    +  compileDocument, // (node) => DocumentParams (inspect the model, no render)
    +} from 'pdfnative-react';
    +
    +

    options is { layout?: Partial<PdfLayoutOptions>; fontEntries?: FontEntry[]; fonts?: FontsMap } and merges on top of anything set on <Document> — page size, margins, colors, PDF/A mode, encryption, and non-Latin fonts. Note that fonts is only honored by the async entry points (renderToFile, renderToFileStream, renderToResponse, usePdf, usePdfStream) — font loading is asynchronous, so the synchronous entries (renderToBytes, renderToBlob, renderToStream) ignore it; resolve manually first (fontEntries: await resolveFonts(fonts) — but see the fontRef caveat below).

    +
    +

    Build fontEntries yourself — resolveFonts produces an invalid fontRef#

    +

    resolveFonts (and the fonts option, which calls it) sets each entry's +fontRef to the bare language code. fontRef is written straight into the PDF +as a resource name, so it must be a PDF name starting with / — and /F1 +and /F2 are reserved by the engine. A bare code yields latin 12 Tf and a +/Font key of latin 5 0 R, neither of which is valid syntax: Acrobat refuses +the file with "an error occurred while reading this document (14)", and Chrome +falls back to a default encoding and draws raw glyph indices — Multilingual +renders as 0XOWLOLQJXDO.

    +

    Until that is fixed upstream, resolve the fonts by hand:

    +
    import { registerFonts, loadFontData } from 'pdfnative';
    +
    +registerFonts({
    +  latin: () => import('pdfnative/fonts/noto-sans-data.js'),
    +  ar:    () => import('pdfnative/fonts/noto-arabic-data.js'),
    +});
    +
    +const langs = ['latin', 'ar'];
    +const fontEntries = await Promise.all(
    +  langs.map(async (lang, i) => {
    +    const fontData = await loadFontData(lang);
    +    // loadFontData resolves to null (it does not throw) when the code has no
    +    // registered loader — fail loudly instead of embedding nothing:
    +    if (!fontData) throw new Error(`font "${lang}" failed to load — did you call registerFonts first?`);
    +    return { fontData, fontRef: `/F${3 + i}`, lang };   // /F3, /F4, …
    +  }),
    +);
    +
    +const bytes = renderToBytes(<Doc />, { fontEntries });
    +
    +

    Rendering is synchronous, so registering without awaiting loadFontData embeds +nothing at all and every non-Latin glyph comes out blank.

    +
    +
    const bytes = renderToBytes(<Invoice />, {
    +  layout: { tagged: 'pdfa2b', compress: true },
    +});
    +
    +
    +

    Hooks & client components#

    +

    The published root bundle does not carry 'use client' (a directive in an internal module does not survive single-file bundling); import hooks and viewer components from pdfnative-react/client in a React Server Components app, or add the directive to your own file as below.

    +
    'use client';
    +import { usePdf } from 'pdfnative-react';
    +
    +function Preview({ doc }: { doc: React.ReactElement }) {
    +  const { url, loading } = usePdf(doc);
    +  return loading ? <p>Rendering…</p> : <iframe title="preview" src={url} />;
    +}
    +
    +
      +
    • usePdf(element, options?){ url, blob, bytes, loading, error, update }
    • +
    • usePdfStream(element, options?){ getStream() }
    • +
    • PDFViewer — live <iframe> preview.
    • +
    • PDFDownloadLink — one-click download (supports a render-prop child).
    • +
    • BlobProvider — render-prop access to the raw Blob.
    • +
    +
    +

    Try it live in the React playground — edit JSX or a DocSpec and see the PDF render in your browser.

    +
    +
    +

    Agent authoring — the token-frugal DocSpec#

    +

    pdfnative-react is a library, so the place LLM agents spend tokens is authoring documents. The compact DocSpec expresses the same document as terse, JSON-serializable tuples — and compiles to the exact same PDF as the JSX, because it is built on the very same components.

    +
    import { renderSpecToBytes, type DocSpec } from 'pdfnative-react';
    +
    +const spec: DocSpec = {
    +  title: 'Invoice #1024',
    +  footerText: 'Acme Inc',
    +  blocks: [
    +    ['h1', 'Invoice #1024'],
    +    ['p', 'Thank you for your business.', { align: 'right' }],
    +    ['table', { h: ['Item', 'Total'], r: [['Pro plan', '$49.00']], zebra: true }],
    +    ['qr', 'https://acme.example/pay/1024', { align: 'right' }],
    +  ],
    +};
    +
    +const bytes = renderSpecToBytes(spec);
    +
    +

    The equivalent JSX is several times more tokens for a typical document, because every block carries opening/closing tags and prop names. Same bytes out, far fewer tokens in.

    +
      +
    • compileSpec(spec)DocumentParams · specToElement(spec)<Document> element
    • +
    • renderSpecToBytes / renderSpecToBlob / renderSpecToStream / renderSpecToFile
    • +
    • docSpecSchema() → a Draft 2020-12 JSON Schema whose $id embeds the package version, so agents can self-validate a spec before rendering; docSpecSchemaId() returns the $id.
    • +
    +

    Block tuples: ['h1'|'h2'|'h3', text, opts?], ['p', text, opts?], ['ul'|'ol', items, opts?], ['table', { h?, r }], ['img', { data }], ['link', text, { url }], ['sp', height?], ['br'], ['page', blocks], ['toc', opts?], ['qr'|'code128'|'ean13'|'pdf417'|'datamatrix', data, opts?], ['svg', data, opts?], ['chart', { chartType, series, … }], ['field', { fieldType, name, … }].

    +
    +

    Fonts & environment#

    +

    Re-exported from the engine: registerFonts, registerFont, loadFontData, validateFontData, downloadBlob (browser), initNodeCompression (Node). (loadFontData is a pure dynamic import — it works in the browser too.) Pass non-Latin fonts via the fontEntries render option (or on <Document fontEntries={…}>), unlocking all 22 bundled Unicode scripts and COLRv1 colour emoji exactly as in the core library.

    +
    import { Document, Text, renderToBytes, registerFont, loadFontData } from 'pdfnative-react';
    +
    +registerFont('th', () => import('pdfnative/fonts/noto-thai-data.js'));
    +const th = await loadFontData('th');
    +if (!th) throw new Error('Thai font failed to load');
    +const bytes = renderToBytes(
    +  <Document
    +    title="สวัสดี"
    +    fontEntries={[{ fontData: th, fontRef: '/F3', lang: 'th' }]} // /F1 and /F2 are reserved
    +  >
    +    <Text>สวัสดีชาวโลก</Text>
    +  </Document>,
    +);
    +
    +
    +

    Migrating from @react-pdf/renderer#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @react-pdf/rendererpdfnative-react
    <Document> / <Page><Document> / <Page>
    <Text><Text> (alias of <Paragraph>)
    <View> + flexbox styles(none — declarative block flow; use blocks + <Spacer>)
    StyleSheetper-component props (align, color, fontSize, …)
    <PDFViewer> / <PDFDownloadLink> / <BlobProvider>same names, same shape
    usePDF()usePdf()
    +

    The biggest mental shift: there is no flexbox layout engine. Documents are a top-to-bottom block flow. Use <Spacer>, <PageBreak>, tables, and per-component alignment props instead of <View> containers.

    +
    +

    Diagnostics#

    +

    PdfStructureError is thrown with an actionable message when a tree cannot be mapped onto the document model (for example, a <Cell> outside a <Row>, or an unsupported child of <Document>). Catch it to surface authoring mistakes early:

    +
    import { PdfStructureError, renderToBytes } from 'pdfnative-react';
    +
    +try {
    +  const bytes = renderToBytes(<MyDoc />);
    +} catch (e) {
    +  if (e instanceof PdfStructureError) console.error('Invalid document tree:', e.message);
    +  else throw e;
    +}
    +
    +
    +

    Resources#

    + +
    diff --git a/docs/guides/signatures.html b/docs/guides/signatures.html index e774475..be9be73 100644 --- a/docs/guides/signatures.html +++ b/docs/guides/signatures.html @@ -1,85 +1,488 @@ - - - - - - Digital signatures — pdfnative - - - + + + + + + Digital signatures — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Digital signatures

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Digital signatures

    +
    + +

    Digital signatures in pdfnative

    +

    pdfnative ships a zero-dependency CMS/PKCS#7 detached signature +implementation (ISO 32000-1 §12.8) with full crypto in pure TypeScript — +RSA PKCS#1 v1.5 and ECDSA P-256, both with SHA-256, X.509 DER parsing, +and ASN.1 DER encoding. No OpenSSL, no node-forge, no external crypto.

    +

    TL;DR — sign any PDF in 3 lines#

    +
    import {
    +    buildDocumentPDFBytes,
    +    addSignaturePlaceholder,
    +    signPdfBytes,
    +    parseCertificate,
    +    parseRsaPrivateKey,
    +} from 'pdfnative';
    +
    +const unsigned = buildDocumentPDFBytes(params);
    +const placeheld = addSignaturePlaceholder(unsigned, { fieldName: 'Author' });
    +const signed = signPdfBytes(placeheld, {
    +    signerCert: parseCertificate(certDer),  // X509Certificate (from DER bytes)
    +    rsaKey: parseRsaPrivateKey(keyDer),     // RsaPrivateKey (from DER bytes)
    +    algorithm: 'rsa-sha256',
    +});
    +
    +

    That's it. addSignaturePlaceholder() injects an +AcroForm + invisible signature widget + /Sig dictionary into the +existing PDF via an incremental update (ISO 32000-1 §7.5.6), then +signPdfBytes() computes the /ByteRange, hashes the document, builds +the CMS SignedData, and writes the result into the /Contents +placeholder.

    +

    Three-step pipeline#

    +
    buildDocumentPDFBytes(params)
    +        │
    +        ▼
    +   unsigned PDF
    +        │
    +        │  addSignaturePlaceholder()  ← injects AcroForm + /Sig dict
    +        ▼
    +   PDF with /ByteRange + /Contents placeholder
    +        │
    +        │  signPdfBytes()             ← hashes, signs, fills /Contents
    +        ▼
    +   signed PDF (Adobe Reader ✓ / openssl-cms ✓)
    +
    +

    1. addSignaturePlaceholder(pdfBytes, options?)#

    +

    Idempotent. If the input already contains an /FT /Sig widget, +returns the input unchanged. Throws on encrypted input.

    +

    Options:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptionDefaultNotes
    placeholderBytes16384Size of the /Contents hex placeholder
    fieldName'Signature1'AcroForm field name
    pageIndex0Page to attach the (invisible) widget to
    rect[0, 0, 0, 0]Widget rectangle (invisible by default)
    allowMultiplefalseAppend a placeholder even when signature fields already exist — the multi-signature flow (v1.7.0, see below)
    +

    To size placeholderBytes from your actual certificate chain instead of +relying on the 16 KiB default, use estimateContentsSize(certSizes, algorithm). +Since v1.7.0 it takes an options object: estimateContentsSize(certSizes, 'rsa-sha256', { timestamp: true }) reserves ~8 KiB of extra headroom for an +RFC 3161 timestamp token (covering the TSA's own certificate chain) when an +external signer will add one to the CMS.

    +

    Signer metadata (name, reason, location, contactInfo, signingTime) +is not set here — the placeholder writes an empty /Sig dictionary; pass +the metadata to signPdfBytes, which writes it at signing time.

    +

    2. signPdfBytes(pdfBytes, options)#

    +

    Reads the /ByteRange, hashes the two byte ranges (everything except +the /Contents slot), builds a CMS SignedData with the certificate +chain and signedAttrs (content-type, message-digest, signing-time), +signs the signedAttrs digest, and writes the DER-encoded CMS into +/Contents.

    +

    Options:

    +
      +
    • signerCertX509Certificate (parse DER bytes with parseCertificate).
    • +
    • rsaKey or ecKey — the private key. Parse RSA keys from DER with +parseRsaPrivateKey; for ECDSA there is no DER parser in the engine — supply +the EcPrivateKey scalar directly, or install a crypto provider +(setCryptoProvider / per-call provider) and omit the raw key entirely.
    • +
    • algorithm'rsa-sha256' | 'rsa-sha384' | 'rsa-sha512' | 'ecdsa-sha256' +(default 'rsa-sha256'; the SHA-384/512 RSA variants are new in v1.7.0).
    • +
    • certChain? — additional intermediate-CA certificates for the chain.
    • +
    • signingTime? — forwarded to signedAttrs.
    • +
    • fieldName? — with several unsigned placeholders in the file (the +allowMultiple flow below), selects which one to sign by its AcroForm field +name. With a single placeholder — the only case prior to v1.7.0 — the option +is unnecessary and behaviour is unchanged. (v1.7.0)
    • +
    +

    3. Verifying#

    +

    The library does not ship a PDF-signature verifier — its +verification surface is limited to openPdf() plus +verifyCertSignature() (an X.509 certificate-signature check). To +verify a signed PDF end to end (byte-range digest, CMS signature value, +chain, trust, timestamps, revocation), use +pdfnative-cli verify or the +verify_pdf MCP tool — the CMS verification logic lives +there. Since pdfnative-mcp 1.6.0, verify_pdf also validates +/DocTimeStamp fields as RFC 3161 tokens and reports the achieved +PAdES level (B-B → B-LTA) with ltv: true.

    +

    Why a separate placeholder step?#

    +

    The PDF signature spec is unusual: the /Contents field of the /Sig +dictionary must contain the CMS bytes, but the /ByteRange excludes +exactly that slot. So the PDF is hashed without the bytes we're +about to write, which means the file must already have the right +layout — including the placeholder reserved bytes — before we sign.

    +

    addSignaturePlaceholder() is the canonical way to produce that +layout. It replaces the ad-hoc reimplementations that downstream +tooling (notably pdfnative-cli's sign command and pdfnative-mcp's +prepare_signature_placeholder workaround) previously had to ship.

    +

    Multiple signatures (v1.7.0)#

    +

    Simply repeating the placeholder + sign pass does not add a second +signature: addSignaturePlaceholder() is idempotent by contract, so on a PDF +that already carries any /FT /Sig field it returns the input unchanged. To +add further signatures, opt in with allowMultiple and give each signature its +own field name:

    +
    // First signer.
    +let pdf = addSignaturePlaceholder(unsigned, { fieldName: 'Author' });
    +pdf = signPdfBytes(pdf, {
    +    signerCert: authorCert, rsaKey: authorKey, algorithm: 'rsa-sha256',
    +});
    +
    +// Second signer — appended as a fresh incremental update.
    +pdf = addSignaturePlaceholder(pdf, { fieldName: 'Reviewer', allowMultiple: true });
    +pdf = signPdfBytes(pdf, {
    +    signerCert: reviewerCert, rsaKey: reviewerKey, algorithm: 'rsa-sha256',
    +    fieldName: 'Reviewer', // selects which unsigned placeholder to fill
    +});
    +
    +

    The exact semantics:

    +
      +
    • allowMultiple: false (default) preserves the 1.x idempotent +short-circuit: any existing signature field returns the input unchanged.
    • +
    • allowMultiple: true — an existing unsigned placeholder with the same +fieldName returns the input unchanged (per-name idempotence); a signed +field with the same name throws (pass a fresh fieldName); other signature +fields are left alone and a new placeholder is appended via incremental +update, so /Prev chains compose naturally.
    • +
    • signPdfBytes with several unsigned placeholders requires the +fieldName selector — without it, the call throws and lists the unsigned +field names. Already-signed signatures are never modified.
    • +
    +

    Inspecting signatures — listSignatures() (v1.7.0)#

    +

    listSignatures(pdfBytes) enumerates every signature field in a PDF, in +AcroForm /Fields order — signed signatures, document timestamps, and +still-unsigned placeholders:

    +
    import { listSignatures } from 'pdfnative';
    +
    +for (const sig of listSignatures(signedPdf)) {
    +    console.log(sig.fieldName, sig.subFilter, sig.isPlaceholder);
    +}
    +// → 'Author'   'adbe.pkcs7.detached' false
    +// → 'Reviewer' 'adbe.pkcs7.detached' false
    +
    +

    Each entry is a PdfSignatureInfo:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeMeaning
    fieldName?stringThe widget's /T field name, when present
    subFilterstring/SubFilter — e.g. 'adbe.pkcs7.detached', 'ETSI.CAdES.detached', 'ETSI.RFC3161' ('' when absent)
    byteRangereadonly number[]/ByteRange offsets
    contentsUint8ArrayThe full decoded /Contents value, trailing zero padding included
    isDocTimestampbooleantrue for /Type /DocTimeStamp entries (ISO 32000-2 §12.8.5)
    isPlaceholderbooleantrue for an unsigned placeholder (all-zero /ByteRange)
    sigObjNumnumberObject number of the /Sig dictionary
    +

    listSignatures() is a read-only inspection API — it never verifies +anything (see Verifying above for the verification story).

    +

    Related v1.7.0 addition: SigDictMetadata — the metadata subset shared by +buildSigDict() and the signing options — gains a subFilter field, +'adbe.pkcs7.detached' (default, unchanged legacy behaviour) or +'ETSI.CAdES.detached' to declare a PAdES (ETSI EN 319 142) signature. +buildSigDict() writes it into the /Sig dictionary, and listSignatures() +reports it back on each entry. When declaring 'ETSI.CAdES.detached', pair it +with the profile: 'pades' signing option so the CMS carries the matching +ESS signing-certificate-v2 attribute.

    +

    Algorithms#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AlgorithmHashCurve / ModulusNotes
    rsa-sha256SHA-2562048 / 3072 / 4096PKCS#1 v1.5 (default)
    rsa-sha384SHA-3842048 / 3072 / 4096PKCS#1 v1.5 (v1.7.0)
    rsa-sha512SHA-5122048 / 3072 / 4096PKCS#1 v1.5 (v1.7.0)
    ecdsa-sha256SHA-256P-256 (secp256r1)DER-encoded ECDSA signature
    +

    That is the complete SignatureAlgorithm union — the RSA variants widened +to SHA-384/512 in v1.7.0; ECDSA is offered with SHA-256 only.

    +

    All primitives live under src/crypto/: +SHA-256/384/512 in sha.ts, ASN.1 DER in asn1.ts, RSA modular +arithmetic in rsa.ts, ECDSA P-256 in ecdsa.ts, X.509 parsing in +x509.ts, and the CMS SignedData builder in cms.ts.

    +

    Validating the output#

    +
    # openssl-cms — extract the CMS payload and verify against the certificate
    +openssl pkcs7 -in signed.pdf -inform DER -print_certs
    +
    +# Adobe Reader — open signed.pdf; signatures panel shows the field name,
    +# signing time, signer subject, and a green check if the chain validates.
    +
    +# pdfnative-cli — full CMS verification (the library itself has no PDF-signature verifier)
    +npx pdfnative-cli verify --input signed.pdf --strict
    +# → { "signatures": [ { "integrity": true, "signatureValid": true, ... } ] }
    +
    +

    Reading the validator output#

    +

    Two warnings commonly surface when testing the sample PDFs in Adobe Reader. +Both are expected by spec — they are not pdfnative bugs.

    +

    "Validity unknown" / "Identité du signataire inconnue"#

    +

    Adobe shows this whenever the signing certificate's issuer chain does +not terminate in a root CA listed in Adobe's Approved Trust List (AATL) +or in the user's locally configured Trusted Identities.

    +
      +
    • The scripts/generators/digital-signature.ts sample uses a +self-signed demo CA so it can ship deterministically. The +cryptographic signature itself is valid (Adobe says so: +"Le document n'a pas été modifié depuis l'apposition de la signature"); +only the identity link to a public root is missing.
    • +
    • To remove the warning in Adobe Reader: Preferences → Signatures → +Identités → Identités autorisées → Ajouter and import the demo +certificate as a trusted root.
    • +
    • To verify the CMS independently of any trust store, use +openssl pkcs7 -in signed.pdf -inform DER -print_certs and +openssl cms -verify -CAfile demo-ca.pem.
    • +
    • For production signatures, use a certificate issued by a CA in the +Adobe Approved Trust List (Sectigo, DigiCert, GlobalSign…) or your +organisation's enterprise CA distributed via group policy.
    • +
    +

    "Signature non valable" on a placeholder PDF#

    +

    A PDF that has only been through addSignaturePlaceholder() (i.e. not +yet signPdfBytes()) will read as invalid in Adobe — and that is +correct behaviour. The /Sig dictionary's /Contents slot is +zero-padded hex by design, reserved for the CMS SignedData that the +external signer will produce. Adobe sees a malformed CMS and reports +the signature as broken.

    +

    The scripts/generators/signature-placeholder.ts sample produces +exactly this shape on purpose, to demonstrate:

    +
      +
    1. The placeholder layout is byte-stable (the -idempotent companion +PDF proves it — re-running the function produces identical bytes).
    2. +
    3. Downstream tooling (HSMs, cloud KMS, smartcards) can fill in +/Contents without touching the surrounding objects.
    4. +
    +

    To turn a placeholder into a valid signature, call signPdfBytes() on +the placeholder bytes — that's the pipeline shown in the TL;DR above.

    +

    Caveats#

    +
      +
    • Encrypted PDFs. addSignaturePlaceholder() throws on encrypted +input — sign before encrypting, or decrypt first.
    • +
    • Timestamping (RFC 3161). Supported since v1.7.0 — +signPdfBytesWithTimestamp() embeds a verified TSA token as the +id-aa-signatureTimeStampToken unsigned attribute, and +addDocumentTimestamp() appends /DocTimeStamp revisions. See the +Long-term validation guide for the full PAdES B-B → B-LTA +pipeline (timestamps, /DSS + /VRI, injected providers).
    • +
    • Multiple signatures. Supported since v1.7.0, but not by naively +repeating the placeholder + sign pass — addSignaturePlaceholder() is +idempotent and returns an already-signed PDF unchanged. Pass +allowMultiple: true with a fresh fieldName, then select that field in +signPdfBytes — see Multiple signatures +above.
    • +
    • PDF/A + signatures. PDF/A-2b/3b allow signatures; ISO 19005-2 +§6.3.5 forbids certain /Sig dictionary fields (/Reference, +/Changes). pdfnative emits only the conformant subset.
    • +
    • Timing side-channels (pure-JS BigInt). The RSA/ECDSA math runs on +JavaScript BigInt, which is not constant-time — the RSA modular +exponentiation in particular is a secret-dependent square-and-multiply +loop. Signing a PDF once per user action is not meaningfully +exploitable, but a high-frequency server signing thousands of PDFs/s +with the same key under adversarial timing observation could +theoretically leak key material. For such pipelines, install a native +constant-time signer via setCryptoProvider() (see below) — or compute the +CMS/PKCS#7 blob with a constant-time native backend (Node.js +crypto.sign() or WebCrypto crypto.subtle.sign()) and inject it via +signPdfBytes(). See SECURITY.md +for the full analysis.
    • +
    +

    Native crypto provider (v1.4.0)#

    +

    For high-security, high-frequency server pipelines you can replace +pdfnative's pure-JS RSA/ECDSA math with a native, constant-time signer +without giving up the zero-dependency default. Install a provider globally +with setCryptoProvider(provider), or pass one per call via +PdfSignOptions.provider (per-call wins over global). When a provider is set, +rsaKey / ecKey are no longer required.

    +
    import { setCryptoProvider, signPdfBytes } from 'pdfnative';
    +import { createSign, createPrivateKey } from 'node:crypto';
    +
    +const key = createPrivateKey(pemPrivateKey);
    +
    +setCryptoProvider({
    +    // `tbs` is the DER-encoded CMS signed attributes. The provider hashes it
    +    // with SHA-256 internally (node:crypto does this for you) and returns the
    +    // raw signature value (RSA PKCS#1 v1.5, or a DER-encoded ECDSA-P256 sig).
    +    sign(tbs, algorithm) {
    +        return new Uint8Array(createSign('sha256').update(tbs).sign(key));
    +    },
    +});
    +
    +const signed = signPdfBytes(placeheld, {
    +    signerCert: cert,
    +    algorithm: 'rsa-sha256', // rsaKey/ecKey no longer needed
    +});
    +
    +// Restore the pure-JS default at any time:
    +setCryptoProvider(null);
    +
    +

    This is the in-library escape hatch for the BigInt timing caveat above — the +secret-dependent math runs in node:crypto / Web Crypto / an HSM while +pdfnative's CMS/PKCS#7 assembly is reused unchanged.

    +

    Full example#

    +

    See scripts/generators/digital-signature.ts +for a runnable RSA + ECDSA sample (key generation, certificate +construction, sign, verify) and +scripts/generators/signature-placeholder.ts +for the idempotency proof.

    + +
      +
    • PDF/A conformance → — how signatures interact with PDF/A-2b/3b.
    • +
    • Architecture → — where the crypto module sits in the dependency graph.
    • +
    • CLI →pdfnative-cli sign wraps this exact pipeline.
    • +
    • MCP integration →pdfnative-mcp exposes signing as an AI tool, and since v1.6.0 the complete PAdES ladder: sign_pdf (profile: 'pades', RFC 3161 timestamp) → add_ltv (/DSS + /VRI, B-LT) → timestamp_pdf (/DocTimeStamp, B-LTA), verified with verify_pdf ltv: true.
    • +
    + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/streaming.html b/docs/guides/streaming.html index 9f6eff7..dd405da 100644 --- a/docs/guides/streaming.html +++ b/docs/guides/streaming.html @@ -1,85 +1,252 @@ - - - - - - Streaming output — pdfnative - - - + + + + + + Streaming output — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Streaming output

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Streaming output

    +
    + +

    Streaming output

    +
    +

    pdfnative emits PDFs as AsyncGenerator<Uint8Array> so you can pipe them to disk, an HTTP response, or a Web Stream without buffering the whole document. New in v1.3.0: a mode in which the fully-joined PDF binary never exists in memory — see what this does and does not buy you for the exact profile.

    +
    +

    Three streaming modes#

    + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionMemory profileWhen to use
    buildDocumentPDFStream / buildPDFStreamAssembles full binary, then yields fixed-size chunksSimple back-pressure-friendly piping
    buildDocumentPDFStreamPageByPage / buildPDFStreamPageByPageAssembles full binary, yields one PDF object per chunkObject-granular persistence / diagnostics
    buildDocumentPDFStreamTrue / buildPDFStreamTrueNever joins the binary — frees each part as it yieldsLarge documents, and anything over ~512 MB of output
    +

    All three produce byte-identical output to buildDocumentPDFBytes() / +buildPDFBytes().

    +

    Streaming without joining the binary (v1.3.0)#

    +
    import { createWriteStream } from 'node:fs';
    +import { buildDocumentPDFStreamTrue } from 'pdfnative';
    +
    +const out = createWriteStream('report.pdf');
    +for await (const chunk of buildDocumentPDFStreamTrue(params, layout, { chunkSize: 65536 })) {
    +  out.write(chunk);
    +}
    +out.end();
    +
    +

    Internally, the builder assembles the PDF into an array of raw parts (objects, +xref, trailer) and the generator walks that array, encoding each part to bytes +and freeing it (parts[i] = '') as soon as it is emitted.

    +

    What this does and does not buy you#

    +

    Being precise here matters, because the two claims are often conflated.

    +

    What it avoids. The joined PDF binary never exists. That removes a full +second copy of the document, and it lifts the hard ceiling that +buildDocumentPDFStream hits: that variant joins everything into one JavaScript +string, and V8 caps a single string at roughly 512 MB. Past that point it throws +regardless of how much RAM the machine has. buildDocumentPDFStreamTrue has no +such ceiling.

    +

    What it does not avoid. assembleDocumentParts() runs to completion before +the first chunk is yielded, so every part is resident at that moment. Peak memory +is therefore still proportional to total output size — roughly 2 bytes per output +character, since JavaScript strings are UTF-16. From the first yield onwards +memory falls monotonically as parts are freed, but the peak has already happened.

    +

    Two practical consequences:

    +
      +
    • Budget for about twice your expected output size, not for a fixed ceiling. +A 300 MB PDF wants roughly 600 MB of headroom.
    • +
    • There is no progress signal during assembly. Most of the wall-clock time is +spent inside assembleDocumentParts(), which yields nothing, so a percentage +bar covering that phase would be invented rather than measured. Report an +indeterminate state until the first chunk arrives, then switch to a byte +counter. The scale playground does exactly this.
    • +
    +

    True page-by-page assembly — where peak memory is bounded by one page rather than +the whole document — is not implemented yet.

    +

    HTTP response (Node)#

    +
    import { buildDocumentPDFStreamTrue } from 'pdfnative';
    +
    +app.get('/report.pdf', async (req, res) => {
    +  res.setHeader('Content-Type', 'application/pdf');
    +  for await (const chunk of buildDocumentPDFStreamTrue(params)) {
    +    res.write(chunk);
    +  }
    +  res.end();
    +});
    +
    +

    Web Streams (browser / Deno / edge)#

    +
    const stream = new ReadableStream({
    +  async pull(controller) {
    +    for await (const chunk of buildDocumentPDFStreamTrue(params)) {
    +      controller.enqueue(chunk);
    +    }
    +    controller.close();
    +  },
    +});
    +return new Response(stream, { headers: { 'Content-Type': 'application/pdf' } });
    +
    +

    Options#

    +
    interface StreamOptions {
    +  /** Bytes per yielded chunk. Default 65536 (64 KB). Clamped to 1 KB–16 MB. */
    +  chunkSize?: number;
    +}
    +
    +

    Draining to a file: streamToFile (v1.4.0)#

    +

    streamToFile() drains any AsyncGenerator<Uint8Array> — including all +three streaming modes above — straight to a file on disk in Node.js. It honours +the OS write back-pressure (awaiting the 'drain' event when the kernel buffer +fills) and supports cancellation via an AbortSignal, so a single call covers +the common "generate a large PDF to disk without buffering it" case.

    +
    import { buildDocumentPDFStreamTrue, streamToFile } from 'pdfnative';
    +
    +const { bytesWritten, path } = await streamToFile(
    +  buildDocumentPDFStreamTrue(params),
    +  'report.pdf',
    +);
    +console.log(`Wrote ${bytesWritten} bytes to ${path}`);
    +
    +

    Cancellation#

    +
    const ac = new AbortController();
    +setTimeout(() => ac.abort(), 5000); // give up after 5s
    +
    +await streamToFile(buildDocumentPDFStreamTrue(params), 'report.pdf', {
    +  signal: ac.signal,
    +}); // rejects with the abort reason; the partial file is closed
    +
    +

    On abort — or on any write error — streamToFile releases the file descriptor +and removes the partially-written file (best-effort), so a cancelled or +failed run never leaves an orphaned half-written PDF on disk.

    +

    streamToFile is Node-only — it loads node:fs lazily via a dynamic import, +so importing it in a browser or Deno bundle adds no static Node dependency. In +non-Node runtimes, drive the generator yourself with the Web Streams snippet +above.

    +

    Streaming merge & split (v1.6.0)#

    +

    The page-tree API has constant-memory variants — +streamMergedPdfs / streamSplitPdf / streamExtractPages — that emit the +assembled document as fixed-size chunks while holding only the cross-reference +offsets in memory. Each is byte-identical to its buffered counterpart and +composes with streamToFile. (Exception: with MergeOptions.encrypt the +output is AES-encrypted with fresh random IVs/salts, so repeated invocations +are structurally — not byte — identical.)

    +
    import { streamMergedPdfs, streamSplitPdf, streamToFile } from 'pdfnative';
    +
    +await streamToFile(streamMergedPdfs([a, b]), 'combined.pdf');
    +
    +for await (const part of streamSplitPdf(body, ranges)) {
    +  await streamToFile(part.pdf, `part-${part.index}.pdf`);   // drain each fully, in order
    +}
    +
    +

    Note the honest memory profile: output bytes are never buffered, but the +source PDFs are still in-memory Uint8Arrays. See the +PDF manipulation guide for the +full API.

    +

    Constraints#

    +

    Streaming is incompatible with two features that need a second pass over the +whole document. Both are validated at the boundary and throw a descriptive +error:

    +
      +
    • TOC blocks — the table of contents requires multi-pass pagination to +resolve page numbers.
    • +
    • {pages} placeholder in header/footer templates — the total page count +is unknown during progressive emission. Use {page} instead, or fall back to +buildDocumentPDFBytes().
    • +
    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/tables.html b/docs/guides/tables.html index 861ccf6..a3f8067 100644 --- a/docs/guides/tables.html +++ b/docs/guides/tables.html @@ -1,85 +1,410 @@ - - - - - - Smart tables — pdfnative - - - + + + + + + Smart tables — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Smart tables

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Smart tables

    +
    + +

    Smart tables

    +
    +

    Backward-compatible with v1.1.0 — existing single-page tables produce byte-identical output.

    +
    +

    pdfnative's table renderer is planner-driven and multi-page-safe by default. Long tables wrap on column overflow, slice cleanly across pages, and reprint their header on every continuation page — matching the behaviour readers expect from commercial PDF libraries.

    +

    This guide documents the TableBlock fields, the planner architecture, the tagged-mode contract, and migration tips.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +
    +const bytes = buildDocumentPDFBytes({
    +    title: 'Invoice',
    +    blocks: [
    +        {
    +            type: 'table',
    +            headers: ['Item', 'Qty', 'Price'],
    +            // PdfRow[]: e.g. { cells: ['Pro plan', '1', '$49.00'], type: 'default', pointed: false }
    +            rows: bigInvoiceRows, // any length
    +            columns: [            // optional ColumnDef[]: f = width fraction, a = align,
    +                                  // mx / mxH = char caps (data / header), plus minWidth? / maxWidth?
    +                { f: 0.6, a: 'l', mx: 40, mxH: 20 },
    +                { f: 0.2, a: 'r', mx: 10, mxH: 8 },
    +                { f: 0.2, a: 'r', mx: 10, mxH: 8 },
    +            ],
    +            autoFitColumns: true,  // content-aware widths (respects minWidth/maxWidth)
    +            wrap: 'auto',          // ← new (default)
    +            repeatHeader: true,    // ← new (default)
    +            zebra: true,           // ← new (opt-in)
    +            caption: 'Invoice line items',
    +            minRowHeight: 14,
    +            cellPadding: 5,
    +        },
    +    ],
    +});
    +
    +

    Existing v1.1.0 code with no new fields continues to work and produces byte-identical output on single-page tables.

    +
    +

    TableBlock fields (all optional)#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDefaultDescription
    wrap'auto' | 'always' | 'never''auto'Per-cell wrap policy.
    repeatHeaderbooleantrueReprint the header row at the top of each continuation page.
    zebraboolean | PdfColorfalseAlternating data-row fill. true uses '0.969 0.973 0.984'.
    captionstringundefinedCaption printed once above the first slice.
    minRowHeightnumber (points)12Minimum visual row height.
    cellPaddingnumber (points)3Internal cell padding.
    cellBordersCellBordersundefinedPer-cell vector borders (v1.4.0). Omit for byte-identical pre-1.4.0 output.
    cellVAlign'top' | 'middle' | 'bottom'undefinedVertical alignment of cell text within the row band (v1.4.0).
    +

    wrap#

    +
      +
    • 'auto' (default) — single-line rendering when cell content fits within the column width; wraps on overflow only. This is the GAFAM-grade default — fast typical case, correct edge case.
    • +
    • 'always' — every cell is run through the word-wrapper. Use when row heights need to be uniform regardless of content length.
    • +
    • 'never' — v1.1.0 behaviour. Content is clipped at the column boundary. Use when output byte-stability against v1.1.0 is mandatory.
    • +
    +

    repeatHeader#

    +
      +
    • true (default) — header row reprints at the top of every continuation page.
    • +
    • false — header appears only once. Set this alongside wrap: 'never' to preserve the exact v1.1.0 multi-page rendering shape.
    • +
    +

    zebra#

    +
      +
    • false (default) — no row fill.
    • +
    • true — alternating even data rows (1-indexed, so the second row, fourth row, …) are filled with '0.969 0.973 0.984' (a soft cool-grey tuned for accessibility contrast).
    • +
    • A PdfColor — hex ('#f7f8fa'), tuple ([247, 248, 250], channels 0–255), or PDF-rgb string ('0.97 0.97 0.98', channels 0.0–1.0) — overrides the default.
    • +
    +

    caption#

    +
      +
    • Printed once at the top of the table (above the first slice), using Helvetica 9pt.
    • +
    • In tagged mode, emitted as a /Caption structure element child of /Table (ISO 14289-1 §7.10.6).
    • +
    • Multi-line captions wrap to fit the table width.
    • +
    +

    minRowHeight / cellPadding#

    +
      +
    • minRowHeight enforces a floor so rows look consistent even with short text.
    • +
    • cellPadding is the internal padding around each cell's text. Header padding inherits this but the baseline offset is a fixed v1.1.0-compatible constant (preserves byte-stability for the row body).
    • +
    +

    cellBorders (v1.4.0)#

    +

    Draw per-cell vector borders. All sides are off by default — omitting cellBorders entirely keeps the table byte-identical to pre-1.4.0 (header underline + row separators only).

    +
    {
    +    type: 'table',
    +    headers: ['Item', 'Qty'],
    +    rows: [/* … */],
    +    cellBorders: {
    +        all: true,            // shorthand for top+right+bottom+left
    +        color: '#cccccc',     // PdfColor — default '0.8 0.8 0.8'
    +        width: 0.5,           // points — default 0.5
    +        style: 'solid',       // 'solid' | 'dashed' | 'dotted'
    +    },
    +}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDefaultDescription
    top / right / bottom / leftbooleanfalseEnable individual edges.
    allbooleanfalseShorthand — enables all four edges.
    colorPdfColor'0.8 0.8 0.8'Stroke colour (hex / tuple / PDF-rgb).
    widthnumber (points)0.5Stroke width.
    style'solid' | 'dashed' | 'dotted''solid'Stroke style. The dash is reset after each cell so row separators stay solid.
    +

    Borders are pure vector strokes (m … l S), so output stays PDF/A-safe.

    +

    cellVAlign (v1.4.0)#

    +

    Vertically align cell text within the row band. Useful when rows have a tall minRowHeight or wrapped cells of differing height. A per-column ColumnDef.vAlign overrides the table default.

    +
    { type: 'table', headers: [/* … */], rows: [/* … */], minRowHeight: 30, cellVAlign: 'middle' }
    +
    +

    'top' | 'middle' | 'bottom'. When omitted, the historic baseline placement is preserved exactly (byte-identical to pre-1.4.0).

    +
    +

    ColumnDef.kind field (optional)#

    + + + + + + + + + + + + + + + +
    FieldTypeDefaultDescription
    kind'amount'undefinedSemantic hint. When set to 'amount', data cells in this column render in Helvetica-Bold with credit/debit colouring driven by row.type. Replaces the pre-1.2.0 hardcoded i === 3 heuristic in renderTable.
    +
    {
    +  type: 'table',
    +  headers: ['Date', 'Description', 'Status', 'Amount'],
    +  columns: [
    +    { f: 0.20, a: 'l', mx: 12, mxH: 12 },
    +    { f: 0.45, a: 'l', mx: 60, mxH: 60 },
    +    { f: 0.20, a: 'l', mx: 20, mxH: 20 },
    +    { f: 0.15, a: 'r', mx: 18, mxH: 18, kind: 'amount' }, // ← opt-in bold + credit/debit colour
    +  ],
    +  rows: [
    +    { cells: ['2026-05-01', 'Salary', 'Cleared', '+3 000.00'], type: 'credit', pointed: false },
    +    { cells: ['2026-05-03', 'Rent',   'Pending', '-1 250.00'], type: 'debit',  pointed: false },
    +  ],
    +}
    +
    +
    +

    Behaviour change for document-builder tables without kind. Previously the renderer applied Helvetica-Bold + credit/debit colour to whichever column happened to be at index 3. The current renderer removes that heuristic — opt in explicitly via kind: 'amount'. The legacy buildPDF() (financial-statement) path keeps the historical heuristic for byte-identical v1.0/v1.1 output.

    +
    +
    +

    How multi-page tables are sliced#

    +

    pdfnative uses a two-phase pipeline:

    +
      +
    1. Plan phaseplanTable() (src/core/pdf-renderers.ts) measures the entire table once: resolves columns (including autoFit), word-wraps each cell according to wrap, computes per-row heights, and produces a TablePlan containing every row's exact pixel height.
    2. +
    3. Slice phase_paginateBlocks() in src/core/pdf-document.ts walks the plan greedily: it packs rows onto the current page until the next row would overflow, then emits a TableSlice ( { fromRow, toRow, drawCaption, drawHeader, isFinalSlice }) and starts a new page. The caption is emitted once (on the first slice); the header is emitted on every slice when repeatHeader: true.
    4. +
    +

    renderTable() is page-lifecycle-free — it accepts an optional slice parameter and renders exactly the rows the paginator asked for. There is no recursive "if I overflow, start a new page" inside the renderer; pagination decisions are deterministic and centralised.

    +

    Edge cases handled#

    +
      +
    • Empty rows array — emits a header-only slice with caption (if any). No crash, no zero-height row.
    • +
    • Single row taller than a fresh page — emitted as a one-row slice; clipCells (existing v1.1.0 behaviour) handles vertical overflow inside the cell.
    • +
    • No room on current page even after pushing the table to start — paginator forces a new page and retries.
    • +
    +
    +

    Tagged-mode / PDF/UA#

    +

    When the document is built with tagged: true (or any explicit PDF/A mode), the table emits the following structure tree:

    +
    /Table
    +├── /Caption       (only when caption is present)
    +├── /TR  ← header
    +│   ├── /TH
    +│   ├── /TH
    +│   └── /TH
    +├── /TR  ← data row 1
    +│   ├── /TD
    +│   ├── /TD
    +│   └── /TD
    +└── /TR  ← data row N
    +    └── …
    +
    +

    The structure is single even when the table spans multiple pages. _paginateBlocks() shares a tableStructAccum array across all slices of the same table; the final slice commits it as { type: 'Table', children: tableStructAccum }. Each /TR carries the correct /StructParents for its page so screen readers reconstruct the logical reading order correctly (ISO 14289-1 §7.10.6).

    +

    Repeated headers in repeatHeader: true mode are not re-emitted in the structure tree — they are visual continuations only. The single /TR for the header sits at the top of the /Table element.

    +
    +

    Tagged-mode + zebra#

    +

    Zebra fills are decorative — they do not appear in the structure tree. PDF/UA conformance is preserved.

    +
    +

    ⚠️ PDF/A-1b note. PDF/A-1b forbids transparency (ISO 19005-1 §6.4). Zebra fills are opaque solid rectangles, so they are safe under PDF/A-1b, but avoid combining zebra with pdfa1b watermarks that rely on /ExtGState. Default tagged: true (PDF/A-2b) has no such restriction.

    +
    +
    +

    Migration from v1.1.0#

    +
    +

    One unconditional fix. Right- and centre-aligned bold header cells now use Helvetica-Bold metrics for width measurement (Adobe AFM), where pre-1.2.0 they were measured with Helvetica-Regular. This corrects a 2–5pt overshoot per cell that visually clipped the trailing glyph (e.g. the t in Amount). The fix shifts header glyph positioning by 2–5pt vs v1.1.0 — a genuine correctness improvement, not a regression. There is no opt-out.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    You want…Setting
    Exact byte-identical v1.1.0 multi-page body outputwrap: 'never', repeatHeader: false (header positioning still corrected)
    Modern default (recommended)Omit all new fields — defaults are correct.
    Invoice / report parity with commercial libswrap: 'auto', repeatHeader: true, zebra: true, caption: '…'
    Uniform row heights regardless of contentwrap: 'always', minRowHeight: 18
    Maximum information densitywrap: 'auto', cellPadding: 2, minRowHeight: 10
    +
    +

    Samples shipped#

    +

    Run npm run test:generate to produce:

    +
      +
    • test-output/document/table-wrap-auto.pdf — wrap-on-overflow demo
    • +
    • test-output/document/table-multipage-header-repeat.pdf — 60-row table across 2+ pages with repeated header
    • +
    • test-output/document/table-zebra-caption.pdf — zebra + caption + min row height
    • +
    • test-output/document/table-smart-autofit.pdfautoFit columns + wrap
    • +
    +

    Generator: scripts/generators/document-table-parity.ts.

    +
    +

    Reference#

    +
      +
    • ISO 32000-1:2008 §9 — text rendering and positioning.
    • +
    • ISO 14289-1:2014 §7.10.6 — tagged-PDF table structure (/Table, /TR, /TH, /TD, /Caption).
    • +
    • ISO 19005-2:2011 — PDF/A-2b conformance.
    • +
    +

    Internal contracts (for contributors)#

    +
      +
    • planTable() and TableSlice live in src/core/pdf-renderers.ts. They are not re-exported from the package root. Treat them as internal — they may change without a major bump as long as the public TableBlock contract is preserved.
    • +
    • Single-line row rendering uses rowTop - rowH + 3 baseline for data and rowTop - rowH + 4 for headers. These constants (CELL_PAD_BOTTOM, HEADER_PAD_BOTTOM) preserve byte-stability with v1.1.0 single-page output.
    • +
    • The default minRowHeight (12) and default header height (15) match v1.1.0's ROW_H and TH_H constants exactly.
    • +
    +
    +

    See also#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/text-extraction.html b/docs/guides/text-extraction.html index d621279..2d5c240 100644 --- a/docs/guides/text-extraction.html +++ b/docs/guides/text-extraction.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,164 @@

    Home  ›  Guides  ›  Text extraction

    -
    -

    Loading…

    - +
    + +

    Text extraction

    +
    +

    New in v1.6.0. Extract Unicode text from any PDF — including +encrypted ones — with extractText(): per-page reading-order text plus +optional positioned runs. Pure content-stream decoding, zero +dependencies, no OCR engine, no rasterisation.

    +
    +

    TL;DR#

    +
    import { extractText } from 'pdfnative';
    +import { readFileSync } from 'node:fs';
    +
    +const bytes = readFileSync('report.pdf');
    +
    +for (const page of extractText(bytes)) {
    +  console.log(`--- page ${page.pageIndex + 1} ---`);
    +  console.log(page.text);
    +}
    +
    +// Encrypted documents: pass the password (decryption is transparent).
    +const secret = extractText(readFileSync('locked.pdf'), { password: 'hunter2' });
    +
    +// Positioned runs for layout-aware consumers (search hit boxes, RAG chunking).
    +const [first] = extractText(bytes, { includeRuns: true, pages: [0] });
    +for (const run of first.runs ?? []) {
    +  console.log(run.text, `@ (${run.x.toFixed(1)}, ${run.y.toFixed(1)})`, run.fontSize);
    +}
    +
    +

    extractText(bytes, options?)#

    +

    Returns one ExtractedPageText per requested page, in ascending page order:

    +
    interface ExtractTextOptions {
    +  password?: string;        // for encrypted documents (delegated to openPdf)
    +  pages?: number[];         // 0-based page indices; default: all pages
    +  includeRuns?: boolean;    // also return positioned runs (default false)
    +  maxTextLength?: number;   // hard cap on total characters (default 16 000 000)
    +}
    +
    +interface ExtractedPageText {
    +  pageIndex: number;        // 0-based index in the source document
    +  text: string;             // reading-order text, '\n' between lines
    +  runs?: ExtractedTextRun[];// content-stream order, when includeRuns is true
    +}
    +
    +interface ExtractedTextRun {
    +  text: string;             // decoded text of one show operation
    +  x: number; y: number;     // device-space origin of the run (points)
    +  fontSize: number;         // effective size (Tf size scaled by Tm × CTM)
    +  fontName: string;         // font resource name, e.g. 'F1'
    +}
    +
    +

    How text is decoded#

    +

    For every shown string, the extractor resolves each character code in +this order:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SourceUsed forNotes
    /ToUnicode CMapany font that embeds onebfchar, bfrange (both destination forms), surrogate pairs
    /Encoding /Differencessimple fontsglyph names resolved through a compact AGL subset + uniXXXX / uXXXX[XX] patterns
    Base encodingsimple fontsWinAnsiEncoding (CP-1252) or MacRomanEncoding tables
    Fallbackno /Encoding at allWinAnsi heuristic (covers pdfnative's own base-14 output and most western PDFs)
    No mapping anywhereU+FFFD per code
    +

    Type0 (CID) fonts are decoded as 2-byte codes (Identity-H/Identity-V), +mapped through their /ToUnicode CMap — which pdfnative's own CIDFont +output always embeds, so pdfnative-generated documents round-trip +losslessly, including CJK, Arabic, and emoji.

    +

    Since pdfnative 1.7.0 the /ToUnicode coverage is complete across both +builders: base-14 font dictionaries are reached under tagged mode and the +AcroForm /Helv dictionary carries a CMap in every mode — so base-14 text in +tagged documents and text typed into form fields now extract cleanly too.

    +

    Reading order and runs#

    +

    page.text is assembled geometrically: runs are grouped into lines by +baseline (tolerance scales with font size), lines are sorted top→bottom, +runs within a line left→right, and a space is inserted where a visible +horizontal gap separates two runs. Large negative TJ kerning +adjustments (≤ −180/1000 em) are treated as word spaces.

    +

    page.runs (opt-in) preserves content-stream order — useful when you +need the emitter's sequence rather than the geometric one. Positions are +device-space points: Tm × CTM is tracked through q/Q/cm, so +rotated and scaled content reports its true placement. Text inside Form +XObjects is extracted too (Do recursion, depth-capped).

    +

    Encrypted documents#

    +

    extractText rides on openPdf: RC4, AES-128 and AES-256 documents +decrypt transparently with options.password. A missing or wrong +password throws PdfPasswordError; a document with an empty user +password opens without one.

    +

    Safety bounds#

    +

    Designed to be safe on untrusted input:

    +
      +
    • maxTextLength (default 16 M characters) hard-caps the total output — +exceeding it throws rather than exhausting memory.
    • +
    • Decompressed stream sizes are bounded by the parser's global inflate +cap (setMaxInflateOutputSize).
    • +
    • The interpreter is recursion-free with capped stacks (graphics state, +operands, CMap entries, Form-XObject depth) and never throws on +malformed content streams — unknown operators are skipped, unbalanced +BT/q tolerated, inline images (BI…EI) byte-skipped.
    • +
    +

    Limitations#

    +

    By design (documented, not bugs):

    +
      +
    • No OCR — image-only/scanned pages yield empty text.
    • +
    • Type3 fonts are decoded via their encoding/ToUnicode only (glyph +procedures are not interpreted).
    • +
    • Non-Identity CMap /Encodings (e.g. predefined UTF-16 CJK CMaps) are +decoded best-effort as 2-byte codes through /ToUnicode.
    • +
    • Vertical writing mode is treated as horizontal.
    • +
    • The structure tree / /ActualText is not consulted — order is +geometric, and ligature reversal is only as good as the embedded +/ToUnicode. In practice this means shaped Indic scripts +(Devanagari, Bengali, Tamil, Telugu, Sinhala, …) extract with U+FFFD +for conjunct/ligature glyphs that have no single-codepoint mapping — +the PDF renders correctly; only extraction of those clusters is +lossy. Latin, CJK, Arabic and emoji round-trip cleanly.
    • +
    +

    Samples#

    +

    Generated by npm run test:generate:

    +
      +
    • parser/text-extract-source.pdf — a rich source document.
    • +
    • parser/text-extract-report.pdf — its extraction rendered back to PDF +(text + positioned-run table).
    • +
    • parser/text-extract-encrypted-report.pdf — AES-256 encrypted source, +extracted with a password.
    • +
    +

    See also#

    + +
    diff --git a/docs/guides/troubleshooting.html b/docs/guides/troubleshooting.html index 7787f7b..e128094 100644 --- a/docs/guides/troubleshooting.html +++ b/docs/guides/troubleshooting.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,114 @@

    Home  ›  Guides  ›  Troubleshooting

    -
    -

    Loading…

    - +
    + +

    Troubleshooting

    +

    Font Not Rendering (Boxes or Blank)#

    +

    Symptom: Non-Latin text shows as empty rectangles or missing glyphs.

    +

    Cause: The font for that script is not registered or not loaded.

    +

    Fix:

    +
    import { registerFonts, loadFontData } from 'pdfnative';
    +
    +// 1. Register font loaders (lazy — no data loaded yet)
    +registerFonts({
    +  th: () => import('pdfnative/fonts/noto-thai-data.js'),
    +  ar: () => import('pdfnative/fonts/noto-arabic-data.js'),
    +});
    +
    +// 2. Load font data when needed
    +const thaiFont = await loadFontData('th');
    +
    +// 3. Pass fontEntries to your builder
    +const pdf = buildDocumentPDFBytes({
    +  blocks: [{ type: 'paragraph', text: 'สวัสดี' }],
    +  fontEntries: [{ fontData: thaiFont!, fontRef: '/F3', lang: 'th' }],
    +});
    +
    +

    PDF File Too Large#

    +

    Symptom: Output PDF is unexpectedly large (>1 MB for text content).

    +

    Causes & fixes:

    +
      +
    1. Enable compression:

      +
      buildPDFBytes(params, { compress: true });
      +
      +

      FlateDecode typically reduces size by 50–90%.

      +
    2. +
    3. Initialize native compression (Node.js):

      +
      import { initNodeCompression } from 'pdfnative';
      +await initNodeCompression();
      +
      +

      Without this, a stored-block fallback is used (minimal compression).

      +
    4. +
    5. Large images: JPEG is already compressed (DCTDecode). For PNG, the raw pixels are FlateDecode-compressed. Use JPEG for photos.

      +
    6. +
    7. Font subsetting: Automatic — only used glyphs are embedded. If you're embedding many scripts, each adds a font subset.

      +
    8. +
    +

    RTL Text Backwards#

    +

    Symptom: Arabic or Hebrew text appears in logical order instead of visual (right-to-left) order.

    +

    Cause: Font entries must include the correct lang property for BiDi detection.

    +

    Fix:

    +
    const fontEntries = [
    +  { fontData: arabicFont, fontRef: '/F3', lang: 'ar' },
    +  { fontData: hebrewFont, fontRef: '/F4', lang: 'he' },
    +];
    +
    +

    The lang property triggers:

    +
      +
    • BiDi run detection (containsRTL())
    • +
    • Arabic positional shaping (GSUB forms)
    • +
    • Glyph mirroring for brackets and other paired delimiters (the full 428-pair +Unicode BidiMirroring table since v1.7.0, per UAX #9 rule L4)
    • +
    +

    Reversed digits or backwards parentheses in RTL text? Fixed in v1.7.0 with +no API change: digit runs now take even embedding levels (UAX #9 rules I1/I2), +so a number like 1405 keeps its digit order instead of rendering 5041, and +rule L4 mirrors delimiters during reversal, so a logical (X) no longer +renders )X(. If you see either symptom, upgrade pdfnative.

    +

    PDF/A Validation Fails#

    +

    Symptom: veraPDF reports non-conformance.

    +

    Common issues:

    +
      +
    1. Missing tagged mode:

      +
      buildPDFBytes(params, { tagged: true }); // PDF/A-2b
      +// or
      +buildPDFBytes(params, { tagged: 'pdfa1b' }); // PDF/A-1b
      +
      +
    2. +
    3. PDF/A + encryption conflict: ISO 19005-1 §6.3.2 forbids encryption in PDF/A. Use one or the other.

      +
    4. +
    5. Transparency in PDF/A-1b: Watermarks with opacity < 1.0 are blocked in PDF/A-1b (ISO 19005-1 §6.4). Use PDF/A-2b or remove transparency.

      +
    6. +
    +

    "Document has too many blocks" / Very Large Documents#

    +

    Symptom: Generating a multi-thousand-page report throws a "too many blocks" error.

    +

    Cause: The document builder caps the number of content blocks as a safety rail. Before v1.3.0 this was a hard-coded 10 000. As of v1.3.0 the default is 100 000 and is configurable.

    +

    Fix: raise the limit via the layout option:

    +
    buildDocumentPDFBytes(params, { maxBlocks: 500000 });
    +// also honoured by the streaming builders:
    +buildDocumentPDFStream(params, { maxBlocks: 500000 }, { chunkSize: 65536 });
    +
    +

    For very large outputs, prefer the streaming builders (buildDocumentPDFStream, or buildDocumentPDFStreamTrue for constant memory) so the full binary never has to sit in memory at once.

    +

    Parser Fails on External PDF#

    +

    Symptom: openPdf() throws on a PDF file not generated by pdfnative.

    +

    Common causes:

    +
      +
    1. Encrypted PDF opened without a password: Since v1.6.0 the parser decrypts the Standard Security Handler (RC4 V1-V4, AES-128, AES-256 R6) — but it needs the password. Call openPdf(bytes, { password }). Without one you get PdfPasswordError; with an algorithm the handler does not cover, PdfEncryptionUnsupportedError.
    2. +
    3. Linearized PDF: The parser follows standard xref/trailer. Linearized hint tables may cause offset issues.
    4. +
    5. Non-standard formatting: Some PDF generators produce non-compliant output. The parser follows ISO 32000-1 strictly.
    6. +
    +

    Build / Import Issues#

    +

    ESM import paths:

    +
    // Bare package import — no extension involved
    +import { buildPDFBytes } from 'pdfnative';
    +
    +// Font data modules — the .js extension IS required here (ESM subpath),
    +// and the modules only have named exports, so use a namespace import:
    +import * as thaiData from 'pdfnative/fonts/noto-thai-data.js';
    +
    +

    Browser vs Node.js: The library works in both environments. For compression in Node.js, call initNodeCompression() once at startup.

    +
    diff --git a/docs/guides/viewer-preferences.html b/docs/guides/viewer-preferences.html index c02203a..830bc32 100644 --- a/docs/guides/viewer-preferences.html +++ b/docs/guides/viewer-preferences.html @@ -1,85 +1,256 @@ - - - - - - Viewer preferences — pdfnative - - - + + + + + + Viewer preferences — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Viewer preferences

    - -
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Viewer preferences

    +
    + +

    Viewer preferences

    +
    +

    New in v1.4.0. Control how a conforming reader presents your PDF the +moment it opens — initial page layout, the bookmark/thumbnail panel, full-screen +kiosk mode, window fit/centering, UI-chrome visibility, reading direction, and +print scaling. Set via DocumentParams.layout.viewerPreferences. Purely +presentational, PDF/A-safe, and zero overhead when unused.

    +
    +

    TL;DR#

    +
    import { buildDocumentPDFBytes } from 'pdfnative';
    +import type { ViewerPreferences } from 'pdfnative';
    +
    +const viewerPreferences: ViewerPreferences = {
    +  pageLayout: 'twoColumnLeft',  // continuous two-up, odd pages left
    +  pageMode: 'useOutlines',      // open the bookmark panel
    +  displayDocTitle: true,        // titlebar shows /Info /Title, not the filename
    +  fitWindow: true,
    +  centerWindow: true,
    +};
    +
    +const bytes = buildDocumentPDFBytes({
    +  title: 'Annual Report',
    +  blocks: [/* … */],
    +  outline: 'auto',
    +  layout: { viewerPreferences },
    +});
    +
    +

    Page layout (/PageLayout)#

    +

    How pages are arranged in the viewport:

    + + + + + + + + + + + + + + + + + + + + + + + +
    ValueBehaviour
    singlePageOne page at a time
    oneColumnContinuous single column
    twoColumnLeft / twoColumnRightContinuous two columns; odd pages on the left / right
    twoPageLeft / twoPageRightTwo pages at a time; odd pages on the left / right
    +

    Page mode (/PageMode)#

    +

    Which panel (if any) is open and whether the document opens full-screen:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueBehaviour
    useNoneNeither bookmarks nor thumbnails
    useOutlinesBookmark panel open
    useThumbsThumbnail panel open
    fullScreenFull-screen presentation, no menu/panel
    useOCOptional-content (layers) panel
    useAttachmentsAttachments panel
    +

    An explicit pageMode overrides the /UseOutlines default that a document +automatically gets when it has an outline.

    +

    ViewerPreferences flags#

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldEffect
    hideToolbarHide the reader tool bars
    hideMenubarHide the menu bar
    hideWindowUIHide scrollbars / navigation, leaving only the page
    fitWindowResize the window to the first page
    centerWindowCentre the window on screen
    displayDocTitleShow /Info /Title in the titlebar instead of the filename
    nonFullScreenPageModeMode to use after exiting full-screen
    direction'l2r' (default) or 'r2l' reading order
    printScaling'none' or 'appDefault' print-dialog default
    duplex'simplex', 'duplexFlipShortEdge' or 'duplexFlipLongEdge' print-dialog default (v1.7.0)
    pickTrayByPDFSizeSelect the paper tray from the PDF page size (v1.7.0)
    printPageRangeDefault print range as 1-based [from, to] pairs (v1.7.0)
    numCopiesDefault copy count in the print dialog (v1.7.0)
    +

    The four print-dialog defaults ship with v1.7.0's print-production support — +see the print production guide for bleed/trim boxes, printer's +marks and /Trapped.

    +

    Full-screen kiosk#

    +
    const viewerPreferences: ViewerPreferences = {
    +  pageMode: 'fullScreen',
    +  nonFullScreenPageMode: 'useThumbs',
    +  hideToolbar: true,
    +  hideMenubar: true,
    +  direction: 'r2l',
    +  printScaling: 'none',
    +};
    +
    +

    PDF/A note#

    +

    Viewer preferences are metadata only — no transparency, no scripting — so they +are safe to combine with any PDF/A conformance level. Sample generators: +viewer-prefs-showcase.ts.

    + +
    +
    + + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html index b878148..a39f76d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -30,6 +30,7 @@ + diff --git a/docs/learn/index.html b/docs/learn/index.html index 23c2db1..5cc79af 100644 --- a/docs/learn/index.html +++ b/docs/learn/index.html @@ -8,6 +8,7 @@ + diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 15fc4e0..7049627 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -58,12 +58,1502 @@ - [scripts/generators/](https://github.com/Nizoka/pdfnative/tree/main/scripts/generators): 48 sample generators producing 242 reference PDFs. - [tests/](https://github.com/Nizoka/pdfnative/tree/main/tests): 2665+ tests across 122 files. 95%+ statements measured at v1.6.0; CI enforces ≥88%. +## Machine-readable artefacts + +- [llms-full.txt](https://pdfnative.dev/llms-full.txt): This index plus the README and the full Markdown source of every guide, in one request. +- [llms-index.json](https://pdfnative.dev/llms-index.json): Per-page machine index — HTML and raw-Markdown URL, title, summary, section anchors, and exact byte / approximate token sizes for every artefact, so you can decide what to fetch before spending the tokens. +- Every guide serves its raw Markdown at the same URL with `.md` instead of `.html`, declared via `` in each page head. All of these URLs are listed in the sitemap. + ## Ecosystem - [pdfnative-cli](https://github.com/Nizoka/pdfnative-cli): CLI wrapper for pdfnative (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, semver-accepts 1.7.0) — render, fill, annotate, merge, split, extract, sign, verify, encrypt, decrypt, inspect, extract-text, batch, doctor, schema, completion, govern. - [pdfnative-mcp](https://github.com/Nizoka/pdfnative-mcp): Model Context Protocol server exposing pdfnative as 28 AI tools (v1.6.0, built on pdfnative 1.7.0). - [pdfnative-react](https://github.com/Nizoka/pdfnative-react): React renderer — declarative JSX components compiled on-device to pdfnative blocks (v1.1.0, React 19 + pdfnative ^1.6.0 peers). +--- + +# pdfnative + +[![CI](https://github.com/Nizoka/pdfnative/actions/workflows/ci.yml/badge.svg)](https://github.com/Nizoka/pdfnative/actions/workflows/ci.yml) +[![CodeQL](https://github.com/Nizoka/pdfnative/actions/workflows/codeql.yml/badge.svg)](https://github.com/Nizoka/pdfnative/actions/workflows/codeql.yml) +[![npm version](https://img.shields.io/npm/v/pdfnative)](https://www.npmjs.com/package/pdfnative) +[![npm downloads](https://img.shields.io/npm/dm/pdfnative)](https://www.npmjs.com/package/pdfnative) +[![bundle size](https://img.shields.io/bundlephobia/minzip/pdfnative)](https://bundlephobia.com/package/pdfnative) +[![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/pdfnative) +[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![npm provenance](https://img.shields.io/badge/provenance-signed-blueviolet)](https://docs.npmjs.com/generating-provenance-statements) +[![website](https://img.shields.io/badge/pdfnative.dev-0066FF?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxyZWN0IHg9IjMiIHk9IjIiIHdpZHRoPSIxNCIgaGVpZ2h0PSIxOCIgcng9IjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTcgN2g2TTcgMTFoOE03IDE1aDQiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=)](https://pdfnative.dev) +[![pdfnative-mcp](https://img.shields.io/npm/v/pdfnative-mcp?label=pdfnative-mcp&color=6366f1)](https://www.npmjs.com/package/pdfnative-mcp) +[![pdfnative-cli](https://img.shields.io/npm/v/pdfnative-cli?label=pdfnative-cli&color=0e7490)](https://www.npmjs.com/package/pdfnative-cli) +[![pdfnative-react](https://img.shields.io/npm/v/pdfnative-react?label=pdfnative-react&color=06b6d4)](https://www.npmjs.com/package/pdfnative-react) + +Pure native PDF generation library — zero vendor dependencies. Conforms to ISO 32000-1 (PDF 1.7). + +## Ecosystem + +pdfnative ships as four coordinated packages — pick whichever entry point fits your workflow: + +| Package | Latest | Use it for | +|---|:---:|---| +| [`pdfnative`](https://www.npmjs.com/package/pdfnative) | **v1.7.0** | The library itself — call from Node, browsers, Workers, Deno, Bun. | +| [`pdfnative-cli`](https://www.npmjs.com/package/pdfnative-cli) | **v1.3.0** | Render JSON → PDF, sign (RSA + ECDSA-SHA256, native constant-time crypto by default), inspect, verify (PAdES-T + OCSP/CRL), **merge / split / extract** pages, **annotate** (markup annotations), **govern** (AI-governance / HITL gate), batch, and emit JSON Schemas from the shell. Pins pdfnative `^1.6.0` (semver-accepts 1.7.0): 22 scripts + COLRv1 emoji, `--font math`, PDF bookmarks (`--outline`), layout introspection (`--inspect-layout` / `--debug-layout`), and an agent-native `--json`/`E_*`/`--dry-run`/`--summary` contract. | +| [`pdfnative-mcp`](https://www.npmjs.com/package/pdfnative-mcp) | **v1.6.0** | Use pdfnative from Claude Desktop, Cursor, Continue, Zed (or any MCP client, stdio or HTTP) — **28 production tools** spanning the engine's full document model: all 13 block kinds in `generate_basic_pdf`, layout options, build-time `encrypt` that keeps the AcroForm, image watermarks, print production (page boxes, bleed, marks, `/UserUnit`, OutputIntent), charts v2 (stacked / area / scatter, dual axis, log & time scales), the complete PAdES ladder B-B → B-LTA (`sign_pdf` with RFC 3161 timestamps, `add_ltv`, `timestamp_pdf`, `verify_pdf ltv: true`), `update_metadata`, the read-only `inspect_layout` pagination preview, honest PDF/A (`embedFonts` / `strict` / diagnostics), six MCP prompts, token-frugal read modes (`verbosity` / `fields`), the network-free `draft_governance_issue` (AI-governance / HITL), and the MCP **2026-07-28** spec with automatic legacy fallback. No outbound request by default — the only permitted egress is operator-configured TSA / OCSP / CRL. Pins pdfnative `^1.7.0`. | +| [`pdfnative-react`](https://www.npmjs.com/package/pdfnative-react) | **v1.1.0** | Write PDFs as declarative JSX — ``, ``, ``, ``, ``, ``… compiled on-device to pdfnative blocks by a custom React 19 reconciler. Render functions (`renderToBytes` / `renderToStream` / `renderToFile`), client hooks & components (`usePdf`, `PDFViewer`, `PDFDownloadLink`), and a versioned `DocSpec` grammar (`docSpecSchema()`) for AI agents. Peer: pdfnative ^1.6.0, React ^19.0.0, Node ≥22. (A 1.5 engine would silently drop the new `` block.) | + +```bash +npm install pdfnative # library +npm install pdfnative pdfnative-react react # React renderer (pdfnative + React 19 peers) +npm install -g pdfnative-cli # CLI +npm install -g pdfnative-mcp # MCP server +``` + +Detailed docs: [CLI guide](docs/guides/cli.md) · [MCP guide](docs/guides/mcp.md) · [React guide](docs/guides/react.md) · [Onboarding cheatsheet](docs/guides/onboarding.md). + +## Highlights + +- **Zero dependencies** — built from scratch in pure TypeScript. Zero runtime dependencies, tree-shakeable, auditable +- **Conforms to ISO 32000-1** — valid xref tables, /Info metadata, proper font embedding +- **22 Unicode scripts** — Thai, Japanese, Chinese (SC), Korean, Greek, Devanagari, Telugu, Turkish, Vietnamese, Polish, Arabic, Hebrew, Cyrillic, Georgian, Armenian, Bengali, Tamil, Sinhala, Tibetan, Khmer, Myanmar, Ethiopic +- **Thai OpenType shaping** — GSUB substitution + GPOS mark-to-base + mark-to-mark positioning +- **Arabic positional shaping** — GSUB isolated/initial/medial/final forms + lam-alef ligatures +- **BiDi text layout** — Unicode Bidirectional Algorithm (UAX #9) with isolates (LRI/RLI/FSI/PDI) and explicit embeddings (LRE/RLE/LRO/RLO/PDF) including character-level X4–X5 overrides (v1.3.0). **v1.7.0** implements I1/I2 even embedding levels so digit runs (European, Arabic-Indic, Extended Arabic-Indic) keep logical order in RTL text, and full rule-L4 glyph mirroring through the complete 428-pair `BidiMirroring.txt` table +- **USE-lite shaping** — `classifyUseCategory` / `classifyClusters` drive joiner classification across the Devanagari, Bengali, and Tamil shapers, fixing nukta+virama, half-form, eyelash-ra, and ya-phalaa edge cases (v1.3.0) +- **Colour emoji (COLRv1)** — opt-in Noto Color Emoji subset (**expanded to 1167 glyphs in v1.6.0**, ~4.5 MB with the v1.7.0 sequence ligatures, incl. the complete Transport & Map block); solid + linear + radial gradient layers rendered as native PDF Form XObjects; monochrome fallback when not registered (v1.3.0). Variation selectors, ZWJ/ZWNJ, and skin-tone modifiers no longer leave tofu, and glyph `/BBox` is computed from contour bounds so emoji are never clipped (v1.3.0). **Advanced compositing** (v1.4.0): COLRv1 sweep (conic) gradients render as native flat-shaded wedges, and `PaintComposite` separable blend modes (Multiply, Screen, Overlay, Darken, Lighten, …) map to PDF `/BM` ExtGState; structural Porter-Duff modes fall back to monochrome. **Flag & ZWJ sequences** (v1.7.0): 51 flags + 22 ZWJ sequences (families, professions, rainbow/pirate flags, …) resolve through the source font's GSUB into single colour ligature glyphs with a longest-match pre-pass — both VS-16 spellings match, uncovered sequences degrade to the historical per-codepoint rendering, and the CLI bundles any custom set (`--sequences`, `--sequence-list`, skin tones included). [Guide →](docs/guides/colour-emoji.md) +- **Multi-font fallback** — automatic cross-script font switching with continuation bias +- **TTF subsetting** — only used glyphs embedded (dramatic file size reduction) +- **Tagged PDF / PDF/A** — structure tree, /ActualText, XMP metadata, sRGB OutputIntent (PDF/A-1b, 2b, 2u, 3b with embedded file attachments). **Conformance guards** (v1.7.0): configurations that would break the declared PDF/A level (no embedded fonts, DeviceCMYK images) surface a diagnostic — `console.warn` by default, a custom `onDiagnostic` sink, or a thrown error under `strict: true`. [Guide →](docs/guides/pdfa.md) +- **PDF Encryption (round-trip)** — **write** AES-128 (V4/R4) and AES-256 (V5/R6) encrypted PDFs (RC4 is never emitted), and **read** all of AES-128, AES-256, and legacy RC4 (V1–V4); owner + user passwords, granular permissions. **v1.6.0** adds a Standard Security Handler **decryptor** — `openPdf(bytes, { password })` decrypts transparently, the merge/split API ingests encrypted sources **and re-encrypts its output** (`MergeOptions.encrypt`, AES only, fresh keys) — closing the full *open → edit → re-secure* round trip. [Guide →](docs/guides/pdf-manipulation.md) +- **Native vector charts** (v1.6.0, **charts v2 in v1.7.0**) — bar, horizontal-bar, line, pie, donut, **stacked bars, area, and scatter** `chart` blocks rendered as pure PDF path operators (zero deps, no rasterisation); multi-series, legends, "nice" axis ticks, negative values, tagged `/Figure` + alt text. v1.7.0 adds a **secondary right axis**, **log and UTC-deterministic time scales**, **per-point data labels**, and x-label collision handling (automatic stride + `labelStride` / `labelRotation`). [Guide →](docs/guides/charts.md) +- **Text extraction** (v1.6.0) — `extractText()` decodes page content streams into per-page reading-order Unicode text plus optional positioned runs; `/ToUnicode` CMap, `/Encoding /Differences`, and WinAnsi/MacRoman decoding; works on encrypted documents (`{ password }`); hard `maxTextLength` memory cap for untrusted input. [Guide →](docs/guides/text-extraction.md) +- **Free-form document builder** — headings, paragraphs, lists (incl. **nested / hierarchical** bullet & numbered lists, v1.4.0), tables, images, barcodes, SVG paths, form fields, spacers, page breaks, table of contents. Configurable block limit via `layout.maxBlocks` (default 100 000) for very large reports (v1.3.0) +- **Smart tables** — multi-page slicing with repeated headers, auto-wrap on column overflow, zebra striping, captions, and smart auto-fit columns (v1.2.0), plus per-cell **borders** (`cellBorders`) and **vertical alignment** (`cellVAlign` / `ColumnDef.vAlign`, v1.4.0). [Guide →](docs/guides/tables.md) +- **Barcode & QR code generation** — Code 128, EAN-13, QR Code, Data Matrix, PDF417 — pure PDF path operators (no images) +- **SVG rendering** — path, rect, circle, ellipse, line, polyline, polygon as native PDF operators, plus `` elements rendered as upright PDF text with `x`/`y` positioning and `text-anchor` (start/middle/end) support (v1.5.0) +- **AcroForm fields** — text, multiline, checkbox, radio, dropdown, listbox with appearance streams (ISO 32000-1 §12.7). **v1.6.0** adds **fill & flatten of existing forms**: `readFormFields()`, `fillForm()` (regenerates appearances), and `flattenForm()` — non-destructive incremental update that preserves prior signatures, **including on encrypted documents** (appended objects are encrypted under the document's existing scheme). [Guide →](docs/guides/form-filling.md) +- **Digital signatures** — CMS/PKCS#7 detached signatures with RSA (SHA-256/384/512, v1.7.0) + ECDSA-SHA256 (P-256) and X.509 parsing (ISO 32000-1 §12.8). One-call placeholder injection via `addSignaturePlaceholder()` (v1.2.0). Pluggable **native crypto provider** (`setCryptoProvider()` / `PdfSignOptions.provider`, v1.4.0) for constant-time, hardware-backed signing (`node:crypto` / Web Crypto / HSM). **v1.7.0**: PAdES baseline profile (`profile: 'pades'` — ESS signing-certificate-v2, `ETSI.CAdES.detached`), **multiple signatures** (`allowMultiple` + `fieldName` selector), and `listSignatures()` inspection. [Guide →](docs/guides/signatures.md) +- **Print production** (v1.7.0) — bleed/trim/art/crop **page boxes** (`layout.print`, with a one-line `bleed` shorthand), **crop & registration marks** drawn as pure vector operators outside the TrimBox, `/Trapped` metadata with XMP parity, print-dialog defaults (**duplex**, tray pick, page range, copies), caller-supplied **OutputIntent ICC profile** (tagged mode), and large-format `/UserUnit` (banners, plans). Boxes survive `mergePdfs`/`splitPdf`. Byte-identical output when unused. [Guide →](docs/guides/print.md) +- **Long-term validation (LTV, PAdES B-B → B-LTA)** (v1.7.0) — RFC 3161 **signature timestamps** (`signPdfBytesWithTimestamp()`), embedded revocation material in `/DSS` + per-signature `/VRI` (`addValidationInfo()` — OCSP RFC 6960 + CRL RFC 5280), and **document timestamps** (`addDocumentTimestamp()`). Network transport is injected (`TimestampProvider` / `RevocationProvider`) — the engine stays offline and zero-dependency; rejected or tampered TSA tokens are never embedded. [Guide →](docs/guides/ltv.md) +- **Streaming output** — AsyncGenerator-based progressive PDF emission with configurable chunk size, object-boundary page-by-page streaming, and **true constant-memory streaming** (`buildDocumentPDFStreamTrue()`, v1.3.0) where the full PDF binary never materialises. One-call `streamToFile()` drains any stream to disk with back-pressure and `AbortSignal` support (v1.4.0). [Guide →](docs/guides/streaming.md) +- **Document outline & page labels** — nested bookmarks (`/Outlines` tree, with bold/italic/colour, collapsible nodes via `open: false`, explicit or `outline: 'auto'` from headings) and logical page numbering (`/PageLabels`: decimal, roman, alpha, prefixes, custom start) (v1.4.0). [Guide →](docs/guides/outlines.md) +- **Viewer preferences** — `PdfLayoutOptions.viewerPreferences` controls initial `/PageLayout` & `/PageMode` plus the `/ViewerPreferences` dict (hide toolbar/menubar, fit/center window, display doc title, non-full-screen mode, reading direction, print scaling) — PDF/A-safe (v1.4.0). **v1.7.0** adds the print-dialog defaults: `duplex`, `pickTrayByPDFSize`, `printPageRange`, `numCopies`. [Guide →](docs/guides/viewer-preferences.md) +- **Font-data validator** — opt-in `validateFontData()` structurally checks custom font modules (SFNT magic, base64 integrity, cmap coverage, glyph-id range, width array, finite metrics) and returns `{ valid, errors, warnings }` (v1.4.0). [Guide →](docs/guides/font-validation.md) +- **PDF parser & modifier** — read existing PDFs (tokenizer, xref, object parser, FlateDecode inflate) + incremental modification. Read-only PDF/UA structural checker `validatePdfUA()` (ISO 14289-1: MarkInfo, StructTree, ParentTree, Lang, per-page MCID uniqueness) (v1.3.0). **Page-tree manipulation** (v1.4.0): `mergePdfs()`, `splitPdf()`, `extractPages()` rebuild a clean object graph (inherited attributes resolved, annotations/signatures optionally dropped, deterministic trailer `/ID`, bounded-depth copy, 256 MiB output cap via `maxOutputSize`). **Round-trip readers** (v1.5.0): `getPageLabels()` parses `/PageLabels` back into a typed `PageLabelRange[]`; `getAnnotations()` / `getPageRef()` read page annotations, and `PdfModifier.addAnnotation()` injects new ones incrementally. **v1.6.0**: a Standard Security Handler **decryptor** (`openPdf(bytes, { password })`, RC4/AES-128/AES-256) lets the reader and page-tree API ingest encrypted sources, and **constant-memory streaming** variants `streamMergedPdfs()` / `streamSplitPdf()` / `streamExtractPages()` emit merges/splits in fixed-size chunks (byte-identical to the buffered functions). [Guide →](docs/guides/pdf-manipulation.md) +- **Markup annotations** — typed annotation model (text, highlight, underline, strikeout, squiggly, square, circle, line, freetext) via `buildAnnotation()` / `buildAnnotationBody()`, plus `PdfReader.getAnnotations()` and `PdfModifier.addAnnotation()` for round-trip read/write (v1.5.0); **v1.6.0** lets `addAnnotation()` operate on encrypted documents (RC4/AES sources, annotations stored encrypted). [Guide →](docs/guides/annotations.md) +- **Layout debug & inspection** — opt-in `layout: { debug: true }` overlays margin / content / cell boxes for visual layout debugging; `inspectDocumentLayout()` returns a programmatic per-page block-geometry report. Byte-identical when debug is off (v1.5.0). [Guide →](docs/guides/debugging.md) +- **Math & technical symbols** — bundleable math font under lang `'math'`; mathematical operators, Greek, arrows, and technical symbols route automatically via script detection (v1.5.0) +- **Font-data tooling** — `pdfnative/tools` exposes `compileFontData()` / `parseFontData()` to build and introspect font-data modules programmatically (v1.5.0) +- **Image embedding** — JPEG (DCTDecode) and PNG (FlateDecode) with auto-scaling and alignment +- **Hyperlinks** — PDF link annotations (/URI) with URL validation, blue underlined text, tagged /Link +- **Header/footer templates** — configurable `PageTemplate` with left/center/right zones and `{page}`/`{pages}`/`{date}`/`{title}` placeholders +- **Watermarks** — text and image overlays with configurable opacity, rotation, and position (background/foreground) +- **Table of contents** — auto-generated TOC with internal /GoTo links, dot leaders, and page numbers +- **FlateDecode compression** — zlib stream compression (50–90% size reduction), zero-dependency, platform-native +- **Web Worker support** — off-main-thread generation for large datasets +- **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations +- **Heavily tested** — 2665+ tests across 122 files, fuzz suite, dual-mode visual-regression suite, performance benchmarks; 95.41% statement coverage measured at the v1.6.0 release, with CI enforcing ≥88% statements / 80% branches / 85% functions / 90% lines (vitest.config.ts) +- **NPM provenance** — signed builds via GitHub Actions OIDC +- **On-device generation** — runs in Node, browsers, Workers, Deno, Bun. No SaaS round-trip; documents never leave the calling process unless your application explicitly sends them +- **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home +- **AI client integration** — use pdfnative from Claude Desktop, Cursor, Continue, and Zed via [`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) — **28 production tools** (generate, tables, barcodes, forms, sign with the full PAdES ladder, verify, validate, attachments, extraction, inspect, metadata, layout preview, plus page-tree `merge_pdfs` / `split_pdf` / `extract_pages`) +- **Command-line interface** — render, sign, verify, inspect, and batch-render PDFs from the shell with [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) — zero-config, scriptable, agent-native (`--json`/`E_*`/`--dry-run`), ideal for CI/CD pipelines +- **React renderer** — author PDFs as declarative JSX with [`pdfnative-react`](https://github.com/Nizoka/pdfnative-react): ``/`
    `/`` components, `usePdf`/`PDFViewer` client hooks, on-device rendering with no DOM or headless browser + +## Installation + +```bash +npm install pdfnative +``` + +**Requirements:** Node.js >= 22 | Modern browsers | Deno | Bun + +## Documentation + +- 🌐 **Website:** [pdfnative.dev](https://pdfnative.dev) — landing page, live in-browser demo with 12 examples, comparisons, benchmarks. +- 📘 **Quick Start:** [docs/guides/quickstart.md](docs/guides/quickstart.md) — Node.js, browser, Web Worker, streaming. +- 🏛️ **Architecture:** [docs/guides/architecture.md](docs/guides/architecture.md) — modules, builders, generation pipeline. +- ♿ **Accessibility:** [docs/guides/accessibility.md](docs/guides/accessibility.md) — tagged PDF, PDF/UA, PDF/A. +- ❓ **FAQ:** [docs/guides/faq.md](docs/guides/faq.md) — fonts, encryption, signatures, comparisons. +- 🤖 **Agentic workflows:** [docs/guides/agentic-workflows.md](docs/guides/agentic-workflows.md) — extend the engine at runtime (register fonts without a release) and embed agent-generated images. +- 🛠️ **Troubleshooting:** [docs/guides/troubleshooting.md](docs/guides/troubleshooting.md) — common pitfalls. +- 🎮 **Playgrounds:** nine interactive demos at [docs/playgrounds/](docs/playgrounds/) — [extreme-scripts](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests), [all-scripts](docs/playgrounds/all-scripts.html) (every Unicode script), [scale](docs/playgrounds/scale.html) (1k-100k pages, Web Worker + true streaming), [authoring-plus](docs/playgrounds/authoring-plus.html) (SVG text, layout overlay, annotations), [toolkit](docs/playgrounds/toolkit.html) (bookmarks, page labels, viewer prefs, merge/split/extract, and v1.6.0 form fill/flatten), [charts](docs/playgrounds/charts.html) (native vector charts — all 9 v1.7.0 types), plus [cli](docs/playgrounds/cli.html), [mcp](docs/playgrounds/mcp.html) and [react](docs/playgrounds/react.html) ecosystem explorers. +- 🧪 **Sample PDFs:** [scripts/generators/](scripts/generators/) — 242 sample PDFs across 37 categories (48 generators) (see [Sample PDFs](#sample-pdfs) below). +- 🎓 **Learn:** [docs/learn/](docs/learn/) — an eight-step guided path from installing the package to generating 100,000 pages, live at [pdfnative.dev/learn/](https://pdfnative.dev/learn/). +- 🤖 **Machine-readable:** [llms.txt](llms.txt) (doc index for LLMs), [AGENTS.md](AGENTS.md) (agent guidance), and [docs/assets/ecosystem.json](docs/assets/ecosystem.json) — the canonical manifest of ecosystem versions and counts, enforced by `npm run verify:docs`. + +## Why pdfnative? + +pdfnative was designed for teams that need **ISO-compliant, production-grade PDF generation** with zero supply-chain risk. Here is how it compares to other popular JavaScript PDF libraries: + +| Feature | pdfnative | jsPDF | pdfkit | pdf-lib | pdfmake | +|---------|:---------:|:-----:|:------:|:-------:|:------:| +| Runtime dependencies | **0** | 3 | 6 | 4 | 3 | +| TypeScript declarations | Built-in | Built-in | @types/* | Built-in | @types/* | +| PDF/A (ISO 19005) | 1b, 2b, 2u, 3b | — | — | — | — | +| Tagged PDF / PDF/UA | ✅ | — | ✅ | — | — | +| Encryption (read + write) | AES-128/256 + RC4 read | write (RC4) | write | — | write | +| Complex text shaping (GSUB/GPOS) | ✅ Thai, Arabic, Devanagari, Bengali, Tamil | — | Via fontkit | Via @pdf-lib/fontkit | Via pdfkit | +| BiDi (RTL) layout | ✅ | — | — | — | — | +| Modify existing PDFs | ✅ (incremental) | — | — | ✅ | — | +| Forms (create + fill + flatten) | ✅ all three | create | create | create + fill | — | +| Native charts (vector) | ✅ 9 kinds incl. stacked/area/scatter | — | — | — | — | +| Digital signatures | ✅ (RSA + ECDSA, PAdES) | — | — | — | — | +| LTV / timestamps (PAdES B-LTA) | ✅ RFC 3161 + /DSS | — | — | — | — | +| Print production (bleed/trim boxes, marks) | ✅ | — | — | — | — | +| Barcode / QR code (native) | ✅ 5 formats | — | — | — | QR | +| SVG path rendering | ✅ | Plugin | ✅ | Paths only | ✅ | +| Streaming output | ✅ | — | ✅ | — | ✅ | +| PDF parser | ✅ | — | — | ✅ | — | +| Tree-shakeable (ESM) | ✅ | — | — | ✅ | — | +| NPM provenance (SLSA) | ✅ | — | — | — | ✅ | + +> **Data sources:** npm registry metadata and official README/documentation for each library, compared on **2026-07-28** (pdfkit 0.19.1, jsPDF 4.2.1, pdfmake 0.3.11, pdf-lib 1.17.1 — see [docs/data/comparison-2026-07-28.json](docs/data/comparison-2026-07-28.json)). Dependency counts reflect direct `dependencies` listed in each package's published manifest. "—" means the feature is not supported or not documented. Feature claims about third-party libraries are based on their public documentation and may not reflect the latest version — please verify against current releases. Sample PDFs validate with veraPDF (PDF/A) and Adobe Acrobat. + +**When to choose another library:** You need advanced vector graphics (complex gradients, arbitrary transforms), rich interactive form scripting (JavaScript actions), or mature ecosystem integrations with existing toolchains. + +**When to choose pdfnative:** You need zero-dependency PDF generation with ISO archival compliance (PDF/A), accessibility (tagged PDF), AES encryption, digital signatures, multi-script Unicode support — particularly Arabic/Hebrew BiDi and Thai GSUB/GPOS shaping — form fields, barcode generation, SVG rendering, or the ability to parse and incrementally modify existing PDFs. + +## Quick Start + +```typescript +import { buildPDFBytes, downloadBlob } from 'pdfnative'; + +const pdf = buildPDFBytes({ + title: 'Monthly Report', + infoItems: [ + { label: 'Period', value: 'January 2026' }, + { label: 'Account', value: 'Main Account' }, + ], + balanceText: 'Balance: $1,234.56', + countText: '42 transactions', + headers: ['Date', 'Description', 'Category', 'Amount', 'Status'], + rows: [ + { cells: ['01/15', 'Grocery Store', 'Food', '-$45.00', ''], type: 'debit', pointed: false }, + { cells: ['01/16', 'Salary', 'Income', '+$3,000.00', 'X'], type: 'credit', pointed: true }, + ], + footerText: 'Generated by MyApp', +}); + +// Browser: trigger download +downloadBlob(pdf, 'report.pdf'); + +// Node.js: write to file +import { writeFileSync } from 'fs'; +writeFileSync('report.pdf', pdf); +``` + +### Document Builder + +Build free-form documents with headings, paragraphs, lists, tables, images, barcodes, and more: + +```typescript +import { buildDocumentPDFBytes } from 'pdfnative'; + +const pdf = buildDocumentPDFBytes({ + title: 'Project Report', + blocks: [ + { type: 'toc' }, + { type: 'heading', text: 'Executive Summary', level: 1 }, + { type: 'paragraph', text: 'This quarter saw strong growth across all divisions...' }, + { type: 'image', data: jpegBytes, width: 400, align: 'center', alt: 'Revenue chart' }, + { type: 'list', items: ['Revenue up 15%', 'Costs down 8%', 'Net profit +23%'], style: 'bullet' }, + { type: 'table', headers: ['Q1', 'Q2', 'Q3', 'Q4'], rows: [ + { cells: ['$1.2M', '$1.4M', '$1.6M', '$1.8M'], type: 'credit', pointed: false }, + ]}, + { type: 'spacer', height: 20 }, + { type: 'heading', text: 'Next Steps', level: 2 }, + { type: 'paragraph', text: 'Focus areas for next quarter include...', align: 'left' }, + { type: 'link', text: 'View full report online', url: 'https://example.com/report' }, + { type: 'barcode', format: 'qr', data: 'https://example.com/report', align: 'center' }, + ], + footerText: 'Confidential', +}, { + headerTemplate: { center: 'Project Report', right: '{date}' }, + footerTemplate: { left: 'Confidential', right: 'Page {page} of {pages}' }, +}); +``` + +## Unicode Font Support + +For non-Latin scripts, register font data loaders (lazy-loaded on demand): + +```typescript +import { registerFonts, loadFontData, buildPDFBytes } from 'pdfnative'; + +registerFonts({ + th: () => import('pdfnative/fonts/noto-thai-data.js'), + ja: () => import('pdfnative/fonts/noto-jp-data.js'), + zh: () => import('pdfnative/fonts/noto-sc-data.js'), + ko: () => import('pdfnative/fonts/noto-kr-data.js'), + el: () => import('pdfnative/fonts/noto-greek-data.js'), + hi: () => import('pdfnative/fonts/noto-devanagari-data.js'), + tr: () => import('pdfnative/fonts/noto-turkish-data.js'), + vi: () => import('pdfnative/fonts/noto-vietnamese-data.js'), + pl: () => import('pdfnative/fonts/noto-polish-data.js'), + ar: () => import('pdfnative/fonts/noto-arabic-data.js'), + he: () => import('pdfnative/fonts/noto-hebrew-data.js'), + ru: () => import('pdfnative/fonts/noto-cyrillic-data.js'), + ka: () => import('pdfnative/fonts/noto-georgian-data.js'), + hy: () => import('pdfnative/fonts/noto-armenian-data.js'), + bn: () => import('pdfnative/fonts/noto-bengali-data.js'), + ta: () => import('pdfnative/fonts/noto-tamil-data.js'), + te: () => import('pdfnative/fonts/noto-telugu-data.js'), // v1.3.0 + si: () => import('pdfnative/fonts/noto-sinhala-data.js'), // v1.3.0 + bo: () => import('pdfnative/fonts/noto-tibetan-data.js'), // v1.3.0 + km: () => import('pdfnative/fonts/noto-khmer-data.js'), // v1.3.0 + my: () => import('pdfnative/fonts/noto-myanmar-data.js'), // v1.3.0 + am: () => import('pdfnative/fonts/noto-ethiopic-data.js'), // v1.3.0 + // v1.1.0+ — optional Latin fallback for PDF/A documents with curly quotes, + // em-dash, ellipsis, etc. (activates automatically when needed): + latin: () => import('pdfnative/fonts/noto-sans-data.js'), + // v1.1.0+ — optional monochrome emoji: + emoji: () => import('pdfnative/fonts/noto-emoji-data.js'), +}); + +const thaiFont = await loadFontData('th'); + +const pdf = buildPDFBytes({ + title: 'รายงานประจำเดือน', + // ... other params + fontEntries: thaiFont ? [{ fontData: thaiFont, fontRef: '/F3', lang: 'th' }] : [], +}); +``` + +### Supported Languages + +| Language | Code | Font | Script | +|----------|------|------|--------| +| Thai | `th` | Noto Sans Thai | GSUB + GPOS shaping | +| Japanese | `ja` | Noto Sans JP | CJK ideographs + kana | +| Chinese (Simplified) | `zh` | Noto Sans SC | CJK ideographs | +| Korean | `ko` | Noto Sans KR | Hangul syllables | +| Greek | `el` | Noto Sans Greek | Greek alphabet | +| Hindi (Devanagari) | `hi` | Noto Sans Devanagari | GSUB conjuncts + GPOS marks | +| Turkish | `tr` | Noto Sans Turkish | Latin extended (İ/ı) | +| Vietnamese | `vi` | Noto Sans Vietnamese | Latin + combining marks | +| Polish | `pl` | Noto Sans Polish | Latin extended (Ł/ł) | +| Arabic | `ar` | Noto Sans Arabic | GSUB positional shaping | +| Hebrew | `he` | Noto Sans Hebrew | Right-to-left script | +| Russian (Cyrillic) | `ru` | Noto Sans | Cyrillic alphabet | +| Georgian | `ka` | Noto Sans Georgian | Mkhedruli script | +| Armenian | `hy` | Noto Sans Armenian | Armenian alphabet | +| Bengali | `bn` | Noto Sans Bengali | GSUB conjuncts + GPOS marks | +| Tamil | `ta` | Noto Sans Tamil | GSUB ligatures + split vowels | +| Latin (PDF/A) | `latin` | Noto Sans VF | WinAnsi-extended Latin (curly quotes, em-dash, ellipsis…) | +| Emoji | `emoji` | Noto Emoji | Monochrome emoji (BMP/SMP, Fitzpatrick, ZWJ, VS-15/16) | + +## Multi-Font (Mixed Scripts) + +Generate PDFs with multiple scripts in the same document: + +```typescript +const fonts = await Promise.all([ + loadFontData('th'), + loadFontData('ja'), + loadFontData('zh'), +]); + +const fontEntries = fonts + .filter(Boolean) + .map((fd, i) => ({ fontData: fd!, fontRef: `/F${3 + i}`, lang: ['th', 'ja', 'zh'][i] })); + +const pdf = buildPDFBytes({ + title: 'Multi-Language Report', + headers: ['Date', 'Description', 'Category', 'Amount', 'Status'], + rows: [ + { cells: ['01/01', 'English text', 'Test', '+100', 'OK'], type: 'credit', pointed: false }, + { cells: ['01/02', 'ข้อความไทย', 'ทดสอบ', '-50', ''], type: 'debit', pointed: false }, + { cells: ['01/03', '日本語テキスト', 'テスト', '+200', '済'], type: 'credit', pointed: true }, + ], + // ... other params + fontEntries, +}); +``` + +## Web Worker (Large Datasets) + +```typescript +import { createPDF } from 'pdfnative'; + +const pdf = await createPDF(params, { + workerUrl: new URL('pdfnative/worker', import.meta.url), + threshold: 500, // use Worker above 500 rows + timeout: 30000, // Worker timeout in ms (default: 60000) + onProgress: (percent) => console.log(`${percent}%`), +}); +``` + +For lower-level control, use `generatePDFInWorker` directly with `WorkerGenerationOptions`: + +```typescript +import { generatePDFInWorker } from 'pdfnative'; +import type { WorkerGenerationOptions } from 'pdfnative'; + +const options: WorkerGenerationOptions = { + timeout: 15000, + onProgress: (percent) => console.log(`${percent}%`), +}; + +const pdf = await generatePDFInWorker(workerUrl, params, options); +``` + +## Layout Customization + +```typescript +const pdf = buildPDFBytes(params, { + pageWidth: 595.28, // A4 (default) + pageHeight: 841.89, // A4 (default) + margins: { t: 45, r: 36, b: 35, l: 36 }, + colors: { + title: '#2563EB', // hex — primary format + credit: [15, 145, 121], // RGB tuple [0–255] + debit: '0.863 0.149 0.149', // PDF operator string [0.0–1.0] + // ... see PdfColors type + }, + columns: [ + { f: 0.15, a: 'l', mx: 12, mxH: 12 }, + { f: 0.35, a: 'l', mx: 50, mxH: 50 }, + { f: 0.20, a: 'r', mx: 20, mxH: 20 }, + { f: 0.30, a: 'r', mx: 30, mxH: 30 }, + ], +}); +``` + +### Color Formats + +All color values accept three formats: + +| Format | Example | Description | +|--------|---------|-------------| +| Hex string | `'#2563EB'` or `'#26E'` | Primary format — `#RRGGBB` or `#RGB` | +| RGB tuple | `[37, 99, 235]` | Array with values 0–255 | +| PDF operator | `'0.145 0.388 0.922'` | Raw PDF RGB string (0.0–1.0) | + +```typescript +import { parseColor } from 'pdfnative'; + +parseColor('#2563EB'); // '0.145 0.388 0.922' +parseColor([37, 99, 235]); // '0.145 0.388 0.922' +parseColor('0.145 0.388 0.922'); // '0.145 0.388 0.922' +``` + +All inputs are validated and normalized before interpolation into PDF content streams, preventing operator injection. + +### Font Sizes + +Customize font sizes for each zone (title, info bar, table header, table cells, footer): + +```typescript +const pdf = buildPDFBytes(params, { + fontSizes: { + title: 20, // Title text (default: 16) + info: 10, // Info bar items (default: 9) + th: 9, // Table header cells (default: 8) + td: 8, // Table body cells (default: 7.5) + ft: 8, // Footer text (default: 7) + }, +}); +``` + +| Zone | Key | Default | Description | +|------|-----|---------|-------------| +| Title | `title` | 16 | PDF title text | +| Info bar | `info` | 9 | Key-value pairs below title | +| Table header | `th` | 8 | Column header row | +| Table cells | `td` | 7.5 | Data row cells | +| Footer | `ft` | 7 | Page footer text | + +All values are in PDF points (1pt = 1/72 inch). Partial overrides are supported — unspecified keys use defaults. + +## Building Custom Font Data + +### Obtaining TTF Files + +For Noto Sans fonts, download the raw `.ttf` file directly from the [noto-fonts GitHub repository](https://github.com/notofonts): + +1. Navigate to the font's GitHub repository (e.g., `github.com/notofonts/bengali`) +2. Find the TTF file under `fonts/NotoSansBengali/unhinted/ttf/` (or similar path) +3. Click the file, then click **"Download raw file"** (or use the raw URL) +4. Save it to `fonts/ttf/` + +No zip download or extraction needed — each TTF is a standalone file you can download directly. + +### Building the Data Module + +Convert any TTF font into an importable data module: + +```bash +npx pdfnative-build-font fonts/ttf/MyFont.ttf fonts/my-font-data.js +``` + +The tool extracts cmap, widths, metrics, GSUB, GPOS, and embeds the raw TTF as base64. + +### Full colour-emoji coverage (`pdfnative-build-emoji-font`) + +The bundled colour-emoji module (`pdfnative/fonts/noto-color-emoji-data.js`) +ships a lean curated subset to keep the package small. When you need glyphs +beyond that subset — up to the **full ~3,600-glyph** Noto Color Emoji set — a +second bundled binary generates a custom data module on demand, so even +**pdfnative-only** users get full coverage without the package ever carrying the +~32 MB source font: + +```bash +# Download the pinned Noto Color Emoji (SHA-256 verified) and emit every glyph +npx pdfnative-build-emoji-font --download --all --out my-color-emoji-data.js + +# …or build from a local TTF, selecting only the glyphs you need +npx pdfnative-build-emoji-font --ttf NotoColorEmoji-Regular.ttf \ + --codepoints "1F600,1F680,2764" --out my-color-emoji-data.js +``` + +Select glyphs with `--all`, `--preset`, `--codepoints`, or `--ranges`, then +register the generated module under lang `'emoji'`. See the +[Colour-emoji CLI guide](docs/guides/colour-emoji-cli.md). + +## Agentic workflows + +pdfnative is shaped so an AI agent can do more than *call* the engine — it can +**extend** it at runtime and **feed it content it generated itself**, without +waiting for a library release. Both patterns use already-shipped, public APIs. + +**1. Extend the engine at runtime — no release required.** The font registry is a +runtime API. An agent can compile a TTF/OTF in memory and register it on the spot, +so a document renders the moment it needs a new script, symbol set, or brand font: + +```js +import { registerFont, loadFontData, buildDocumentPDFBytes } from 'pdfnative'; +registerFont('th', () => import('pdfnative/fonts/noto-thai-data.js')); +const th = await loadFontData('th'); +if (!th) throw new Error('Thai font failed to load'); +const bytes = buildDocumentPDFBytes({ + title: 'Thai demo', + blocks: [{ type: 'paragraph', text: 'สวัสดีชาวโลก' }], + fontEntries: [{ fontData: th, fontRef: '/F3', lang: 'th' }], // /F1 and /F2 are reserved +}); +``` + +This is how the bundled **Noto Sans Math** font existed as a *working runtime +pattern* before it shipped as a default in v1.5.0. Use `compileFontData()` to emit +a reusable `*-data.js` module (byte-identical to `npx pdfnative-build-font`). + +**2. Embed agent-generated images.** Image-generating agents (e.g. Antigravity, +ChatGPT, and other multimodal assistants) can pipe a generated PNG/JPEG straight +into a document — via the `image` block (library / CLI) or the `embed_image` MCP +tool. pdfnative parses and embeds it natively (no rasterization) and validates it +at the boundary. + +Runtime extensibility is **not** autonomous modification of the published package: +the agent extends its own in-process instance; the repository is only ever changed +by a human under the [AI-governance / human-in-the-loop contract](docs/guides/ai-governance.md). +See the [Agentic workflows guide](docs/guides/agentic-workflows.md) for the full walkthrough. + +## Visual PDF Inspection + + + +Generate sample PDFs for all supported languages to visually verify output: + +```bash +npm run test:generate +``` + +This creates **242 sample PDFs** in `test-output/` (git-ignored), organized in 37 categories produced by 48 generators (including `charts/` native vector charts, `bidi/` Persian and mirroring showcases, `signatures/` LTV pipelines, and `print/` prepress output added in v1.7.0). +See [scripts/README.md](scripts/README.md) for the modular generator architecture. + +### Financial Statements (per language) + +| File | Content | +|------|---------| +| `sample-latin.pdf` | English / Helvetica | +| `sample-th.pdf` | Thai with GSUB + GPOS shaping | +| `sample-ja.pdf` | Japanese (CJK ideographs) | +| `sample-zh.pdf` | Chinese Simplified | +| `sample-ko.pdf` | Korean (Hangul) | +| `sample-el.pdf` | Greek | +| `sample-hi.pdf` | Hindi (Devanagari) | +| `sample-tr.pdf` | Turkish (İ/ı special casing) | +| `sample-vi.pdf` | Vietnamese (combining marks) | +| `sample-pl.pdf` | Polish (Ł/ł) | +| `sample-ar.pdf` | Arabic (RTL, positional shaping) | +| `sample-he.pdf` | Hebrew (RTL) | +| `sample-ru.pdf` | Russian (Cyrillic) | +| `sample-ka.pdf` | Georgian (Mkhedruli) | +| `sample-hy.pdf` | Armenian | +| `sample-bn.pdf` | Bengali (GSUB conjuncts + GPOS marks) | +| `sample-ta.pdf` | Tamil (GSUB + split vowel decomposition) | +| `sample-multi.pdf` | Mixed: all 22 scripts in one PDF | +| `sample-pagination.pdf` | 200 rows, multi-page layout | + +### Diverse Use Cases (non-financial) + +| File | Content | +|------|---------| +| `diverse-student-transcript.pdf` | University academic transcript (Latin) | +| `diverse-recipe-th.pdf` | Thai recipe — Tom Yum Goong ingredients (Thai) | +| `diverse-server-ja.pdf` | Server monitoring dashboard (Japanese) | +| `diverse-inventory-zh.pdf` | Warehouse product inventory (Chinese) | +| `diverse-sports-ko.pdf` | K-League football standings (Korean) | +| `diverse-library-el.pdf` | Classical Greek library catalog (Greek) | +| `diverse-medical-hi.pdf` | Blood test lab results (Hindi) | +| `diverse-menu-tr.pdf` | Turkish restaurant dinner menu (Turkish) | +| `diverse-weather-vi.pdf` | Weekly weather forecast — Hanoi (Vietnamese) | +| `diverse-train-pl.pdf` | Train schedule — Warsaw (Polish) | +| `diverse-marketplace-ar.pdf` | Gold marketplace catalog — Dubai (Arabic) | +| `diverse-museum-he.pdf` | Museum exhibition catalog — Jerusalem (Hebrew) | + +### Alphabet / Character Coverage + +| File | Content | +|------|---------| +| `alphabet-thai.pdf` | 44 consonants, vowels, tone marks, digits | +| `alphabet-japanese.pdf` | Hiragana, Katakana, Kanji numerals & common | +| `alphabet-chinese.pdf` | 121 characters by category (HSK frequency) | +| `alphabet-korean.pdf` | Hangul jamo, syllables, complex clusters | +| `alphabet-greek.pdf` | Full uppercase/lowercase, accented, archaic | +| `alphabet-devanagari.pdf` | Vowels, consonants, matras, conjuncts, digits | +| `alphabet-turkish.pdf` | 29 letters, İ/ı dotted-I distinction test | +| `alphabet-vietnamese.pdf` | 7 base vowels × 6 tones, all diacritics | +| `alphabet-polish.pdf` | 32 letters, digraphs, pangram | +| `alphabet-arabic.pdf` | 28 letters, harakat, numerals, ligatures | +| `alphabet-hebrew.pdf` | 22 letters, final forms, vowel points | +| `alphabet-cyrillic.pdf` | 33 Russian letters, Ukrainian/Serbian extended | +| `alphabet-georgian.pdf` | 33 Mkhedruli letters, Asomtavruli | +| `alphabet-armenian.pdf` | 38 letters, ligatures | +| `alphabet-bengali.pdf` | Vowels, consonants, conjuncts, digits | +| `alphabet-tamil.pdf` | Vowels, consonants, compound characters, digits | + +### PDF/A Conformance Variants + +| File | Content | +|------|---------| +| `tagged-pdfa2b-default.pdf` | PDF/A-2b (tagged=true, default) | +| `tagged-pdfa2b-explicit.pdf` | PDF/A-2b (tagged='pdfa2b', explicit) | +| `tagged-pdfa1b.pdf` | PDF/A-1b (tagged='pdfa1b', legacy) | +| `tagged-pdfa2u.pdf` | PDF/A-2u (tagged='pdfa2u', Unicode) | +| `tagged-pdfa3b.pdf` | PDF/A-3b (tagged='pdfa3b', embedded file attachments) | + +### Encrypted PDFs + +| File | Content | +|------|---------| +| `encrypted-aes128.pdf` | AES-128 (V4/R4) owner-only | +| `encrypted-aes256.pdf` | AES-256 (V5/R6) owner-only | +| `encrypted-aes128-user.pdf` | AES-128 with user+owner passwords | +| `encrypted-aes256-user.pdf` | AES-256 with user+owner passwords | +| `encrypted-readonly.pdf` | AES-128 read-only (no copy/modify) | +| `encrypted-noprint.pdf` | AES-128 fully restricted | + +**Sample passwords** (for testing only — all documented in `scripts/generate-samples.ts`): + +| File | Owner Password | User Password | +|------|---------------|---------------| +| `encrypted-aes128.pdf` | `owner123` | _(none — opens freely)_ | +| `encrypted-aes256.pdf` | `owner256` | _(none — opens freely)_ | +| `encrypted-aes128-user.pdf` | `owner123` | `user456` | +| `encrypted-aes256-user.pdf` | `owner256` | `user789` | +| `encrypted-readonly.pdf` | `owner-ro` | _(none — opens freely)_ | +| `encrypted-noprint.pdf` | `owner-np` | _(none — opens freely)_ | +| `doc-encrypted-aes128.pdf` | `docowner` | `docuser` | +| `doc-encrypted-aes256.pdf` | `strongowner256` | _(none — opens freely)_ | + +### Document Builder Samples + +| File | Content | +|------|---------| +| `doc-headings-paragraphs.pdf` | H1/H2/H3 + paragraphs with text wrapping | +| `doc-lists.pdf` | Bullet + numbered lists | +| `doc-links.pdf` | External hyperlink annotations | +| `doc-table.pdf` | Embedded table in document | +| `doc-spacer-pagebreak.pdf` | Spacers + forced page breaks (3 pages) | +| `doc-encrypted-aes128.pdf` | Document builder + AES-128 encryption | +| `doc-encrypted-aes256.pdf` | Document builder + AES-256 encryption | +| `doc-image.pdf` | Image embedding (JPEG, centered) | +| `doc-custom-colors.pdf` | Color formats (hex, tuple, PDF operator) | +| `doc-japanese.pdf` | Japanese Unicode document (headings, lists, table) | +| `doc-arabic.pdf` | Arabic RTL document (headings, lists, table, BiDi) | +| `doc-hebrew.pdf` | Hebrew RTL document (headings, lists, table, BiDi) | +| `doc-thai.pdf` | Thai user manual (GSUB+GPOS shaping, pricing table) | +| `doc-bengali.pdf` | Bengali document (GSUB conjuncts + GPOS marks) | +| `doc-tamil.pdf` | Tamil document (GSUB substitution + split vowels) | +| `doc-devanagari.pdf` | Hindi (Devanagari) document — GSUB conjuncts, reph reordering, matra reordering, split vowels | +| `doc-telugu.pdf` | Telugu document (virama conjuncts + GPOS marks, no reph) | +| `doc-sinhala.pdf` | Sinhala document (virama conjuncts + pre-base kombuva reordering) | +| `doc-tibetan.pdf` | Tibetan document (vertical subjoined-consonant stacking) | +| `doc-khmer.pdf` | Khmer document (USE-lite: coeng subscripts, pre-base vowels) | +| `doc-myanmar.pdf` | Myanmar document (USE-lite: medials, pre-base reordering) | +| `doc-amharic.pdf` | Amharic/Ethiopic document (syllabic abugida, no reordering) | +| `doc-chinese-catalog.pdf` | Chinese product catalog (tables, ordering info) | +| `doc-multi-language.pdf` | Multi-language showcase: all 22 Unicode scripts in one PDF | +| `doc-invoice.pdf` | Invoice template (line items, totals, payment link) | +| `doc-report-multipage.pdf` | 3-page technical report (7 sections, 4 tables) | +| `doc-contract-bilingual.pdf` | Bilingual EN/AR contract (legal sections, signatures) | +| `doc-showcase-all-blocks.pdf` | All 13 block types in one PDF | + +### Compressed PDFs (FlateDecode) + +| File | Content | +|------|---------| +| `compressed-latin-100rows.pdf` | 100-row Latin table (87% smaller) | +| `uncompressed-latin-100rows.pdf` | Same 100-row table without compression (baseline) | +| `compressed-japanese.pdf` | Japanese CIDFont + TTF subset (62% smaller) | +| `compressed-arabic.pdf` | Arabic RTL + GSUB shaping (compressed) | +| `compressed-thai.pdf` | Thai GSUB+GPOS shaping (compressed) | +| `compressed-tagged-pdfa2b.pdf` | FlateDecode + Tagged PDF/A-2b (XMP uncompressed) | +| `compressed-encrypted-aes128.pdf` | FlateDecode + AES-128 encryption | +| `doc-compressed.pdf` | Document builder with FlateDecode | + +### Stress Test PDFs + +| File | Content | +|------|---------| +| `stress-test-10k-rows.pdf` | 10,000-row table (167 pages, 4.3MB) | +| `doc-extreme-bidi-wrapping.pdf` | Extreme BiDi mixed-script text wrapping | +| `table-heavy-text-overflow.pdf` | Dense table with heavy text overflow | +| `media-rich-document.pdf` | Media-rich document with multiple images | +| `tagged-accessibility-complex.pdf` | Complex tagged PDF/A accessibility tree | +| `layout-extreme-customization.pdf` | Extreme layout customization (margins, columns, colors) | + +### Edge-Case Stress Tests + +| File | Content | +|------|---------| +| `doc-unbreakable-text.pdf` | 1000-char words with no spaces (DNA, URL, Base64) | +| `table-micro-columns.pdf` | Extreme column fractions (f=0.025, mx=1) | +| `doc-link-annotation-bomb.pdf` | 500 link annotations across 10 pages | +| `zero-content-empty-table.pdf` | Table with headers but 0 rows | +| `zero-content-empty-doc.pdf` | Document with no blocks | +| `zero-content-empty-strings.pdf` | Empty headings, paragraphs, and list items | +| `doc-heavy-buffer-5mb.pdf` | 5 MB synthetic JPEG embedded (memory stress) | + +### Barcode & QR Code Samples + +| File | Content | +|------|---------| +| `barcode-showcase.pdf` | All 5 formats: Code 128, EAN-13, QR Code, Data Matrix, PDF417 | +| `barcode-alignment-sizing.pdf` | Alignment (left/center/right) and custom size variations | +| `barcode-tagged-pdfa.pdf` | Barcodes in tagged PDF/A-2b mode (/Figure structure elements) | + +### SVG Path Rendering Samples + +| File | Content | +|------|---------| +| `svg-basic-shapes.pdf` | Rect, circle, ellipse, line, polyline, polygon | +| `svg-complex-paths.pdf` | Cubic/quadratic Bézier curves, arcs, combined paths | +| `svg-tagged-pdfa.pdf` | SVG elements in tagged PDF/A-2b mode | + +### Form Field Samples + +| File | Content | +|------|---------| +| `form-fields.pdf` | All field types: text, multiline, checkbox, radio, dropdown, listbox | +| `form-contact.pdf` | Contact form with name, email, message, and submit fields | + +### Digital Signature Samples + +| File | Content | +|------|---------| +| `sig-rsa-self-signed.pdf` | RSA PKCS#1 v1.5 self-signed signature | +| `sig-ecdsa-p256.pdf` | ECDSA P-256 digital signature | +| `sig-multi-field.pdf` | PDF with multiple signature fields | + +### Streaming Output Samples + +| File | Content | +|------|---------| +| `streaming-document.pdf` | Document streamed via `buildDocumentPDFStream()` | +| `streaming-table.pdf` | Table streamed via `buildPDFStream()` | + +### PDF Parser & Modifier Samples + +| File | Content | +|------|---------| +| `parser-original.pdf` | Generated → parsed → verified round-trip | +| `parser-modified.pdf` | Generated → parsed → modified → incremental save | +| `parser-document.pdf` | Document builder → parser round-trip verification | + +### Outline & Page Label Samples (v1.4.0) + +| File | Content | +|------|---------| +| `outline/outline-explicit.pdf` | Nested bookmarks (`/Outlines`) + roman/decimal page labels | +| `outline/outline-auto.pdf` | `outline: 'auto'` — bookmarks derived from headings | +| `outline/page-labels.pdf` | Roman front matter + prefixed appendix page labels | + +### PDF Manipulation Samples (v1.4.0) + +| File | Content | +|------|---------| +| `manipulation/merged.pdf` | `mergePdfs()` — multiple documents combined | +| `manipulation/split-report.pdf` | `splitPdf()` — first page range | +| `manipulation/split-invoice.pdf` | `splitPdf()` — second page range | +| `manipulation/extract-reordered.pdf` | `extractPages()` — selected pages, reordered | +| `manipulation/streamed.pdf` | `streamToFile()` — document streamed straight to disk | + +## API Reference + +### Core + +| Function | Description | +|----------|-------------| +| `buildPDF(params, layout?)` | Build table-centric PDF as binary string | +| `buildPDFBytes(params, layout?)` | Build table-centric PDF as `Uint8Array` | +| `buildDocumentPDF(params, layout?)` | Build free-form document PDF as binary string | +| `buildDocumentPDFBytes(params, layout?)` | Build free-form document PDF as `Uint8Array` | +| `wrapText(text, maxWidth, fontSize, enc)` | Word-wrap text into lines | +| `createPDF(params, options?)` | Smart dispatch (Worker or main thread) | +| `initNodeCompression()` | Initialize native zlib for ESM (call once before `compress: true`) | +| `downloadBlob(bytes, filename)` | Trigger browser download | +| `toBytes(str)` | Convert binary string to `Uint8Array` | +| `slugify(str)` | Sanitize string for filename | + +### Image Support + +| Function | Description | +|----------|-------------| +| `parseImage(bytes)` | Auto-detect and parse JPEG or PNG | +| `parseJPEG(bytes)` | Parse JPEG image (DCTDecode) | +| `parsePNG(bytes)` | Parse PNG image (FlateDecode) | +| `detectImageFormat(bytes)` | Detect JPEG or PNG from magic bytes | +| `buildImageXObject(img, smaskObj?)` | Build PDF Image XObject dictionary | +| `buildImageOperators(ref, x, y, w, h)` | Build `q cm Do Q` content stream operators | + +### Link Annotations + +| Function | Description | +|----------|-------------| +| `validateURL(url)` | Validate URL scheme (http/https/mailto only) | +| `buildLinkAnnotation(annot)` | Build PDF /Link annotation with /URI action | +| `buildInternalLinkAnnotation(link)` | Build PDF /Link with /GoTo action | +| `isLinkAnnotation(annot)` | Type guard for LinkAnnotation | + +### BiDi & Arabic/Hebrew Shaping + +| Function | Description | +|----------|-------------| +| `resolveBidiRuns(text)` | Resolve text into BiDi runs with levels | +| `containsRTL(text)` | Check if text contains RTL characters | +| `shapeArabicText(str, fontData)` | Arabic GSUB positional shaping | +| `containsArabic(text)` | Check for Arabic characters | +| `containsHebrew(text)` | Check for Hebrew characters | + +### Barcode & QR Code + +| Function | Description | +|----------|-------------| +| `renderBarcode(format, data, x, y, opts?)` | Unified barcode renderer (dispatches to format-specific function) | +| `encodeCode128(data)` | Encode data into Code 128 barcode pattern (ISO 15417) | +| `renderCode128(data, x, y, w, h)` | Render Code 128 barcode as PDF path operators | +| `ean13CheckDigit(digits)` | Compute EAN-13 check digit (ISO 15420) | +| `renderEAN13(data, x, y, w, h)` | Render EAN-13 barcode with guard bars and digits | +| `generateQR(data, ecLevel?)` | Generate QR Code matrix (ISO 18004) | +| `renderQR(data, x, y, size, ecLevel?)` | Render QR Code as PDF path operators | +| `generateDataMatrix(data)` | Generate Data Matrix ECC 200 matrix (ISO 16022) | +| `renderDataMatrix(data, x, y, size)` | Render Data Matrix as PDF path operators | +| `encodePDF417(data, ecLevel?)` | Encode data into PDF417 codewords (ISO 15438) | +| `renderPDF417(data, x, y, w, h, ecLevel?)` | Render PDF417 barcode as PDF path operators | + +### SVG Rendering + +| Function | Description | +|----------|-------------| +| `parseSvgPath(d)` | Parse SVG path `d` attribute into segments | +| `renderSvg(segments, options?)` | Render SVG segments (paths + ``) as PDF operators | + +### Markup Annotations + +| Function | Description | +|----------|-------------| +| `buildAnnotation(annot, objNum)` | Build a full markup annotation indirect object (v1.5.0) | +| `buildAnnotationBody(annot)` | Build a markup annotation dictionary body (for the modifier) (v1.5.0) | + +Supported `MarkupAnnotation` types: `text`, `highlight`, `underline`, `strikeout`, `squiggly`, `square`, `circle`, `line`, `freetext`. + +### Layout Debug & Inspection + +| Function | Description | +|----------|-------------| +| `inspectDocumentLayout(params, layout?)` | Return a programmatic per-page block-geometry `LayoutInspection` (v1.5.0) | + +Enable the visual overlay via `layout: { debug: true }` or a granular `LayoutDebugOptions` (`showMargins` / `showContentBounds` / `showCells`). Byte-identical when debug is off. + +### Font-Data Tools (`pdfnative/tools`) + +| Function | Description | +|----------|-------------| +| `compileFontData(buffer, opts?)` | Compile a TTF/OTF `Uint8Array` into a font-data module source string (v1.5.0) | +| `parseFontData(buffer, opts?)` | Parse a TTF/OTF `Uint8Array` into a `FontDataObject` (metrics, cmap, widths, glyph coverage) (v1.5.0) | + +### AcroForm Fields + +| Function | Description | +|----------|-------------| +| `buildFormWidget(field, objNum, pageRef)` | Build form field widget annotation + appearance stream | +| `buildAcroFormDict(fieldRefs)` | Build `/AcroForm` dictionary for catalog | +| `buildRadioGroupParent(group)` | Build radio button group parent object | +| `buildAppearanceStreamDict(width, height)` | Build appearance stream dictionary | +| `defaultFieldHeight(type)` | Default height by field type | + +### Digital Signatures + +| Function | Description | +|----------|-------------| +| `buildSigDict(options)` | Build `/Sig` dictionary with ByteRange/Contents placeholders (`subFilter: 'ETSI.CAdES.detached'` for PAdES, v1.7.0) | +| `buildDocTimeStampDict(contentsSize?)` | Build a `/DocTimeStamp` dictionary variant (ISO 32000-2 §12.8.5, v1.7.0) | +| `signPdfBytes(pdf, options)` | Sign a PDF with CMS/PKCS#7 detached signature (`profile: 'pades'`, `digestAlgorithm`, multi-signature `fieldName` selector, v1.7.0) | +| `signPdfBytesWithTimestamp(pdf, options)` | Sign + embed a verified RFC 3161 signature timestamp (PAdES B-T, v1.7.0) | +| `listSignatures(pdf)` | Enumerate signature fields — SubFilter, ByteRange, /Contents, placeholder/timestamp flags (v1.7.0) | +| `collectValidationInfo(pdf, opts?)` | Gather certificates + OCSP/CRL material for every signature via the injected `RevocationProvider` (v1.7.0) | +| `embedValidationInfo(pdf, data)` | Write pre-collected LTV material as `/DSS` + `/VRI` — sync, offline, deterministic (v1.7.0) | +| `addValidationInfo(pdf, opts?)` | Collect + embed in one call (PAdES B-LT, v1.7.0) | +| `addDocumentTimestamp(pdf, opts?)` | Append a `/DocTimeStamp` revision (PAdES B-LTA, v1.7.0) | +| `estimateContentsSize(certSizes, algorithm?, opts?)` | Estimate `/Contents` size (`{ timestamp: true }` adds RFC 3161 headroom, v1.7.0) | +| `setCryptoProvider(provider)` | Install (or clear with `null`) a global native signature provider (v1.4.0) | +| `getCryptoProvider()` | Return the current global `CryptoProvider`, or `null` (v1.4.0) | +| `setTimestampProvider(provider)` / `getTimestampProvider()` | Global RFC 3161 transport — TimeStampReq bytes in, TimeStampResp bytes out (v1.7.0) | +| `setRevocationProvider(provider)` / `getRevocationProvider()` | Global OCSP/CRL transport (v1.7.0) | + +### Streaming Output + +| Function | Description | +|----------|-------------| +| `buildDocumentPDFStream(params, layout?, streamOpts?)` | Stream document PDF as `AsyncGenerator` | +| `buildPDFStream(params, layout?, streamOpts?)` | Stream table PDF as `AsyncGenerator` | +| `buildDocumentPDFStreamTrue(params, layout?, streamOpts?)` | **True constant-memory** document streaming — frees each part as it yields (v1.3.0) | +| `buildPDFStreamTrue(params, layout?, streamOpts?)` | **True constant-memory** table streaming (v1.3.0) | +| `buildDocumentPDFStreamPageByPage(params, layout?)` | Stream document PDF chunked at PDF object boundaries | +| `buildPDFStreamPageByPage(params, layout?)` | Stream table PDF chunked at PDF object boundaries | +| `validateDocumentStreamable(params, layout?)` | Validate document is compatible with streaming (no TOC, no `{pages}`) | +| `validateTableStreamable(params, layout?)` | Validate table is compatible with streaming | +| `chunkBinaryString(str, chunkSize)` | Split binary string into `Uint8Array` chunks | +| `concatChunks(chunks)` | Concatenate `Uint8Array` chunks into one | +| `streamByteLength(stream)` | Count total bytes from an async stream | +| `streamToFile(stream, filePath, opts?)` | Drain an `AsyncGenerator` to disk with back-pressure + `AbortSignal` (Node) — returns `{ bytesWritten, path }` (v1.4.0) | + +### Crypto (Hashing, ASN.1, RSA, ECDSA, X.509, CMS) + +| Function | Description | +|----------|-------------| +| `sha1(data)` | SHA-1 — `/VRI` keying and OCSP CertID identification only, never a security digest (v1.7.0) | +| `sha384(data)` / `sha512(data)` | SHA-384 / SHA-512 hash (FIPS 180-4) | +| `hmacSha256(key, data)` | HMAC-SHA-256 (RFC 2104) | +| `derDecode(data)` | Decode DER-encoded ASN.1 | +| `derSetOf(...values)` / `derGeneralizedTime(date)` | Canonical DER SET OF (X.690 §11.6) / GeneralizedTime encoder (v1.7.0) | +| `rsaSign(msg, key, digest?)` / `rsaVerify(msg, sig, key, digest?)` | RSA PKCS#1 v1.5 sign/verify — SHA-256/384/512 (digest param v1.7.0) | +| `ecdsaSign(hash, key)` / `ecdsaVerify(hash, sig, key)` | ECDSA P-256 sign/verify | +| `parseCertificate(der)` | Parse X.509 DER certificate — incl. SKI/AKI, EKU, AIA (OCSP/caIssuers URLs), CRL distribution points (v1.7.0) | +| `certHasEku(cert, oid)` | Check an extended-key-usage OID (v1.7.0) | +| `buildCmsSignedData(options)` | Build CMS SignedData — PKCS#7 or PAdES profile, SHA-256/384/512 (v1.7.0) | +| `parseCmsSignedData(der)` / `addUnsignedAttribute(cms, attr)` / `buildAttribute(oid, ...v)` | CMS inspection + unsigned-attribute surgery, signed bytes untouched (v1.7.0) | +| `buildTimestampRequest(imprint, opts?)` / `parseTimestampResponse(der)` / `parseTimestampToken(der)` / `verifyTimestampImprint(info, hash)` | RFC 3161 timestamp protocol (v1.7.0) | +| `buildOcspRequest(cert, issuer, opts?)` / `parseOcspResponse(der)` | OCSP RFC 6960 (v1.7.0) | +| `parseCrl(der)` / `isSerialRevoked(crl, serial)` | CRL RFC 5280 (v1.7.0) | +| `initCrypto()` | Initialize crypto module (lazy load) | + +### PDF Parser & Modifier + +| Function | Description | +|----------|-------------| +| `openPdf(bytes, opts?)` | Parse a PDF `Uint8Array` and return a `PdfReader`. Pass `{ password }` to open an encrypted document (RC4, AES-128, AES-256) | +| `createModifier(reader)` | Create an incremental `PdfModifier` from a `PdfReader` | +| `createTokenizer(data, offset?)` | Create a low-level PDF tokenizer | +| `parseValue(tok)` | Parse a single PDF value from token stream | +| `parseIndirectObject(tok)` | Parse an indirect object (`N M obj ... endobj`) | +| `findStartxref(data)` | Find `startxref` offset in PDF bytes | +| `parseXrefTable(data, offset)` | Parse xref table/stream at given offset | +| `isRef(v)` / `isDict(v)` / `isArray(v)` / `isStream(v)` | Type guards for parsed PDF values | +| `dictGet(dict, key)` / `dictGetName(dict, key)` | Dictionary value accessors | +| `inflateSync(data)` | Decompress FlateDecode data (zlib inflate) | +| `validatePdfUA(bytes)` | Read-only PDF/UA structural checker — returns `{ valid, errors, warnings }` (v1.3.0) | +| `mergePdfs(sources, opts?)` | Merge multiple PDFs into one, rebuilding a clean object graph; `opts.maxOutputSize` caps output at 256 MiB by default (v1.4.0); `opts.encrypt` re-encrypts the output (AES-128/AES-256, v1.6.0) | +| `splitPdf(src, ranges, opts?)` | Split a PDF into multiple documents by inclusive 0-based page ranges (v1.4.0) | +| `extractPages(src, indices, opts?)` | Extract specific pages (0-based) into a new PDF (v1.4.0) | +| `reader.getPageLabels()` | Parse an existing `/PageLabels` number tree into `PageLabelRange[]` or `null` (v1.5.0) | +| `reader.getAnnotations(pageIndex)` | Read a page's annotations into `ParsedAnnotation[]` (v1.5.0) | +| `reader.getPageRef(pageIndex)` | Get the indirect `PdfRef` for a page (v1.5.0) | +| `modifier.addAnnotation(pageIndex, body)` | Inject a new annotation on a page via incremental update (v1.5.0) | +| `extractText(bytes, opts?)` | Extract per-page reading-order Unicode text (+ optional positioned runs) from any PDF, incl. encrypted (`opts.password`); `ToUnicode`/`Differences`/WinAnsi/MacRoman decoding, hard `maxTextLength` memory cap (v1.6.0) | + +### Document Block Types + +| Type | Description | +|------|-------------| +| `HeadingBlock` | H1/H2/H3 with color, auto-wrapped | +| `ParagraphBlock` | Text with fontSize, lineHeight, align, indent, color | +| `TableBlock` | Headers + rows using PdfRow/ColumnDef | +| `ListBlock` | Bullet or numbered items; entries may be plain strings or nested `ListItem` `{ text, items }` for hierarchical lists (v1.4.0) | +| `ImageBlock` | JPEG/PNG with optional width, height, align, alt text | +| `LinkBlock` | Hyperlink with URL, blue underline, tagged /Link | +| `SpacerBlock` | Vertical whitespace | +| `PageBreakBlock` | Force new page | +| `TocBlock` | Auto-generated table of contents with /GoTo links | +| `BarcodeBlock` | Barcode / QR code rendered via PDF path operators | +| `SvgBlock` | SVG path/shape rendering as native PDF path operators | +| `FormFieldBlock` | AcroForm interactive fields (text, checkbox, radio, dropdown, listbox) | + +### Tagged PDF & PDF/A + +| Function | Description | +|----------|-------------| +| `resolvePdfAConfig(tagged)` | Resolve tagged option → PDF/A config (version, part, conformance) | +| `encodePdfTextString(str)` | Encode string as PDF text (PDFDocEncoding or UTF-16BE hex) | + +### Encryption + +Encryption is configured via the `encryption` option in layout options. Since v1.6.0 the reader side is public too — `openPdf(bytes, { password })` plus the `PdfPasswordError`, `PdfEncryptionUnsupportedError` and `PdfEncryptionInfo` exports. + +```typescript +const pdf = buildPDFBytes(params, { + encryption: { userPassword: 'secret', ownerPassword: 'admin', permissions: { print: true } } +}); +``` + +### Color Utilities + +| Function | Description | +|----------|-------------| +| `parseColor(input)` | Parse hex / tuple / PDF string → validated PDF RGB string | +| `isValidPdfRgb(str)` | Check if string is valid `"R G B"` format (0.0–1.0) | +| `normalizeColors(colors)` | Validate and normalize all fields in a PdfColors object | + +### Compression + +| Function | Description | +|----------|-------------| +| `initNodeCompression()` | Initialize native zlib (async, call once in ESM before `compress: true`) | +| `setDeflateImpl(fn)` | Inject custom DEFLATE function (e.g. for browser polyfill) | + +**Browser compression** — In browser environments without native zlib, inject a third-party DEFLATE via `setDeflateImpl`: + +```typescript +import { setDeflateImpl, buildPDFBytes } from 'pdfnative'; +import { deflateSync } from 'fflate'; // or pako + +setDeflateImpl(deflateSync); + +const pdf = buildPDFBytes(params, { compress: true }); +``` + +### Fonts + +| Function | Description | +|----------|-------------| +| `registerFont(lang, loader)` | Register a font data loader | +| `registerFonts(map)` | Register multiple font loaders | +| `loadFontData(lang)` | Lazy-load font data (cached) | +| `hasFontLoader(lang)` | Check if loader is registered | +| `getRegisteredLangs()` | List registered language codes | +| `createEncodingContext(fontEntries)` | Create encoding context | +| `validateFontData(data)` | Opt-in structural validation of custom font data — returns `{ valid, errors, warnings }` (v1.4.0) | + +### Shaping + +| Function | Description | +|----------|-------------| +| `shapeThaiText(str, fontData)` | Thai OpenType shaping (GSUB + GPOS) | +| `shapeBengaliText(str, fontData)` | Bengali GSUB conjuncts + GPOS marks | +| `shapeTamilText(str, fontData)` | Tamil GSUB + split vowel decomposition | +| `shapeDevanagariText(str, fontData)` | Devanagari cluster shaping + GSUB/GPOS | +| `shapeTeluguText(str, fontData)` | Telugu GSUB conjuncts + GPOS marks (v1.3.0) | +| `shapeSinhalaText(str, fontData)` | Sinhala conjuncts + pre-base reorder + GSUB/GPOS (v1.3.0) | +| `shapeTibetanText(str, fontData)` | Tibetan vertical subjoined stacking (v1.3.0) | +| `shapeKhmerText(str, fontData)` | Khmer USE-lite — coeng subscripts + pre-base vowels (v1.3.0) | +| `shapeMyanmarText(str, fontData)` | Myanmar USE-lite — medials + virama stacking (v1.3.0) | +| `detectFallbackLangs(texts, primaryLang)` | Detect needed fallback fonts | +| `detectCharLang(codePoint)` | Map codepoint to preferred font language | +| `splitTextByFont(str, fontEntries)` | Multi-font text run splitting | +| `needsUnicodeFont(str)` | Check if text needs CIDFont | +| `containsThai(str)` | Check for Thai characters | +| `resolveBidiRuns(text)` | Resolve BiDi runs (UAX #9) | +| `containsRTL(text)` | Detect RTL content | +| `shapeArabicText(str, fontData)` | Arabic GSUB positional shaping | +| `containsArabic(text)` | Detect Arabic content | +| `containsHebrew(text)` | Detect Hebrew content | +| `containsTelugu(text)` | Detect Telugu content (v1.3.0) | +| `isTeluguCodepoint(cp)` | Telugu codepoint predicate (v1.3.0) | +| `containsSinhala(text)` / `containsTibetan(text)` / `containsKhmer(text)` / `containsMyanmar(text)` / `containsEthiopic(text)` | Detect script content (v1.3.0) | +| `isSinhalaCodepoint(cp)` / `isTibetanCodepoint(cp)` / `isKhmerCodepoint(cp)` / `isMyanmarCodepoint(cp)` / `isEthiopicCodepoint(cp)` | Codepoint predicates (v1.3.0) | +| `containsMath(text)` / `isMathCodepoint(cp)` | Detect / test mathematical symbols → lang `'math'` (v1.5.0) | + +### Layout Constants + +| Constant | Description | +|----------|-------------| +| `PG_W` / `PG_H` | A4 page dimensions (points) | +| `DEFAULT_MARGINS` | Default margins `{ t, r, b, l }` | +| `DEFAULT_COLORS` | Default color palette | +| `DEFAULT_COLUMNS` | Default 5-column layout | +| `ROW_H` / `TH_H` | Row / header heights | +| `HEADER_H` | Header zone height (15pt) | +| `PAGE_SIZES` | Preset page dimensions (A4, Letter, Legal, A3, Tabloid) | +| `resolveTemplate(tpl, page, pages, title, date)` | Resolve header/footer template placeholders | + +## Ecosystem + +pdfnative ships as a library, but three official companion packages cover the most common non-library use cases — a CLI, an MCP server, and a React renderer. All live in separate repositories and depend on `pdfnative` only through the public API, so the core library stays zero-dependency. + +### pdfnative-cli — command-line interface + +[`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) v1.3.0 is the **official CLI**, built on `pdfnative` v1.6.0. It exposes 17 commands in five groups — create & edit (`render`, `fill`, `annotate`), page tree (`merge`, `split`, `extract`), security (`sign`, `verify`, `encrypt`, `decrypt`), read & extract (`inspect`, `extract-text`), and automation & meta (`batch`, `doctor`, `schema`, `completion`, `govern`) — for use in shell scripts, Makefiles, GitHub Actions, and Docker images. Zero extra runtime dependencies, npm-provenance-signed, with a CycloneDX SBOM attached to every release. + +**New in v1.1.0:** **22 Unicode scripts + COLRv1 colour emoji** through the `--font`/`--lang` shortcuts, **true constant-memory streaming** (`--stream-true`), a `--max-blocks` cap for very large documents, and a **PDF/UA (ISO 14289-1) structural validator** (`inspect --pdfua` / `--check pdfua`). It also adds an **agent-native contract** — a global `--json` status/error envelope, stable `E_*` error codes, a `--dry-run` validation mode, the new **`schema`** command (Draft 2020-12), and token-economy output projection (`--summary` / `--fields` + compact JSON) that cuts agent output ~90 %. **100 % backward-compatible.** + + +**New in v1.2.0:** five new commands — **`merge`**, **`split`**, **`extract`** (page-tree manipulation via pdfnative 1.5.0), **`annotate`** (markup annotations via incremental save, so existing signatures stay intact), and **`govern`** (the AI-governance / Human-in-the-Loop contract: `govern rules` / `govern policy` / `govern verify-issue`, with a stable `E_POLICY` error code). Plus PDF bookmarks (`--outline auto` or an explicit tree), the bundled math font (`--font math`), layout introspection (`--inspect-layout` / `--debug-layout`), and native constant-time crypto by default in `sign` (opt out with `--pure-crypto`). + +**New in v1.3.0:** five more commands on the pdfnative 1.6 engine — **`fill`** (fill, flatten, and export existing AcroForms via incremental save, encrypted PDFs included), **`encrypt`** / **`decrypt`** (AES-128/256 re-securing and password removal; RC4 never emitted), **`extract-text`** (reading-order Unicode text as text/JSON/NDJSON, `--runs`, `--password`), and **`doctor`** (offline environment preflight). Also native vector charts in `render`, `--password` + `--encrypt` re-encryption on merge/split/extract, an agent capability manifest (`schema manifest` + `llms.txt`), and PowerShell completion. + +```bash +# render with full layout coverage (encryption + watermark + PDF/A-2b) +npx pdfnative-cli render --input doc.json --output report.pdf \ + --tagged pdfa2b --compress \ + --watermark-text "DRAFT" --watermark-opacity 0.15 + +# sign with metadata and intermediate cert chain +npx pdfnative-cli sign --input report.pdf --output signed.pdf \ + --reason "Approved" --name "Finance Team" \ + --signing-time 2026-04-28T10:00:00Z \ + --cert-chain intermediate.pem + +# verify embedded signatures (byte-range + chain + trust + revocation) +npx pdfnative-cli verify --input signed.pdf --strict --trust ca-root.pem + +# inspect with CI assertions, incl. PDF/UA accessibility gate (exit 1 on failure) +npx pdfnative-cli inspect --input signed.pdf \ + --check pdfa --check signed --check pdfua --json --summary +``` + + +See the [CLI Guide](https://pdfnative.dev/guides/cli.html) for the full v1.3.0 reference, agent contract, security model, and recipes. Try the [interactive CLI playground](https://pdfnative.dev/playgrounds/cli.html) to build commands without leaving the browser. + +### pdfnative-mcp — Model Context Protocol server + +[`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) v1.6.0 is a **Model Context Protocol server** that bridges pdfnative to any MCP-compatible AI client. Once configured, your AI assistant can generate PDFs and native vector charts, embed barcodes, create, read, fill and flatten forms, sign documents through the complete PAdES ladder (B-B → B-LTA with RFC 3161 timestamps and embedded revocation material), verify signatures and document timestamps, encrypt and decrypt PDFs, validate PDF/UA structure, embed and extract attachments, extract Unicode text, render international text, merge, split and extract pages, annotate existing PDFs, rewrite document metadata, preview pagination without producing a PDF, draft governance-compliant GitHub issues (human-in-the-loop), and inspect existing PDFs — all without writing code. + + +**v1.0.0:** first stable MCP release with 12 tools, `verify_pdf`, `add_attachment` (Factur-X / ZUGFeRD PDF/A-3), `extract_text`, smart-table options, auto-placeholder signing, and `_meta.apiVersion`. + +**v1.1.0:** adds `validate_pdf`, six additional scripts (Telugu, Sinhala, Tibetan, Khmer, Myanmar, Ethiopic), and COLRv1 colour-emoji support via the pdfnative 1.3.0 engine. + +**v1.2.0:** adds `extract_attachments`, watermark options on document tools, Unicode `normalize` (NFC/NFD/NFKC/NFKD), token-frugal read modes (`verbosity`/`fields`), and returns base64 PDF bytes once via a `resource` block. + +**v1.3.0:** adds the page-tree trio `merge_pdfs` / `split_pdf` / `extract_pages` (**17 tools** total), enriched authoring options (`outline`, `pageLabels`, nested lists, `viewerPreferences`, `cellBorders`, `cellVAlign`), a constant-time `node:crypto` signing provider, and DNS-rebinding protection on the HTTP transport — all via the pdfnative 1.4.0 engine. + +**v1.4.0:** adds `annotate_pdf` (markup via incremental update) and the network-free `draft_governance_issue` (**19 tools**), the MCP `prompts` capability (`governance_contract`, `draft_issue_workflow`), `pageLabels[]` in `inspect_pdf`, and the explicit `math` script — via the pdfnative 1.5.0 engine. + +**v1.5.0:** adds `add_chart`, `read_form_fields`, `fill_form`, `encrypt_pdf`, `decrypt_pdf` (**24 tools**), `password` support on the read-only and page-tree tools, MCP resources (`pdfnative://output/…`), and tool annotations — via the pdfnative 1.6.0 engine. + +**v1.6.0:** adds `add_ltv`, `timestamp_pdf`, `update_metadata`, `inspect_layout` (**28 tools**), all 13 block kinds in `generate_basic_pdf`, layout options and build-time `encrypt` on the document tools, image watermarks, print production, charts v2, honest PDF/A (`embedFonts` / `strict` / diagnostics), PAdES timestamps on `sign_pdf`, four new recipe prompts, and the MCP 2026-07-28 spec — via the pdfnative 1.7.0 engine. + +```bash +npx -y pdfnative-mcp +``` + +### Available tools + +| Tool | Purpose | +|------|---------| +| `generate_basic_pdf` | Multi-page documents from structured blocks — all 13 block kinds since v1.6.0 (incl. tables, images, links, TOC, barcodes, SVG, form fields); optional `outline`, `pageLabels`, `pdfA`, layout options, build-time `encrypt`, print production | +| `add_table` | Smart tables (`wrap`, `repeatHeader`, `zebra`, `caption`, `minRowHeight`, `cellPadding`, `cellBorders`, `cellVAlign`) | +| `add_barcode` | QR Code, Code 128, EAN-13, Data Matrix, PDF417 | +| `add_international_text` | 25 `lang` font codes (22 Unicode scripts + `latin` + `emoji` + explicit `math`) with BiDi and OpenType shaping | +| `add_form` | Interactive AcroForm PDFs (`text`, `textarea`, `checkbox`, `radio`, `dropdown`, `listbox` since v1.6.0) | +| `embed_image` | Embed a JPEG or PNG image (base64), with `align` / `alt` since v1.6.0 | +| `prepare_signature_placeholder` | PDF with a `/Sig` field ready to be signed (optional — `sign_pdf` auto-injects one); `subFilter` / `reserveTimestamp` since v1.6.0 | +| `sign_pdf` | PAdES CMS signatures (RSA-SHA256/384/512 / ECDSA-SHA256 P-256); `profile: 'pades'`, RFC 3161 `timestamp`, cert chains, multiple signatures since v1.6.0 | +| `add_ltv` | Embed `/DSS` + `/VRI` long-term-validation material — PAdES B-LT (v1.6.0) | +| `timestamp_pdf` | Append a `/DocTimeStamp` through the operator TSA — PAdES B-LTA (v1.6.0) | +| `validate_pdf` | Read-only PDF/UA structural validation | +| `verify_pdf` | Verify every PAdES signature and `/DocTimeStamp` (integrity + value + optional chain trust; `ltv: true` reports the achieved PAdES level since v1.6.0) | +| `add_attachment` | PDF/A-3 with embedded files (Factur-X / ZUGFeRD) | +| `extract_attachments` | Extract embedded files (optionally metadata-only) | +| `extract_text` | Unicode text extraction (positioned runs; `password` since v1.5.0) | +| `merge_pdfs` | Concatenate 2–50 PDFs (encrypted sources via `password`; re-encrypt since v1.5.0) | +| `split_pdf` | Split one PDF into one document per page range | +| `extract_pages` | Pull an arbitrary, order-preserving page subset into a new PDF | +| `annotate_pdf` | Overlay markup annotations (9 types) via incremental update; not a redaction | +| `draft_governance_issue` | Governance-compliant GitHub-issue draft, network-free, never submits (HITL) | +| `add_chart` | Native vector charts, zero rasterisation — nine types since v1.6.0 (bar, horizontal bar, stacked bar, stacked horizontal bar, line, area, scatter, pie, donut), dual axis, log & time scales | +| `read_form_fields` | List an existing AcroForm's fields (types, values, options) | +| `fill_form` | Fill and optionally flatten an existing AcroForm (encrypted PDFs supported) | +| `encrypt_pdf` | Re-secure with AES-128/AES-256, owner/user passwords + permissions | +| `decrypt_pdf` | Remove encryption in-server (RC4 / AES-128 / AES-256 sources) | +| `update_metadata` | Rewrite `/Info` (+ XMP) of an existing PDF via incremental update (v1.6.0) | +| `inspect_pdf` | Structured report (metadata, pages, signatures, PDF/A, attachments, `encryptionInfo`, `pageLabels`; annotation and signature inventories, `dss`, page boxes since v1.6.0) | +| `inspect_layout` | Read-only pagination dry run — page count and block geometry, no PDF produced (v1.6.0) | + +### Claude Desktop configuration + +```json +{ + "mcpServers": { + "pdfnative": { + "command": "npx", + "args": ["-y", "pdfnative-mcp"], + "env": { + "PDFNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pdfs" + } + } + } +} +``` + +See the [MCP Integration Guide](https://pdfnative.dev/guides/mcp.html) and the [pdfnative-mcp repository](https://github.com/Nizoka/pdfnative-mcp) for configuration on Cursor, Continue, Zed, and more. + +### pdfnative-react — declarative JSX renderer + +[`pdfnative-react`](https://github.com/Nizoka/pdfnative-react) v1.1.0 turns declarative **JSX** into real, on-device PDFs powered by the zero-dependency pdfnative engine — no DOM, no headless browser, no SaaS round-trips. A custom React reconciler compiles your component tree synchronously into the pdfnative block model. Requires **React 19**, **pdfnative ^1.6.0** and **Node.js ≥ 22**. React and pdfnative are peer dependencies; the package itself adds one runtime dependency, `react-reconciler`. The pdfnative engine remains dependency-free. + +```tsx +import { Document, Heading, Text, Table, renderToBytes } from 'pdfnative-react'; + +const bytes = renderToBytes( + + Invoice #1024 + Thank you for your business. +
    + , +); // → Uint8Array, a valid PDF +``` + +Every component (`Document`, `Page`, `Section`, `Heading`, `Paragraph`/`Text`, `List`/`Item`, `Table`/`Row`/`Cell`, `Image`, `Link`, `Spacer`, `PageBreak`, `TableOfContents` (alias `Toc`), `Barcode`, `Svg`, `Chart`, `FormField`) maps 1:1 onto a pdfnative block. Render with `renderToBytes` / `renderToBlob` / `renderToStream` / `renderToFile` / `renderToFileStream` / `renderToResponse`, introspect with `inspectDocument` / `lintDocument`, preview live with the `usePdf` / `usePdfStream` hooks and `PDFViewer` / `PDFDownloadLink` / `BlobProvider` client components, or let AI agents author documents with the token-frugal `DocSpec` (terse JSON tuples that compile to the *same* PDF, validated by a versioned JSON Schema). + +See the [React Guide](https://pdfnative.dev/guides/react.html) for the full component reference, and try the [interactive React playground](https://pdfnative.dev/playgrounds/react.html) to render JSX to PDF in your browser. + +## Architecture + +``` +src/ +├── index.ts # Public API — single entry point +├── types/ +│ ├── pdf-types.ts # Core TypeScript type definitions +│ └── pdf-document-types.ts # Document builder type definitions (blocks, params) +├── core/ +│ ├── pdf-builder.ts # Table-centric PDF assembly + /Info metadata + tagged PDF +│ ├── pdf-document.ts # Free-form document builder (headings, paragraphs, lists, tables, images) +│ ├── pdf-assembler.ts # Shared PDF binary assembly primitives (xref, trailer, writer) +│ ├── encoding-context.ts # Encoding context factory (dependency inversion from fonts/) +│ ├── pdf-image.ts # JPEG/PNG parsing + PDF Image XObject builder +│ ├── pdf-text.ts # Text rendering (Latin + CIDFont + shaped + tagged) +│ ├── pdf-stream.ts # Binary utilities + download +│ ├── pdf-stream-writer.ts # AsyncGenerator streaming output +│ ├── pdf-layout.ts # Layout constants & computation +│ ├── pdf-tags.ts # Tagged PDF: structure tree, XMP metadata, ICC profile +│ ├── pdf-annot.ts # Link annotations: /URI, /GoTo, URL validation + control-char hardening +│ ├── pdf-color.ts # Color parsing, validation, normalization +│ ├── pdf-compress.ts # FlateDecode stream compression (zlib, stored-block fallback) +│ ├── pdf-watermark.ts # Text/image watermarks with ExtGState transparency +│ ├── pdf-barcode.ts # Barcode/QR code encoders + PDF path rendering (5 formats) +│ ├── pdf-svg.ts # SVG path/shape rendering as native PDF operators +│ ├── pdf-form.ts # AcroForm interactive fields with appearance streams +│ ├── pdf-signature.ts # CMS/PKCS#7 digital signatures (RSA + ECDSA) +│ └── pdf-encrypt.ts # AES-128/256 encryption, MD5, SHA-256, key derivation +├── crypto/ +│ ├── sha.ts # SHA-384, SHA-512, HMAC-SHA-256 +│ ├── asn1.ts # ASN.1 DER encoding/decoding +│ ├── rsa.ts # RSA PKCS#1 v1.5 sign/verify +│ ├── ecdsa.ts # ECDSA P-256 sign/verify +│ ├── x509.ts # X.509 certificate parsing +│ └── cms.ts # CMS SignedData (PKCS#7) builder +├── parser/ +│ ├── pdf-inflate.ts # DEFLATE decompression (zlib inflate) +│ ├── pdf-tokenizer.ts # PDF lexical scanner (ISO 32000-1 §7.2) +│ ├── pdf-object-parser.ts # PDF object parser with type guards +│ ├── pdf-xref-parser.ts # Cross-reference table/stream parser +│ ├── pdf-reader.ts # High-level PDF reader (page tree, stream decode) +│ └── pdf-modifier.ts # Incremental modification (non-destructive save) +├── fonts/ +│ ├── encoding.ts # WinAnsi + CIDFont pure encoding functions (no shaping deps) +│ ├── font-loader.ts # Configurable font registry + cache +│ ├── font-subsetter.ts # TTF subsetting engine (with buffer bounds checking) +│ └── font-embedder.ts # CMap builder + width arrays +├── shaping/ +│ ├── script-registry.ts # Centralized Unicode range constants & script predicates +│ ├── thai-shaper.ts # Thai GSUB + GPOS shaping pipeline +│ ├── bengali-shaper.ts # Bengali GSUB conjuncts + GPOS mark positioning +│ ├── tamil-shaper.ts # Tamil GSUB + split vowel decomposition +│ ├── script-detect.ts # Unicode script range detection (uses script-registry) +│ ├── multi-font.ts # Cross-script font run splitting +│ ├── bidi.ts # Unicode Bidirectional Algorithm (UAX #9) +│ └── arabic-shaper.ts # Arabic GSUB positional shaping (uses script-registry) +└── worker/ + ├── worker-api.ts # Worker/main-thread dispatch + └── pdf-worker.ts # Self-contained worker entry + +fonts/ # Pre-built font data modules (22 scripts) +tools/ # CLI: build-font-data.cjs (TTF → JS module) +scripts/ # Modular sample PDF generation (48 generators, 242 PDFs) +tests/ # 2665+ tests (122 files: unit + integration + fuzz + parser) +bench/ # Performance benchmarks (vitest bench) +``` + +## Development + +```bash +git clone https://github.com/Nizoka/pdfnative.git +cd pdfnative +npm install + +npm run build # tsup → dist/ (ESM + CJS + .d.ts) +npm run test # vitest run (2665+ tests) +npm run test:coverage # vitest with v8 coverage (95.41% statements at the v1.6.0 release; CI gates: 88/80/85/90) +npm run test:generate # Generate 242 sample PDFs → test-output/ +npm run lint # ESLint 9 + typescript-eslint strict +npm run typecheck # tsc --noEmit (src/) +npm run typecheck:tests # tsc --project tsconfig.test.json +npm run typecheck:scripts # tsc --project tsconfig.scripts.json +npm run typecheck:all # Typecheck src/ + tests/ + scripts/ +npm run bench # Performance benchmarks (vitest bench) +``` + +### Quality Metrics + +| Metric | Value | +|--------|-------| +| Tests | 2665+ (122 files) | +| Statement coverage | 95.41% (measured at the v1.6.0 release; CI enforces ≥88%, vitest.config.ts) | +| Branch coverage | 87.79% (measured at the v1.6.0 release; CI enforces ≥80%) | +| Function coverage | 98.5% (measured at the v1.6.0 release; CI enforces ≥85%; lines gate: ≥90%) | +| Fuzz tests | 89 tests in 5 files (`tests/fuzzing/`) | +| Benchmarks | Latin 500 rows ~10ms, Unicode ~13ms (Apple M1, Node 22) | +| Dependencies | 0 runtime | +| CI | Node 22/24 matrix | +| Provenance | npm signed builds | + +## Known Limitations — Visual vs. Semantic PDF + +pdfnative generates **visually pixel-perfect** PDFs for all 22 supported scripts. However, PDF is fundamentally a *visual* format (a digital printer), not a *semantic* one. This distinction matters for **text extraction** (copy-paste, `pdftotext`, screen readers): + +### Complex Text Layout (CTL) scripts + +For scripts with combining marks — **Thai**, **Devanagari**, **Vietnamese tones** — the shaper positions each mark in its own `BT…ET` block with precise GPOS offsets. PDF viewers **render** this correctly, but text extractors reconstruct content by spatial position rather than logical order. This can produce garbled output when copying text from the PDF. + +| Scenario | Visual rendering | Text extraction (Ctrl+C) | +|----------|:---:|:---:| +| Latin, Greek, Polish, Turkish | ✅ Perfect | ✅ Perfect | +| CJK (Japanese, Chinese, Korean) | ✅ Perfect | ✅ Perfect | +| Vietnamese (combining diacritics) | ✅ Perfect | ⚠️ May show Win-1252 fallback artifacts | +| Thai (GSUB + GPOS shaping) | ✅ Perfect | ⚠️ Combining marks may be reordered | +| Devanagari (matras, conjuncts) | ✅ Perfect | ⚠️ Combining marks may be reordered | +| Bengali (conjuncts, GPOS marks) | ✅ Perfect | ⚠️ Combining marks may be reordered | +| Tamil (split vowels, GSUB) | ✅ Perfect | ⚠️ Split vowel recomposition may fail | + +### Why this happens + +This is an inherent limitation of the PDF spec (ISO 32000-1), not a bug in pdfnative. The ToUnicode CMap correctly maps glyph IDs back to Unicode code points, but extractors that rely on spatial reconstruction rather than CMap lookup will produce artifacts. This behavior is shared by most PDF generators that don't use Tagged PDF. + +### Tagged PDF, /ActualText & PDF/A — Implemented ✅ + +All three roadmap items are now implemented and available via the `tagged` layout option: + +```ts +const pdf = buildPDFBytes(params, { tagged: true }); // PDF/A-2b (default) +const pdf1b = buildPDFBytes(params, { tagged: 'pdfa1b' }); // PDF/A-1b (legacy) +const pdf2u = buildPDFBytes(params, { tagged: 'pdfa2u' }); // PDF/A-2u (Unicode) +``` + +When `tagged` is set, the output includes: + +- **Tagged PDF (PDF/UA)** — full structure tree (`/Document → /Table → /TR → /TH|/TD`, `/H1-H3`, `/P`, `/L → /LI`, `/Figure`, `/Link`) with `/Span` marked content operators and `/StructParents` on every page +- **/ActualText** — original Unicode string attached as UTF-16BE hex to every `/Span BDC...EMC` sequence, solving text extraction for GPOS-repositioned glyphs (Thai, Arabic, Devanagari) +- **PDF/A-2b compliance** (default) — PDF 1.7, XMP metadata with `pdfaid:part=2` + `pdfaid:conformance=B`, sRGB ICC OutputIntent (`GTS_PDFA1`), `/MarkInfo << /Marked true >>` on Catalog +- **PDF/A-1b compatibility** — explicit `tagged: 'pdfa1b'` uses PDF 1.4, `pdfaid:part=1` +- **PDF/A-2u variant** — `tagged: 'pdfa2u'` uses PDF 1.7, `pdfaid:conformance=U` + +The `tagged` option is backward-compatible — omitting it or setting `false` produces the same output as before. + +> **PDF/A status (v1.1.0).** Every PDF/A-claiming sample now passes +> the **veraPDF** reference validator (1b / 2b / 2u / 3b) when the +> Latin font module is registered. Trailer `/ID` and +> `/Info CreationDate` are byte-equivalent to `xmp:CreateDate` +> (with timezone offset). ``, ``, +> `` mirror `/Info /Title`, `/Subject`, `/Keywords` +> byte-for-byte (ISO 19005-1 §6.7.3 t1 / t4 / t5). Object 3 / Object 4 +> are emitted as Type0 redirector dicts pointing to the embedded +> CIDFontType2 chain — no more unembedded `Helvetica` references +> (ISO 19005-1 §6.3.4 / ISO 19005-2 §6.2.11.4.1). To produce strict +> PDF/A: +> +> ```ts +> import { registerFont } from 'pdfnative'; +> registerFont('latin', () => import('pdfnative/fonts/noto-sans-data.js')); +> ``` +> +> Run `npm run validate:pdfa` locally (with veraPDF installed, see +> [docs/guides/pdfa.html](docs/guides/pdfa.html)) to verify against +> the reference validator. CI runs veraPDF as a blocking check. + +### PDF Encryption — Implemented ✅ + +AES-128 and AES-256 encryption with owner/user passwords and granular permissions: + +```ts +const pdf = buildPDFBytes(params, { + encryption: { + ownerPassword: 'owner123', // Required — full access password + userPassword: 'user456', // Optional — password to open the PDF + algorithm: 'aes128', // 'aes128' (default) or 'aes256' + permissions: { + print: true, // Allow printing (default: true) + copy: false, // Allow copy/paste (default: false) + modify: false, // Allow modification (default: false) + extractText: true, // Allow text extraction (default: true) + }, + }, +}); +``` + +| Algorithm | PDF Version | Revision | Key Length | CFM | +|-----------|------------|----------|------------|-----| +| `aes128` | 1.4 | R4 (V4) | 128-bit | /AESV2 | +| `aes256` | 1.4 | R6 (V5) | 256-bit | /AESV3 | + +**Note:** PDF/A and encryption are mutually exclusive (ISO 19005-1 §6.3.2). Setting both `tagged` and `encryption` will throw an error. + +## Typography Convention: En-Dash Separator + +pdfnative uses **en-dash** `–` (U+2013) with surrounding spaces as the standard title and footer separator: + +``` +"Arabic Script Coverage – الأبجدية العربية" ✅ recommended +"Arabic Script Coverage — الأبجدية العربية" ⚠️ works, but wider gap +``` + +**Why en-dash?** + +| Property | Em-dash `—` (U+2014) | En-dash `–` (U+2013) | +|----------|:---:|:---:| +| Helvetica width | 1000 units (1 em) | 556 units (0.56 em) | +| Visual gap at 16pt | ~24pt with spaces | ~18pt with spaces | +| WinAnsi encodable | ✅ (0x97) | ✅ (0x96) | +| International standard | US English only | ISO / Europe / technical | +| Cursive script rendering | Disproportionate gap | Balanced spacing | + +The en-dash is **44% narrower** than the em-dash and follows ISO/international typography standards. This eliminates disproportionate visual gaps in cursive scripts (Arabic, Thai) where compact shaped text amplifies the perceived space around wider separators. + +Both em-dash and en-dash are **fully supported** by the library (encoding, width metrics, BiDi classification) — this is a typographic recommendation for the best cross-script visual balance, not a restriction. + +## Stream Compression (FlateDecode) + +Enable FlateDecode compression for dramatically smaller PDFs: + +```typescript +import { initNodeCompression, buildPDFBytes } from 'pdfnative'; + +// Initialize native zlib (required once in ESM context) +await initNodeCompression(); + +const pdf = buildPDFBytes(params, { compress: true }); +``` + +| Stream Type | Compressed? | Typical Reduction | +|-------------|:-----------:|:-----------------:| +| Page content (text operators) | ✅ | 80–90% | +| FontFile2 (TTF subset) | ✅ | 60–80% | +| ToUnicode CMap | ✅ | 80–90% | +| ICC sRGB profile | ✅ | 40–60% | +| XMP metadata | ❌ (tagged mode) | — | +| JPEG image | ❌ (already DCTDecode) | — | +| PNG image | ❌ (already FlateDecode) | — | + +### Compression + Encryption + +Both features compose correctly — compression is applied **before** encryption per ISO 32000-1 §7.3.8: + +```typescript +const pdf = buildPDFBytes(params, { + compress: true, + encryption: { + ownerPassword: 'owner123', + algorithm: 'aes128', + }, +}); +``` + +### Platform Support + +| Runtime | Compression Method | Performance | +|---------|-------------------|-------------| +| Node.js 22+ | `zlib.deflateSync()` (native C) | Optimal | +| Browser | Stored-block fallback (valid FlateDecode) | No size reduction | +| Deno / Bun | CJS require fallback | Depends on compat layer | + +For browser contexts with full compression, call `setDeflateImpl()` with a custom DEFLATE function. + +## Browser & Runtime Compatibility + +pdfnative targets ES2020 and works in any environment that supports `Uint8Array`, `TextEncoder`, and `crypto.getRandomValues()`. + +| Runtime | Version | Status | Notes | +|---------|---------|:------:|-------| +| Node.js | 22, 24+ | ✅ Tested in CI | Full support (ESM + CJS) | +| Chrome | 80+ | ✅ | ESM via bundler or `\n \n`; + shell = shell.replace(LD_BLOCK_RE, ''); + shell = shell.replace(/<\/head>/, `${ldBlock}`); + + return shell; +} + +/** All guide shells that pair with a Markdown source, alphabetical. */ +export function listGuideShells(root: string): string[] { + const dir = join(root, 'docs', 'guides'); + return readdirSync(dir) + .filter((f) => f.endsWith('.html') && f !== 'index.html') + .filter((f) => existsSync(join(dir, f.replace(/\.html$/, '.md')))) + .sort(); +} + +const isMain = process.argv[1] && resolve(process.argv[1]).includes('build-guides'); +if (isMain) { + const root = resolve(import.meta.dirname, '..'); + let changed = 0; + for (const htmlName of listGuideShells(root)) { + const path = join(root, 'docs', 'guides', htmlName); + const before = readFileSync(path, 'utf8'); + const after = applyGuideRender(root, htmlName); + if (before !== after) { + writeFileSync(path, after); + changed++; + } + } + console.log(`build-guides: ${changed} shell(s) updated, ${listGuideShells(root).length} total.`); +} diff --git a/scripts/build-llms-full.ts b/scripts/build-llms-full.ts index e94fe51..5edf4b5 100644 --- a/scripts/build-llms-full.ts +++ b/scripts/build-llms-full.ts @@ -1,20 +1,29 @@ #!/usr/bin/env tsx /** - * pdfnative — llms-full.txt generator - * ==================================== - * Emits `docs/llms-full.txt`: the root `llms.txt` index followed by the full - * Markdown source of every guide, so an agent can ingest the whole - * documentation set in a single request (the llmstxt.org "full" convention). + * pdfnative — llms-full.txt + llms-index.json generator + * ===================================================== + * Emits two machine-readable artefacts, both committed and both policed by + * `scripts/verify-docs.ts` (rules `llms-sync` and `llms-index-sync`): + * + * - `docs/llms-full.txt` — the root `llms.txt` index followed by the full + * Markdown source of the README and of every guide, so an agent can ingest + * the whole documentation set in a single request (the llmstxt.org "full" + * convention). + * - `docs/llms-index.json` — a per-page machine index: HTML and raw-Markdown + * URL, title, summary, section anchors, and exact byte / approximate token + * sizes, so an agent can decide what to fetch before spending the tokens. + * Sizes live here rather than in `llms.txt` itself so no artefact has to + * embed its own size (which would be circular). * * The output is deterministic (alphabetical guide order, LF line endings), so - * `scripts/verify-docs.ts` can rebuild it in memory and fail the build when the - * committed copy is stale. + * the verifier can rebuild both in memory and fail the build when a committed + * copy is stale. * * Usage: - * npx tsx scripts/build-llms-full.ts # rewrite docs/llms-full.txt + * npx tsx scripts/build-llms-full.ts # rewrite both artefacts */ -import { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs'; +import { readFileSync, readdirSync, writeFileSync, existsSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; function lf(text: string): string { @@ -28,6 +37,12 @@ export function buildLlmsFull(root: string): string { const index = join(root, 'llms.txt'); if (existsSync(index)) parts.push(lf(readFileSync(index, 'utf8')).trimEnd()); + const readme = join(root, 'README.md'); + if (existsSync(readme)) { + parts.push('\n\n---\n\n'); + parts.push(lf(readFileSync(readme, 'utf8')).trimEnd()); + } + const guidesDir = join(root, 'docs', 'guides'); const guides = existsSync(guidesDir) ? readdirSync(guidesDir).filter((f) => f.endsWith('.md')).sort() @@ -40,10 +55,103 @@ export function buildLlmsFull(root: string): string { return parts.join('') + '\n'; } +// ── llms-index.json ───────────────────────────────────────────────── + +/** Same GitHub-style slugger as scripts/build-guides.ts heading ids. */ +function slugify(text: string): string { + return text + .replace(/`/g, '') + .trim() + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, '') + .replace(/\s+/g, '-'); +} + +function approxTokens(bytes: number): number { + // Rough English-text heuristic (~4 bytes per token); good enough for an + // agent's fetch-or-not decision, and labelled as approximate in the output. + return Math.round(bytes / 4); +} + +interface IndexPage { + title: string; + summary: string; + html: string; + markdown: string; + anchors: string[]; + bytes: number; + approxTokens: number; +} + +interface LlmsIndex { + $comment: string; + site: string; + artefacts: Array<{ url: string; description: string; bytes: number; approxTokens: number }>; + guides: IndexPage[]; +} + +/** Build the machine index for the repo rooted at `root`. */ +export function buildLlmsIndex(root: string): string { + const site = 'https://pdfnative.dev'; + const guidesDir = join(root, 'docs', 'guides'); + const guides = readdirSync(guidesDir).filter((f) => f.endsWith('.md')).sort(); + + const pages: IndexPage[] = []; + for (const name of guides) { + const md = lf(readFileSync(join(guidesDir, name), 'utf8')); + const title = md.match(/^# (.+)$/m)?.[1].replace(/`/g, '').trim() ?? name; + const quote = md.match(/^> \*\*([\s\S]*?)$/m)?.[0] ?? ''; + const firstPara = md.split(/\n\n+/).find((p) => /^[A-Za-z[]/.test(p.trim())) ?? ''; + const summary = (quote || firstPara) + .replace(/^> /gm, '') + .replace(/\*\*/g, '') + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') + .replace(/\s+/g, ' ') + .trim() + .slice(0, 300); + const anchors = [...md.matchAll(/^## (.+)$/gm)].map((m) => slugify(m[1])); + const bytes = Buffer.byteLength(md, 'utf8'); + pages.push({ + title, + summary, + html: `${site}/guides/${name.replace(/\.md$/, '.html')}`, + markdown: `${site}/guides/${name}`, + anchors, + bytes, + approxTokens: approxTokens(bytes), + }); + } + + const artefactList: LlmsIndex['artefacts'] = []; + const artefactSources: Array<[string, string, string]> = [ + [`${site}/llms.txt`, join(root, 'llms.txt'), 'Documentation index (llmstxt.org convention).'], + [`${site}/llms-full.txt`, join(root, 'docs', 'llms-full.txt'), 'Full corpus: index + README + every guide, one request.'], + ['https://github.com/Nizoka/pdfnative/blob/main/README.md', join(root, 'README.md'), 'Complete feature and API reference (also embedded in llms-full.txt).'], + ]; + for (const [url, path, description] of artefactSources) { + if (!existsSync(path)) continue; + const bytes = statSync(path).size; + artefactList.push({ url, description, bytes, approxTokens: approxTokens(bytes) }); + } + + const out: LlmsIndex = { + $comment: + 'Machine index of the pdfnative documentation. Generated by scripts/build-llms-full.ts; verified by the llms-index-sync rule of scripts/verify-docs.ts. Token counts are approximate (bytes / 4).', + site, + artefacts: artefactList, + guides: pages, + }; + return JSON.stringify(out, null, 2) + '\n'; +} + const isMain = process.argv[1] && resolve(process.argv[1]).includes('build-llms-full'); if (isMain) { const root = resolve(import.meta.dirname, '..'); const out = join(root, 'docs', 'llms-full.txt'); writeFileSync(out, buildLlmsFull(root)); console.log(`build-llms-full: wrote ${out}`); + // Index second: it reports llms-full.txt's on-disk size. + const idx = join(root, 'docs', 'llms-index.json'); + writeFileSync(idx, buildLlmsIndex(root)); + console.log(`build-llms-full: wrote ${idx}`); } diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index 238c487..ef68d59 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -931,6 +931,51 @@ if (!existsSync(LLMS_FULL)) { fail('docs/llms-full.txt', 1, 'llms-sync', 'stale — regenerate with `npx tsx scripts/build-llms-full.ts`'); } +// ── Rule: llms-index-sync ─────────────────────────────────────────── + +/** + * `docs/llms-index.json` tells an agent what every page costs before it + * spends the tokens (URLs, anchors, exact bytes). A stale index quietly + * advertises last release's sizes and anchors, so it is policed exactly like + * `llms-full.txt`. Runs after llms-sync: the index reports the on-disk size + * of llms-full.txt, which the previous rule has just proven fresh. + */ +const { buildLlmsIndex } = await import('./build-llms-full.ts'); + +const LLMS_INDEX = join(ROOT, 'docs', 'llms-index.json'); +if (!existsSync(LLMS_INDEX)) { + fail('docs/llms-index.json', 1, 'llms-index-sync', 'missing — generate it with `npm run docs:llms`'); +} else if (read(LLMS_INDEX).replace(/\r\n/g, '\n') !== buildLlmsIndex(ROOT)) { + fail('docs/llms-index.json', 1, 'llms-index-sync', 'stale — regenerate with `npm run docs:llms`'); +} + +// ── Rule: guide-render-sync ───────────────────────────────────────── + +/** + * Guide shells carry the pre-rendered HTML of their Markdown source (between + * `guide:render` markers) plus server-side JSON-LD, so crawlers that do not + * execute JavaScript — most AI fetchers — receive the full guide instead of + * "Loading…". The renderer is `scripts/build-guides.ts`; this rule rebuilds + * every shell in memory and fails when a committed copy is stale, exactly as + * `llms-sync` polices `llms-full.txt`. A shell whose article has never been + * generated (no marker) fails too: an empty article is the defect this whole + * mechanism exists to remove. + */ +const { applyGuideRender, listGuideShells } = await import('./build-guides.ts'); + +for (const htmlName of listGuideShells(ROOT)) { + const relPath = `docs/guides/${htmlName}`; + const committed = read(join(ROOT, 'docs', 'guides', htmlName)).replace(/\r\n/g, '\n'); + if (!committed.includes('')) { + fail(relPath, 1, 'guide-render-sync', 'article is not pre-rendered — run `npm run docs:guides`'); + continue; + } + const expected = applyGuideRender(ROOT, htmlName); + if (committed !== expected) { + fail(relPath, 1, 'guide-render-sync', 'stale — the committed render differs from its Markdown source; run `npm run docs:guides`'); + } +} + // ── Rule: playground-syntax ───────────────────────────────────────── /** @@ -1049,6 +1094,8 @@ const OFFLINE_RULES = [ 'bench-parity', 'contrast', 'llms-sync', + 'llms-index-sync', + 'guide-render-sync', 'playground-syntax', ] as const; diff --git a/tests/docs/verify-docs.test.ts b/tests/docs/verify-docs.test.ts index c32d447..73d30fe 100644 --- a/tests/docs/verify-docs.test.ts +++ b/tests/docs/verify-docs.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; -import { readFileSync, writeFileSync, mkdtempSync, rmSync, cpSync, existsSync } from 'node:fs'; +import { readFileSync, writeFileSync, mkdtempSync, rmSync, cpSync, existsSync, symlinkSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; @@ -59,6 +59,9 @@ function makeSandbox(): string { const from = join(ROOT, file); if (existsSync(from)) cpSync(from, join(dir, file)); } + // scripts/build-guides.ts imports `marked` from node_modules; link the real + // install into the sandbox (junction: no admin rights needed on Windows). + symlinkSync(join(ROOT, 'node_modules'), join(dir, 'node_modules'), 'junction'); return dir; } @@ -125,6 +128,26 @@ describe('verify-docs', () => { }); }, 120_000); + it('guide-render-sync catches a shell whose Markdown source moved on', () => { + withSandbox((dir) => { + // Edit the .md without regenerating the pre-rendered shell. + patch(dir, 'docs/guides/charts.md', '# Charts (native vector)', '# Charts (native vector, perturbed)'); + const run = runVerifier(dir); + expect(run.output).toContain('guide-render-sync'); + expect(run.output).toContain('docs/guides/charts.html'); + expect(run.status).toBe(1); + }); + }, 120_000); + + it('llms-index-sync catches a stale machine index', () => { + withSandbox((dir) => { + patch(dir, 'docs/llms-index.json', '"site": "https://pdfnative.dev"', '"site": "https://perturbed.example"'); + const run = runVerifier(dir); + expect(run.output).toContain('llms-index-sync'); + expect(run.status).toBe(1); + }); + }, 120_000); + it('bench-parity ties the homepage figures to bench/RESULTS.md', () => { withSandbox((dir) => { patch(dir, 'docs/index.html', '~98 ms', '~33 ms'); From 5c7e30ff89f25135fb87041a856352ff3a37cf4e Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:59:07 +0200 Subject: [PATCH 07/15] docs(site): live demo with inline PDF preview, agent brief, answer-first guides The homepage demo becomes a split view: the example code runs as a real ES module through a Blob URL (its import-from-pdfnative lines are real, top-level await works natively, and errors carry genuine line numbers instead of the old new Function + import-stripping regex), downloadBlob is routed to an inline PDF preview iframe - the same Blob/object-URL pattern the React playground already used - and the first example renders automatically when the section scrolls into view, so a visitor sees a real PDF without clicking. Download becomes an explicit button, navigator.pdfViewerEnabled gates an honest no-inline-preview note for mobile browsers, and #preset= permalinks restore and share the selected example. No new dependency, no code-encoding permalinks. docs/agent-brief.md is the new paste-into-context pack for coding agents: the core API table, the six verified pitfalls that produce wrong code (synchronous builder, top-level title, reserved /F1-/F2, registerFont-without-fontEntries, PDF/A claim in layout, and the pdfkit/jsPDF ghost APIs - named under explicit verify-docs:allow markers), the surface decision tree, and the generate-inspect-assert self-verification loop. Linked from llms.txt, llms-index.json, the sitemap, and a "Copy as prompt" button in the homepage MCP section. It joins the verify-docs corpus (110 files), so its counters and identifiers are policed like every other page. Six guides that opened without an answer-first summary (accessibility, architecture, pdfa, quickstart, signatures, troubleshooting) gain one, extractable by answer engines; signatures.md''s intro claim "both with SHA-256" is corrected (RSA gained SHA-384/512 in 1.7.0). The nine learn pages share a new learn/learn.css instead of nine duplicated inline style blocks. verify:docs 19 rules green; docs suite 20/20. --- docs/agent-brief.md | 124 +++++++++++++++++++ docs/app.js | 179 +++++++++++++++++++-------- docs/guides/accessibility.html | 3 + docs/guides/accessibility.md | 2 + docs/guides/architecture.html | 3 + docs/guides/architecture.md | 2 + docs/guides/pdfa.html | 3 + docs/guides/pdfa.md | 2 + docs/guides/quickstart.html | 3 + docs/guides/quickstart.md | 2 + docs/guides/signatures.html | 5 +- docs/guides/signatures.md | 4 +- docs/guides/troubleshooting.html | 3 + docs/guides/troubleshooting.md | 2 + docs/index.html | 26 ++-- docs/learn/01-install.html | 16 +-- docs/learn/02-blocks.html | 16 +-- docs/learn/03-tables.html | 16 +-- docs/learn/04-page-furniture.html | 16 +-- docs/learn/05-fonts-and-scripts.html | 16 +-- docs/learn/06-browser.html | 16 +-- docs/learn/07-going-big.html | 16 +-- docs/learn/08-next-steps.html | 16 +-- docs/learn/index.html | 14 +-- docs/learn/learn.css | 19 +++ docs/llms-full.txt | 15 ++- docs/llms-index.json | 50 ++++---- docs/llms.txt | 1 + docs/sitemap.xml | 8 ++ docs/style.css | 34 +++++ llms.txt | 1 + scripts/build-llms-full.ts | 1 + 32 files changed, 415 insertions(+), 219 deletions(-) create mode 100644 docs/agent-brief.md create mode 100644 docs/learn/learn.css diff --git a/docs/agent-brief.md b/docs/agent-brief.md new file mode 100644 index 0000000..d3100c4 --- /dev/null +++ b/docs/agent-brief.md @@ -0,0 +1,124 @@ +# pdfnative — agent brief + +> A compact, paste-into-your-context briefing for AI agents writing code with +> [pdfnative](https://pdfnative.dev). Everything here is declarative and +> verified against the source tree by the repository's documentation CI. +> Longer forms: [llms.txt](https://pdfnative.dev/llms.txt) (index), +> [llms-full.txt](https://pdfnative.dev/llms-full.txt) (full corpus), +> [llms-index.json](https://pdfnative.dev/llms-index.json) (per-page sizes and anchors). + +## What it is + +pdfnative is a zero-runtime-dependency TypeScript library that generates and +parses ISO 32000-1 (PDF 1.7) and ISO 19005 (PDF/A) conformant PDFs on-device — +Node ≥ 22, browsers, Deno, Bun, Web Workers. No SaaS round-trip, no telemetry, +no sockets. Current version: 1.7.0. It writes (documents, tables, charts, +barcodes, SVG, forms, watermarks, signatures with long-term validation, print +production) and reads (parse, decrypt, extract text, read/fill/flatten forms, +merge/split/extract pages, verify structure) — 22 Unicode scripts with real +OpenType shaping and UAX #9 BiDi. + +## Choose your surface + +- **Writing application code** → the library: `npm install pdfnative`, `import { … } from 'pdfnative'`. +- **Driving a shell, CI, or Makefile** → `pdfnative-cli` (17 commands, JSON-in/JSON-out agent contract with stable `E_*` error codes). +- **You are a conversational assistant with tool access** → `pdfnative-mcp` (28 tools, MCP 2026-07-28 spec; config: `npx -y pdfnative-mcp`). +- **The host app is React 19** → `pdfnative-react` (declarative JSX compiled on-device to pdfnative blocks). + +All four produce the same PDFs from the same engine. Details: [onboarding](https://pdfnative.dev/guides/onboarding.md). + +## The core API (library) + +```ts +import { buildDocumentPDFBytes } from 'pdfnative'; + +// Synchronous — returns a Uint8Array, not a Promise. +const bytes = buildDocumentPDFBytes({ + title: 'Invoice 42', // top-level, not inside metadata + metadata: { author: 'Me' }, // author / subject / keywords only + blocks: [ + { type: 'heading', text: 'Invoice 42', level: 1 }, + { type: 'paragraph', text: 'Thank you for your order.' }, + { type: 'table', headers: ['Item', 'Price'], rows: [{ cells: ['Widget', '€10'] }] }, + ], + layout: { tagged: 'pdfa2b' }, // optional PDF/A claim lives in layout +}); +// Node: await fs.writeFile('out.pdf', bytes); +// Browser: new Blob([bytes], { type: 'application/pdf' }); +``` + +Thirteen block kinds: `heading`, `paragraph`, `list`, `table`, `image`, `link`, +`toc`, `barcode`, `svg`, `formField`, `chart`, `pageBreak`, `spacer`. + +Functions an agent reaches for most, all exported from `'pdfnative'`: + +| Export | Role | +|---|---| +| `buildDocumentPDFBytes(params)` | Document builder (blocks) → `Uint8Array`. Synchronous. | +| `buildPDFBytes(params)` | Table-centric builder (headers/rows) → `Uint8Array`. | +| `registerFont(lang, loader)` / `loadFontData(lang)` | Enable a non-Latin script; pass the result via `fontEntries`. | +| `downloadBlob(bytes, name)` | Browser download helper. | +| `inspectDocumentLayout(params)` | Pagination dry run — page count and block geometry, no PDF produced. | +| `extractText(bytes, options?)` | Reading-order Unicode text (+ positioned runs) from an existing PDF. | +| `openPdf(bytes, { password? })` | Parse (and decrypt) an existing PDF — metadata, pages, encryption info. | +| `validatePdfUA(bytes)` | Read-only PDF/UA structural check → `{ valid, errors, warnings }`. | +| `readFormFields` / `fillForm` / `flattenForm` | AcroForm round-trip on existing PDFs (incremental update; encrypted sources supported). | +| `mergePdfs` / `splitPdf` / `extractPages` | Page-tree manipulation (with streaming variants). | +| `signPdfBytes(bytes, options)` | PAdES CMS signature (RSA-SHA256/384/512, ECDSA P-256); `addSignaturePlaceholder` prepares the `/Sig` field. | +| `listSignatures(bytes)` | Inventory of signatures and document timestamps. | +| `buildDocumentPDFStreamTrue(params)` | Constant-memory streaming for very large documents. | + +## What agents get wrong (verified pitfalls) + +1. **`buildDocumentPDFBytes` is synchronous.** It returns a `Uint8Array`, not a + Promise — do not `await` it (harmless) and do not `.then()` it (breaks). +2. **`title` is top-level**, not inside `metadata` (`metadata` takes only + `author` / `subject` / `keywords`). +3. **`registerFont` alone is a no-op.** You must also `await loadFontData(lang)` + and pass the result in `fontEntries: [{ fontData, fontRef, lang }]`. +4. **`/F1` and `/F2` are reserved font refs** — start custom `fontRef` at `/F3`. +5. **The PDF/A claim lives in `layout`** (`layout: { tagged: 'pdfa2b' }`), not at + the top level; it is mutually exclusive with encryption. A claim on base-14 + text needs embedded fonts to pass veraPDF (see the + [PDF/A guide](https://pdfnative.dev/guides/pdfa.md)). +6. **This is not pdfkit / jsPDF / pdf-lib.** There is no `new PDFDocument()`, + no `doc.text(…)`, no `pdf.save()`, no `doc.pipe(…)` — documents are plain + data (`blocks` arrays) passed to pure functions. + + `streamDocumentPdf`, `streamPdf` and `buildPdfStream` have never existed; + + the streaming exports are `buildDocumentPDFStream`, `buildPDFStream` and their `…True` variants. + +## Verify your own output + +pdfnative can read what it writes — use that to close the loop instead of +shipping blind: + +```ts +import { buildDocumentPDFBytes, inspectDocumentLayout, extractText, validatePdfUA } from 'pdfnative'; + +const params = { title: 'Report', blocks: [/* … */] }; + +// Before generating: how will it paginate? +const layout = inspectDocumentLayout(params); +if (layout.totalPages > 3) { /* tighten the layout */ } + +const bytes = buildDocumentPDFBytes(params); + +// After generating: is the content really there? Is the structure valid? +const pages = extractText(bytes); // → ExtractedPageText[], one per page +if (!pages[0].text.includes('Report')) throw new Error('content missing'); +const ua = validatePdfUA(bytes); +if (!ua.valid) console.warn(ua.errors); +``` + +The same loop exists on every surface: `pdfnative-cli inspect --check … --json` +(exit 1 on failure), and the MCP tools `inspect_pdf`, `inspect_layout`, +`validate_pdf`, `verify_pdf`. + +## Where to read more + +- [Quick start](https://pdfnative.dev/guides/quickstart.md) · [Onboarding](https://pdfnative.dev/guides/onboarding.md) — first PDF in each surface. +- [MCP guide](https://pdfnative.dev/guides/mcp.md) — the 28 tools, schemas, error codes. +- [CLI guide](https://pdfnative.dev/guides/cli.md) — 17 commands and the `--json` / `E_*` agent contract. +- Every guide serves raw Markdown at the same URL with `.md`; sizes and anchors are in [llms-index.json](https://pdfnative.dev/llms-index.json). diff --git a/docs/app.js b/docs/app.js index f5a4b74..e86855c 100644 --- a/docs/app.js +++ b/docs/app.js @@ -70,6 +70,22 @@ }); }); + // ── Copy-a-URL's-content buttons (e.g. the agent brief) ─── + document.querySelectorAll('[data-copy-url]').forEach(function (btn) { + btn.addEventListener('click', function () { + var url = btn.getAttribute('data-copy-url'); + fetch(url, { cache: 'no-cache' }) + .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }) + .then(function (text) { return navigator.clipboard.writeText(text); }) + .then(function () { + var prev = btn.textContent; + btn.textContent = '✓ Copied'; + setTimeout(function () { btn.textContent = prev; }, 1500); + }) + .catch(function () { btn.textContent = 'Copy failed'; }); + }); + }); + // ── Code tabs ───────────────────────────────────────────── var tabBtns = document.querySelectorAll('.tab-btn'); var tabPanels = document.querySelectorAll('.tab-panel'); @@ -159,6 +175,10 @@ var demoReset = document.getElementById('demo-reset'); var demoDescription = document.getElementById('demo-description'); var demoSourceLink = document.getElementById('demo-source-link'); + var demoDownload = document.getElementById('demo-download'); + var demoShare = document.getElementById('demo-share'); + var demoPreview = document.getElementById('demo-preview'); + var demoPreviewNote = document.getElementById('demo-preview-note'); var pdfnativeModule = null; // ── Examples gallery ────────────────────────────────────── @@ -591,6 +611,8 @@ // ── Populate picker and select default ────────────────── var DEFAULT_ID = 'quickstart'; var currentId = null; + var lastPdf = null; // { bytes, name } captured from the last run + var lastPreviewUrl = null; function loadExample(id) { var ex = EXAMPLES.find(function (e) { return e.id === id; }); @@ -610,6 +632,11 @@ opt.textContent = ex.label; demoPicker.appendChild(opt); }); + // `#preset=` permalinks restore the selected example. + var presetMatch = /(?:^|[#&])preset=([\w-]+)/.exec(location.hash); + if (presetMatch && EXAMPLES.some(function (e) { return e.id === presetMatch[1]; })) { + DEFAULT_ID = presetMatch[1]; + } demoPicker.value = DEFAULT_ID; demoPicker.addEventListener('change', function () { loadExample(demoPicker.value); }); } @@ -620,77 +647,121 @@ loadExample(DEFAULT_ID); - demoBtn.addEventListener('click', async function () { + // ── Inline PDF preview ────────────────────────────────── + // Same Blob → object-URL pattern as the React playground. Some browsers + // (most mobile ones) cannot render PDFs in an iframe; when the engine + // says so, be honest about it instead of showing an empty frame. + var canPreview = navigator.pdfViewerEnabled !== false; + if (!canPreview && demoPreview && demoPreviewNote) { + demoPreview.hidden = true; + demoPreviewNote.hidden = false; + } + + function showPreview(bytes) { + if (!demoPreview || !canPreview) return; + var blob = new Blob([bytes], { type: 'application/pdf' }); + if (lastPreviewUrl) URL.revokeObjectURL(lastPreviewUrl); + lastPreviewUrl = URL.createObjectURL(blob); + demoPreview.src = lastPreviewUrl; + } + + // The demo module calls `downloadBlob` like real pdfnative code; the demo + // routes those bytes to the preview pane and only downloads on request. + function captureSink(bytes, name) { + lastPdf = { bytes: bytes, name: name || 'document.pdf' }; + showPreview(bytes); + if (demoDownload) demoDownload.disabled = false; + } + + if (demoDownload) { + demoDownload.addEventListener('click', function () { + if (lastPdf && pdfnativeModule) pdfnativeModule.downloadBlob(lastPdf.bytes, lastPdf.name); + }); + } + + if (demoShare) { + demoShare.addEventListener('click', function () { + var url = location.origin + location.pathname + '#preset=' + (currentId || DEFAULT_ID); + navigator.clipboard.writeText(url).then(function () { + var prev = demoShare.textContent; + demoShare.textContent = '✓ Copied'; + setTimeout(function () { demoShare.textContent = prev; }, 1500); + }); + }); + } + + // ── Execution: real ES module via a Blob URL ───────────── + // The example code is executed as an actual module, so its + // `import { … } from 'pdfnative'` lines are real, top-level `await` + // works natively, and errors carry genuine line numbers. The pdfnative + // specifier is resolved to the already-loaded module through a global + // (no second CDN request), with `downloadBlob` routed to the preview. + function rewriteImports(code) { + return code.replace( + /^(\s*)import\s*\{([^}]+)\}\s*from\s*['"]pdfnative['"]\s*;?\s*$/gm, + '$1const {$2} = globalThis.__pdfnativeDemo.mod;' + ); + } + + function lineFromStack(err) { + var m = /blob:[^\s)]+:(\d+):\d+/.exec(err && err.stack ? err.stack : ''); + return m ? Number(m[1]) : null; + } + + async function runDemo() { demoStatus.textContent = 'Loading pdfnative…'; demoError.style.display = 'none'; demoError.textContent = ''; demoBtn.disabled = true; + var moduleUrl = null; try { // Lazy-load pdfnative from ESM CDN on first use if (!pdfnativeModule) { pdfnativeModule = await loadPdfnative(); - demoStatus.textContent = 'Generating PDF…'; } + demoStatus.textContent = 'Generating PDF…'; - // Extract user code and execute - var code = demoCode.value; - - // Strip top-level static `import {…} from 'pdfnative'` statements — - // we provide those bindings via the function arguments below. - // Keep dynamic `import('…')` calls intact for examples that need them - // (e.g. multi-language font modules). - var cleanCode = code - .replace(/^\s*import\s*\{[^}]+\}\s*from\s*['"]pdfnative['"]\s*;?/gm, '') - .trim(); - - // Wrap in async IIFE so user code can use top-level `await` - var wrapped = '"use strict"; return (async () => {\n' + cleanCode + '\n})();'; - - var fn = new Function( - 'buildPDFBytes', 'buildDocumentPDFBytes', 'downloadBlob', - 'buildPDF', 'buildDocumentPDF', 'wrapText', - 'buildDocumentPDFStream', 'buildPDFStream', 'concatChunks', - 'registerFonts', 'loadFontData', - 'initNodeCompression', 'signPdfBytes', - 'extractText', 'mergePdfs', 'splitPdf', 'extractPages', - 'openPdf', 'readFormFields', 'fillForm', 'flattenForm', - wrapped - ); - - await fn( - pdfnativeModule.buildPDFBytes, - pdfnativeModule.buildDocumentPDFBytes, - pdfnativeModule.downloadBlob, - pdfnativeModule.buildPDF, - pdfnativeModule.buildDocumentPDF, - pdfnativeModule.wrapText, - pdfnativeModule.buildDocumentPDFStream, - pdfnativeModule.buildPDFStream, - pdfnativeModule.concatChunks, - pdfnativeModule.registerFonts, - pdfnativeModule.loadFontData, - pdfnativeModule.initNodeCompression, - pdfnativeModule.signPdfBytes, - pdfnativeModule.extractText, - pdfnativeModule.mergePdfs, - pdfnativeModule.splitPdf, - pdfnativeModule.extractPages, - pdfnativeModule.openPdf, - pdfnativeModule.readFormFields, - pdfnativeModule.fillForm, - pdfnativeModule.flattenForm - ); - - demoStatus.textContent = 'PDF generated!'; + globalThis.__pdfnativeDemo = { + mod: Object.assign({}, pdfnativeModule, { downloadBlob: captureSink }) + }; + + var source = rewriteImports(demoCode.value); + moduleUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); + await import(moduleUrl); + + demoStatus.textContent = lastPdf ? 'PDF generated — preview updated.' : 'Done (no PDF produced).'; setTimeout(function () { demoStatus.textContent = ''; }, 3000); } catch (err) { - demoError.textContent = err.message || String(err); + var line = lineFromStack(err); + demoError.textContent = (line ? 'Line ' + line + ': ' : '') + (err.message || String(err)); demoError.style.display = 'block'; demoStatus.textContent = ''; } finally { + if (moduleUrl) URL.revokeObjectURL(moduleUrl); demoBtn.disabled = false; } - }); + } + + demoBtn.addEventListener('click', runDemo); + + // First render without a click, once the demo scrolls into view — the + // visitor sees a real PDF instead of an empty pane. One shot only. + if ('IntersectionObserver' in window && canPreview) { + var demoSection = document.getElementById('demo'); + if (demoSection) { + var ran = false; + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting && !ran) { + ran = true; + io.disconnect(); + runDemo(); + } + }); + }, { rootMargin: '200px' }); + io.observe(demoSection); + } + } } })(); diff --git a/docs/guides/accessibility.html b/docs/guides/accessibility.html index 3059ae7..7527db6 100644 --- a/docs/guides/accessibility.html +++ b/docs/guides/accessibility.html @@ -64,6 +64,9 @@

    Accessibility

    +
    +

    Tagged PDF, PDF/UA and screen-reader-friendly output. Pass layout: { tagged: true } (or a PDF/A level) and pdfnative emits the structure tree, /ActualText and metadata that assistive technology needs — then check the result with the read-only validatePdfUA().

    +

    pdfnative produces tagged, accessible PDFs out of the box. This guide covers PDF/UA, PDF/A, the structure tree, and best practices for screen-reader-friendly output.

    Why tagged PDF matters#

    A tagged PDF carries a parallel structure tree alongside the visual content. Assistive technologies (screen readers, refreshable braille displays, reflow tools) walk the structure tree to present the document in reading order — without it, they fall back to heuristic guessing of what's a heading, a paragraph, or a table cell.

    diff --git a/docs/guides/accessibility.md b/docs/guides/accessibility.md index 2589246..c3b43a0 100644 --- a/docs/guides/accessibility.md +++ b/docs/guides/accessibility.md @@ -1,5 +1,7 @@ # Accessibility +> **Tagged PDF, PDF/UA and screen-reader-friendly output.** Pass `layout: { tagged: true }` (or a PDF/A level) and pdfnative emits the structure tree, `/ActualText` and metadata that assistive technology needs — then check the result with the read-only `validatePdfUA()`. + pdfnative produces **tagged, accessible PDFs** out of the box. This guide covers PDF/UA, PDF/A, the structure tree, and best practices for screen-reader-friendly output. ## Why tagged PDF matters diff --git a/docs/guides/architecture.html b/docs/guides/architecture.html index a27ecf1..9feecaf 100644 --- a/docs/guides/architecture.html +++ b/docs/guides/architecture.html @@ -64,6 +64,9 @@

    Architecture Guide

    +
    +

    Two builders, one strict dependency flow. buildPDFBytes is the table-centric builder, buildDocumentPDFBytes the free-form document builder; beneath them the modules flow types → core ← fonts ← shaping ← worker, with standalone crypto and parser modules — no circular imports anywhere.

    +

    Two Builders#

    pdfnative provides two PDF builders for different use cases:

    buildPDF() / buildPDFBytes() — Table-Centric#

    diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index e71872a..26a4a4c 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -1,4 +1,6 @@ # Architecture Guide + +> **Two builders, one strict dependency flow.** `buildPDFBytes` is the table-centric builder, `buildDocumentPDFBytes` the free-form document builder; beneath them the modules flow types → core ← fonts ← shaping ← worker, with standalone crypto and parser modules — no circular imports anywhere. ## Two Builders diff --git a/docs/guides/pdfa.html b/docs/guides/pdfa.html index a12642c..9a7b375 100644 --- a/docs/guides/pdfa.html +++ b/docs/guides/pdfa.html @@ -64,6 +64,9 @@

    PDF/A conformance in pdfnative

    +
    +

    PDF/A-1b, 2b, 2u and 3b via layout: { tagged: … }, validated against the veraPDF reference validator in CI. Since v1.7.0, a configuration that would break the declared level surfaces a diagnostic — console.warn by default, a custom onDiagnostic sink, or a thrown error under strict: true.

    +

    PDF/A is the ISO archival profile for PDF (ISO 19005). pdfnative supports PDF/A-1b, 2b, 2u, and 3b via the tagged build option. This guide explains what works today, what's still in flight, and how to validate diff --git a/docs/guides/pdfa.md b/docs/guides/pdfa.md index 342997e..af06ee0 100644 --- a/docs/guides/pdfa.md +++ b/docs/guides/pdfa.md @@ -1,5 +1,7 @@ # PDF/A conformance in pdfnative +> **PDF/A-1b, 2b, 2u and 3b via `layout: { tagged: … }`, validated against the veraPDF reference validator in CI.** Since v1.7.0, a configuration that would break the declared level surfaces a diagnostic — `console.warn` by default, a custom `onDiagnostic` sink, or a thrown error under `strict: true`. + PDF/A is the ISO archival profile for PDF (ISO 19005). pdfnative supports PDF/A-1b, 2b, 2u, and 3b via the `tagged` build option. This guide explains what works today, what's still in flight, and how to validate diff --git a/docs/guides/quickstart.html b/docs/guides/quickstart.html index ef8dba1..cbb4ee5 100644 --- a/docs/guides/quickstart.html +++ b/docs/guides/quickstart.html @@ -64,6 +64,9 @@

    Quick Start

    +
    +

    One install, one function. npm install pdfnative, then buildDocumentPDFBytes({ title, blocks }) returns a Uint8Array — synchronously, in Node, browsers, Deno and Bun. No config, no build step, no service.

    +

    Generate your first PDF in under a minute.

    Install#

    npm install pdfnative
    diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md
    index be0b5df..a4216a8 100644
    --- a/docs/guides/quickstart.md
    +++ b/docs/guides/quickstart.md
    @@ -1,5 +1,7 @@
     # Quick Start
     
    +> **One install, one function.** `npm install pdfnative`, then `buildDocumentPDFBytes({ title, blocks })` returns a `Uint8Array` — synchronously, in Node, browsers, Deno and Bun. No config, no build step, no service.
    +
     Generate your first PDF in under a minute.
     
     ## Install
    diff --git a/docs/guides/signatures.html b/docs/guides/signatures.html
    index be9be73..a1f7100 100644
    --- a/docs/guides/signatures.html
    +++ b/docs/guides/signatures.html
    @@ -64,9 +64,12 @@
       

    Digital signatures in pdfnative

    +
    +

    CMS/PKCS#7 detached signatures in pure TypeScript — RSA PKCS#1 v1.5 (SHA-256/384/512) and ECDSA P-256, the PAdES baseline profile and multiple signatures since v1.7.0, with a one-call addSignaturePlaceholder() workflow. For timestamps and B-T → B-LTA, see the LTV guide.

    +

    pdfnative ships a zero-dependency CMS/PKCS#7 detached signature implementation (ISO 32000-1 §12.8) with full crypto in pure TypeScript — -RSA PKCS#1 v1.5 and ECDSA P-256, both with SHA-256, X.509 DER parsing, +RSA PKCS#1 v1.5 (SHA-256, plus SHA-384/512 since v1.7.0) and ECDSA P-256 (SHA-256), X.509 DER parsing, and ASN.1 DER encoding. No OpenSSL, no node-forge, no external crypto.

    TL;DR — sign any PDF in 3 lines#

    import {
    diff --git a/docs/guides/signatures.md b/docs/guides/signatures.md
    index 4527d83..351570a 100644
    --- a/docs/guides/signatures.md
    +++ b/docs/guides/signatures.md
    @@ -1,8 +1,10 @@
     # Digital signatures in pdfnative
    +
    +> **CMS/PKCS#7 detached signatures in pure TypeScript** — RSA PKCS#1 v1.5 (SHA-256/384/512) and ECDSA P-256, the PAdES baseline profile and multiple signatures since v1.7.0, with a one-call `addSignaturePlaceholder()` workflow. For timestamps and B-T → B-LTA, see the [LTV guide](ltv.html).
     
     pdfnative ships a zero-dependency CMS/PKCS#7 detached signature
     implementation (ISO 32000-1 §12.8) with full crypto in pure TypeScript —
    -RSA PKCS#1 v1.5 and ECDSA P-256, both with SHA-256, X.509 DER parsing,
    +RSA PKCS#1 v1.5 (SHA-256, plus SHA-384/512 since v1.7.0) and ECDSA P-256 (SHA-256), X.509 DER parsing,
     and ASN.1 DER encoding. No OpenSSL, no node-forge, no external crypto.
     
     ## TL;DR — sign any PDF in 3 lines
    diff --git a/docs/guides/troubleshooting.html b/docs/guides/troubleshooting.html
    index e128094..5f57511 100644
    --- a/docs/guides/troubleshooting.html
    +++ b/docs/guides/troubleshooting.html
    @@ -64,6 +64,9 @@
       

    Troubleshooting

    +
    +

    Symptom-first fixes for the classic failures — tofu boxes (a font registered but never loaded), RTL text appearing backwards, oversized files, PDF/A validation errors, parser rejections — each with the check that identifies it and the change that fixes it.

    +

    Font Not Rendering (Boxes or Blank)#

    Symptom: Non-Latin text shows as empty rectangles or missing glyphs.

    Cause: The font for that script is not registered or not loaded.

    diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index c4df741..51a8867 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -1,4 +1,6 @@ # Troubleshooting + +> **Symptom-first fixes for the classic failures** — tofu boxes (a font registered but never loaded), RTL text appearing backwards, oversized files, PDF/A validation errors, parser rejections — each with the check that identifies it and the change that fixes it. ## Font Not Rendering (Boxes or Blank) diff --git a/docs/index.html b/docs/index.html index a39f76d..0466c44 100644 --- a/docs/index.html +++ b/docs/index.html @@ -426,7 +426,7 @@

    Simple, Powerful API

    Try It Live

    -

    Pick an example, edit the code, then click "Generate PDF" — it runs entirely in your browser via the npm CDN.

    +

    Pick an example, edit the code, and watch the PDF render beside it — everything runs in your browser via the npm CDN. Nothing is uploaded anywhere.

    @@ -434,13 +434,23 @@

    Try It Live

    View source ↗

    - -
    - - - +
    +
    + +
    + + + + + +
    + +
    +
    + + +
    -

    Need more? Browse all 48 generators (242 sample PDFs) @@ -816,6 +826,8 @@

    Claude Desktop — 3-line setup

    } }

    Supports Cursor, Continue, Zed, and any stdio MCP client. See the MCP Integration Guide → · Try the MCP playground →

    +

    Coding with an AI agent?

    +

    Paste the agent brief into your assistant's context: the core API, the verified pitfalls that produce wrong code, and the self-verification loop — in about a page.

    diff --git a/docs/learn/01-install.html b/docs/learn/01-install.html index 7389cc4..7d4ecbb 100644 --- a/docs/learn/01-install.html +++ b/docs/learn/01-install.html @@ -25,21 +25,7 @@ - + + + + + + + + + +
    +

    Home  ›  Guides  ›  Choosing your surface

    +
    + +

    Choosing your surface

    +
    +

    Write application code → the library. Drive a shell, Makefile or CI job → +pdfnative-cli. Give a conversational AI assistant tool access → +pdfnative-mcp. Author documents inside a React 19 app → +pdfnative-react. All four surfaces sit on the same zero-dependency +engine and produce the same ISO 32000-1 / PDF/A-conformant bytes, so the +choice is about who is calling, not about what comes out — and you can +switch later without re-authoring your documents.

    +
    +

    The decision, in prose#

    +

    You are writing application code — a Node.js, Deno, Bun or browser +service, a worker, a script with logic around the PDF. Use the library +(npm install pdfnative). It is the full surface: synchronous builders, the +parser, signatures with long-term validation, streaming, Web Worker support. +Everything the other three surfaces do, they do by calling this package.

    +

    You are driving a shell, a CI pipeline, a container, or a build tool in +another language. Use the CLI (pdfnative-cli, binary pdfnative) — +17 commands over stdin/stdout pipelines, with an agent-native automation +contract: a --json envelope, stable E_* error codes, --dry-run, and +compact --summary / --fields output projection. No JavaScript required.

    +

    You are (or you are building) a conversational assistant with tool +access — Claude Desktop, Cursor, Continue, Zed, or any Model Context +Protocol client. Use the MCP server (pdfnative-mcp, npx -y pdfnative-mcp) — 28 tools with strict JSON Schemas, read-back tools for +self-verification, and no outbound network access except the +operator-configured TSA/OCSP/CRL endpoints.

    +

    Your host application is React 19. Use the React renderer +(pdfnative-react) — declarative JSX compiled on-device to pdfnative blocks +by a custom reconciler (no DOM, no headless browser), with live-preview hooks +and the token-frugal DocSpec for agent authoring. React 19 is a peer +dependency of this package only; the engine stays zero-dependency.

    +

    Capability × surface#

    +

    The same facts in machine-readable form live in +docs/data/surfaces.json; tool, command and export +names are verified against +docs/assets/ecosystem.json and the engine's +export surface by the documentation CI. Version annotations name the release +of that surface's own package which introduced the capability; an em-dash +means the surface does not offer it.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CapabilityLibrary (pdfnative)CLI (pdfnative-cli)MCP (pdfnative-mcp)React (pdfnative-react)
    Generate documentsbuildDocumentPDFBytes / buildPDFBytesrendergenerate_basic_pdf (+ the dedicated document tools)renderToBytes and friends, <Document> tree
    Smart tablestable blockrender (table block, or --variant table)add_table<Table>
    Native vector chartschart block (v1.6.0)render (chart block) (v1.3.0)add_chart (v1.5.0)<Chart>
    Digital signatures (PAdES CMS)addSignaturePlaceholder + signPdfBytessignsign_pdf (+ prepare_signature_placeholder)
    LTV ladder (B-T → B-LTA)signPdfBytesWithTimestamp, addValidationInfo, addDocumentTimestamp (v1.7.0)— (verification only: verify --revocation)add_ltv, timestamp_pdf (v1.6.0)
    Encrypt / decryptbuild-time encryption layout option; existing PDFs via the page-tree encrypt option and openPdf with a passwordencrypt / decrypt (v1.3.0)encrypt_pdf / decrypt_pdf (v1.5.0)build-time only, via the layout render option
    Fill / flatten formsreadFormFields, fillForm, flattenForm (v1.6.0)fill (v1.3.0)read_form_fields, fill_form (v1.5.0)
    Extract textextractText (v1.6.0)extract-text (v1.3.0)extract_text
    Merge / split / extract pagesmergePdfs, splitPdf, extractPages (v1.4.0)merge / split / extract (v1.2.0)merge_pdfs / split_pdf / extract_pages (v1.3.0)
    Markup annotationsbuildAnnotation + PdfModifier.addAnnotation (v1.5.0)annotate (v1.2.0)annotate_pdf (v1.4.0)
    Inspect layout (pagination dry run)inspectDocumentLayout (v1.5.0)render --inspect-layout (v1.2.0)inspect_layout (v1.6.0)inspectDocument
    Validate PDF/UAvalidatePdfUAinspect --pdfua (v1.1.0)validate_pdf (v1.1.0)— (lintDocument checks the authoring model before rendering, not the emitted PDF)
    +

    Honest notes#

    +
      +
    • LTV differs by surface on purpose. The engine opens no socket: in the +library, the RFC 3161 / OCSP / CRL transport is injected by your code +(setTimestampProvider / setRevocationProvider). On MCP, the transport +is operator-configured through environment variables +(PDFNATIVE_MCP_TSA_URL, PDFNATIVE_MCP_REVOCATION, an allow-list) — never +from tool arguments. The CLI currently verifies LTV material +(verify --revocation, RFC 3161 timestamp validation) but does not create +it — sign-side LTV is documented as out of scope there.
    • +
    • The engine ships no cryptographic signature verifier. listSignatures +is an inventory; full verification (digest, CMS, chain, trust, timestamps, +revocation) lives in pdfnative-cli verify and the MCP verify_pdf tool.
    • +
    • React is an authoring surface. It generates documents (including charts, +barcodes, SVG, form widgets and build-time encryption via the layout +render option) but does not operate on existing PDFs — no fill, extract, +merge or signing. When a React app needs those, call the library directly: +it is already installed as the renderer's peer dependency.
    • +
    • The MCP tool names, CLI commands and library exports in the table are the +complete story for these capabilities, not a sample — where a cell is an +em-dash, the surface genuinely lacks the capability today rather than +hiding it under another name.
    • +
    +

    You can switch later#

    +

    All four surfaces call the same engine, so the artefacts are interchangeable: +a PDF rendered by the React reconciler can be signed by the CLI, inspected by +an MCP tool, and have its text extracted by the library. Document inputs +travel too — the CLI's render consumes the same DocumentParams JSON the +library takes, the MCP generate_basic_pdf blocks mirror the engine's block +kinds, and pdfnative-react compiles JSX (or a DocSpec) into that same +model. Starting on the "wrong" surface costs a call-site migration, not a +document rewrite.

    +

    Further reading#

    + + +

    + + + + + + + + + + + + diff --git a/docs/guides/choose.md b/docs/guides/choose.md new file mode 100644 index 0000000..b8d255c --- /dev/null +++ b/docs/guides/choose.md @@ -0,0 +1,107 @@ +# Choosing your surface + +> **Write application code → the library. Drive a shell, Makefile or CI job → +> `pdfnative-cli`. Give a conversational AI assistant tool access → +> `pdfnative-mcp`. Author documents inside a React 19 app → +> `pdfnative-react`.** All four surfaces sit on the same zero-dependency +> engine and produce the same ISO 32000-1 / PDF/A-conformant bytes, so the +> choice is about *who is calling*, not about what comes out — and you can +> switch later without re-authoring your documents. + +## The decision, in prose + +**You are writing application code** — a Node.js, Deno, Bun or browser +service, a worker, a script with logic around the PDF. Use the **library** +(`npm install pdfnative`). It is the full surface: synchronous builders, the +parser, signatures with long-term validation, streaming, Web Worker support. +Everything the other three surfaces do, they do by calling this package. + +**You are driving a shell, a CI pipeline, a container, or a build tool in +another language.** Use the **CLI** (`pdfnative-cli`, binary `pdfnative`) — +17 commands over stdin/stdout pipelines, with an agent-native automation +contract: a `--json` envelope, stable `E_*` error codes, `--dry-run`, and +compact `--summary` / `--fields` output projection. No JavaScript required. + +**You are (or you are building) a conversational assistant with tool +access** — Claude Desktop, Cursor, Continue, Zed, or any Model Context +Protocol client. Use the **MCP server** (`pdfnative-mcp`, `npx -y +pdfnative-mcp`) — 28 tools with strict JSON Schemas, read-back tools for +self-verification, and no outbound network access except the +operator-configured TSA/OCSP/CRL endpoints. + +**Your host application is React 19.** Use the **React renderer** +(`pdfnative-react`) — declarative JSX compiled on-device to pdfnative blocks +by a custom reconciler (no DOM, no headless browser), with live-preview hooks +and the token-frugal `DocSpec` for agent authoring. React 19 is a peer +dependency of this package only; the engine stays zero-dependency. + +## Capability × surface + +The same facts in machine-readable form live in +[`docs/data/surfaces.json`](../data/surfaces.json); tool, command and export +names are verified against +[`docs/assets/ecosystem.json`](../assets/ecosystem.json) and the engine's +export surface by the documentation CI. Version annotations name the release +*of that surface's own package* which introduced the capability; an em-dash +means the surface does not offer it. + +| Capability | Library (`pdfnative`) | CLI (`pdfnative-cli`) | MCP (`pdfnative-mcp`) | React (`pdfnative-react`) | +|---|---|---|---|---| +| Generate documents | `buildDocumentPDFBytes` / `buildPDFBytes` | `render` | `generate_basic_pdf` (+ the dedicated document tools) | `renderToBytes` and friends, `` tree | +| Smart tables | `table` block | `render` (`table` block, or `--variant table`) | `add_table` | `` | +| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` | +| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | +| LTV ladder (B-T → B-LTA) | `signPdfBytesWithTimestamp`, `addValidationInfo`, `addDocumentTimestamp` _(v1.7.0)_ | — (verification only: `verify --revocation`) | `add_ltv`, `timestamp_pdf` _(v1.6.0)_ | — | +| Encrypt / decrypt | build-time `encryption` layout option; existing PDFs via the page-tree `encrypt` option and `openPdf` with a password | `encrypt` / `decrypt` _(v1.3.0)_ | `encrypt_pdf` / `decrypt_pdf` _(v1.5.0)_ | build-time only, via the `layout` render option | +| Fill / flatten forms | `readFormFields`, `fillForm`, `flattenForm` _(v1.6.0)_ | `fill` _(v1.3.0)_ | `read_form_fields`, `fill_form` _(v1.5.0)_ | — | +| Extract text | `extractText` _(v1.6.0)_ | `extract-text` _(v1.3.0)_ | `extract_text` | — | +| Merge / split / extract pages | `mergePdfs`, `splitPdf`, `extractPages` _(v1.4.0)_ | `merge` / `split` / `extract` _(v1.2.0)_ | `merge_pdfs` / `split_pdf` / `extract_pages` _(v1.3.0)_ | — | +| Markup annotations | `buildAnnotation` + `PdfModifier.addAnnotation` _(v1.5.0)_ | `annotate` _(v1.2.0)_ | `annotate_pdf` _(v1.4.0)_ | — | +| Inspect layout (pagination dry run) | `inspectDocumentLayout` _(v1.5.0)_ | `render --inspect-layout` _(v1.2.0)_ | `inspect_layout` _(v1.6.0)_ | `inspectDocument` | +| Validate PDF/UA | `validatePdfUA` | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | + +## Honest notes + +- **LTV differs by surface on purpose.** The engine opens no socket: in the + **library**, the RFC 3161 / OCSP / CRL transport is *injected by your code* + (`setTimestampProvider` / `setRevocationProvider`). On **MCP**, the transport + is *operator-configured* through environment variables + (`PDFNATIVE_MCP_TSA_URL`, `PDFNATIVE_MCP_REVOCATION`, an allow-list) — never + from tool arguments. The **CLI** currently *verifies* LTV material + (`verify --revocation`, RFC 3161 timestamp validation) but does not create + it — sign-side LTV is documented as out of scope there. +- **The engine ships no cryptographic signature verifier.** `listSignatures` + is an inventory; full verification (digest, CMS, chain, trust, timestamps, + revocation) lives in `pdfnative-cli verify` and the MCP `verify_pdf` tool. +- **React is an authoring surface.** It generates documents (including charts, + barcodes, SVG, form *widgets* and build-time encryption via the `layout` + render option) but does not operate on existing PDFs — no fill, extract, + merge or signing. When a React app needs those, call the library directly: + it is already installed as the renderer's peer dependency. +- **The MCP tool names, CLI commands and library exports in the table are the + complete story for these capabilities**, not a sample — where a cell is an + em-dash, the surface genuinely lacks the capability today rather than + hiding it under another name. + +## You can switch later + +All four surfaces call the same engine, so the artefacts are interchangeable: +a PDF rendered by the React reconciler can be signed by the CLI, inspected by +an MCP tool, and have its text extracted by the library. Document *inputs* +travel too — the CLI's `render` consumes the same `DocumentParams` JSON the +library takes, the MCP `generate_basic_pdf` blocks mirror the engine's block +kinds, and `pdfnative-react` compiles JSX (or a `DocSpec`) into that same +model. Starting on the "wrong" surface costs a call-site migration, not a +document rewrite. + +## Further reading + +- [Onboarding](onboarding.html) — the 90-second install-and-first-call for + each surface. +- [Self-verifying generation](self-verify.html) — the generate → inspect → + assert → correct loop on every surface. +- [Architecture](architecture.html) — how the four packages relate. +- [CLI guide](cli.html) · [MCP guide](mcp.html) · [React guide](react.html) — + the complete per-surface references. +- [Agent brief](../agent-brief.md) — the same decision tree in + paste-into-context form for AI agents. diff --git a/docs/guides/index.html b/docs/guides/index.html index 2428e19..f9dcbf1 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -34,7 +34,7 @@ "inLanguage": "en", "@id": "https://pdfnative.dev/guides/#collection", "name": "pdfnative documentation guides", - "description": "All 28 reference guides for the pdfnative ecosystem.", + "description": "All 30 reference guides for the pdfnative ecosystem.", "url": "https://pdfnative.dev/guides/", "isPartOf": { "@type": "WebSite", @@ -43,7 +43,7 @@ }, "mainEntity": { "@type": "ItemList", - "numberOfItems": 28, + "numberOfItems": 30, "itemListOrder": "https://schema.org/ItemListUnordered", "itemListElement": [ { @@ -85,132 +85,144 @@ { "@type": "ListItem", "position": 7, + "name": "Choosing your surface", + "url": "https://pdfnative.dev/guides/choose.html" + }, + { + "@type": "ListItem", + "position": 8, "name": "pdfnative-cli", "url": "https://pdfnative.dev/guides/cli.html" }, { "@type": "ListItem", - "position": 8, + "position": 9, "name": "Colour-emoji font CLI", "url": "https://pdfnative.dev/guides/colour-emoji-cli.html" }, { "@type": "ListItem", - "position": 9, + "position": 10, "name": "Colour emoji (COLRv1)", "url": "https://pdfnative.dev/guides/colour-emoji.html" }, { "@type": "ListItem", - "position": 10, + "position": 11, "name": "Layout debugging", "url": "https://pdfnative.dev/guides/debugging.html" }, { "@type": "ListItem", - "position": 11, + "position": 12, "name": "FAQ", "url": "https://pdfnative.dev/guides/faq.html" }, { "@type": "ListItem", - "position": 12, + "position": 13, "name": "Font validation", "url": "https://pdfnative.dev/guides/font-validation.html" }, { "@type": "ListItem", - "position": 13, + "position": 14, "name": "Fill and flatten AcroForms", "url": "https://pdfnative.dev/guides/form-filling.html" }, { "@type": "ListItem", - "position": 14, + "position": 15, "name": "pdfnative-mcp", "url": "https://pdfnative.dev/guides/mcp.html" }, { "@type": "ListItem", - "position": 15, + "position": 16, "name": "Onboarding — the pdfnative ecosystem", "url": "https://pdfnative.dev/guides/onboarding.html" }, { "@type": "ListItem", - "position": 16, + "position": 17, "name": "Outlines and page labels", "url": "https://pdfnative.dev/guides/outlines.html" }, { "@type": "ListItem", - "position": 17, + "position": 18, "name": "PDF manipulation — merge, split, decrypt", "url": "https://pdfnative.dev/guides/pdf-manipulation.html" }, { "@type": "ListItem", - "position": 18, + "position": 19, "name": "PDF/A conformance", "url": "https://pdfnative.dev/guides/pdfa.html" }, { "@type": "ListItem", - "position": 19, + "position": 20, "name": "Print production", "url": "https://pdfnative.dev/guides/print.html" }, { "@type": "ListItem", - "position": 20, + "position": 21, "name": "Quick start — your first PDF", "url": "https://pdfnative.dev/guides/quickstart.html" }, { "@type": "ListItem", - "position": 21, + "position": 22, "name": "pdfnative-react", "url": "https://pdfnative.dev/guides/react.html" }, { "@type": "ListItem", - "position": 22, + "position": 23, + "name": "Self-verifying generation", + "url": "https://pdfnative.dev/guides/self-verify.html" + }, + { + "@type": "ListItem", + "position": 24, "name": "Digital signatures", "url": "https://pdfnative.dev/guides/signatures.html" }, { "@type": "ListItem", - "position": 23, + "position": 25, "name": "Long-term validation (LTV)", "url": "https://pdfnative.dev/guides/ltv.html" }, { "@type": "ListItem", - "position": 24, + "position": 26, "name": "Streaming output", "url": "https://pdfnative.dev/guides/streaming.html" }, { "@type": "ListItem", - "position": 25, + "position": 27, "name": "Smart tables", "url": "https://pdfnative.dev/guides/tables.html" }, { "@type": "ListItem", - "position": 26, + "position": 28, "name": "Text extraction", "url": "https://pdfnative.dev/guides/text-extraction.html" }, { "@type": "ListItem", - "position": 27, + "position": 29, "name": "Troubleshooting", "url": "https://pdfnative.dev/guides/troubleshooting.html" }, { "@type": "ListItem", - "position": 28, + "position": 30, "name": "Viewer preferences", "url": "https://pdfnative.dev/guides/viewer-preferences.html" } @@ -286,6 +298,10 @@

    Documentation Guides

    Onboarding → 90-second start for the full ecosystem — the library, pdfnative-cli, pdfnative-mcp, and pdfnative-react. Install + first call for each. +
  • + Choosing your surface → + Library, CLI, MCP or React? The decision tree in prose plus a verified capability × surface matrix — every export, command and tool name checked against the sources, with an honest em-dash where a surface lacks a capability. Machine-readable twin in docs/data/surfaces.json. +
  • Quick Start → Install pdfnative and generate your first PDF in Node.js or the browser in under a minute. @@ -378,6 +394,10 @@

    Documentation Guides

    Layout debugging → v1.5.0 opt-in visual overlay (layout: { debug: true }) drawing margin / content / cell boxes, plus inspectDocumentLayout() for programmatic per-page block geometry. Byte-identical when off.
  • +
  • + Self-verifying generation → + The generate → inspect → assert → correct loop as a first-class pattern: plan with inspectDocumentLayout(), then verify the emitted bytes with extractText(), validatePdfUA() and listSignatures() / openPdf() — and the same loop via CLI inspect --check exit codes and the MCP inspect_pdf assertions, ready for CI and agent loops. +
  • Annotations → v1.5.0 typed markup annotations — text, highlight, underline, strikeout, squiggly, square, circle, line, freetext — with buildAnnotation(), getAnnotations() to read existing PDFs, and addAnnotation() incremental write. diff --git a/docs/guides/react.html b/docs/guides/react.html index 51aa2d7..67b826a 100644 --- a/docs/guides/react.html +++ b/docs/guides/react.html @@ -406,7 +406,7 @@

    Migrating from @react-pdf/render

  • - +
    same names, same shape
    usePDF() usePDF() usePdf()
    diff --git a/docs/guides/react.md b/docs/guides/react.md index 95dfa1a..339847c 100644 --- a/docs/guides/react.md +++ b/docs/guides/react.md @@ -309,7 +309,7 @@ const bytes = renderToBytes( | `` + flexbox styles | *(none — declarative block flow; use blocks + ``)* | | `StyleSheet` | per-component props (`align`, `color`, `fontSize`, …) | | `` / `` / `` | same names, same shape | -| `usePDF()` | `usePdf()` | +| `usePDF()` | `usePdf()` | The biggest mental shift: there is **no flexbox layout engine**. Documents are a top-to-bottom block flow. Use ``, ``, tables, and per-component alignment props instead of `` containers. diff --git a/docs/guides/self-verify.html b/docs/guides/self-verify.html new file mode 100644 index 0000000..09b78f8 --- /dev/null +++ b/docs/guides/self-verify.html @@ -0,0 +1,369 @@ + + + + + + Self-verifying generation — pdfnative + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Home  ›  Guides  ›  Self-verifying generation

    +
    + +

    Self-verifying generation

    +
    +

    pdfnative can read what it writes, so a generated PDF never has to ship +unchecked. Plan pagination with inspectDocumentLayout() before building, +then run extractText(), validatePdfUA() and listSignatures() / +openPdf() against the emitted bytes, assert what matters, and correct the +input until every assertion holds. The same generate → inspect → assert → +correct loop runs on all four surfaces: in the library, on the CLI +(inspect --check, stable exit codes and E_* error classes), and on MCP +(inspect_pdf check assertions) — so a test suite, a CI job or an AI agent +can close the loop without a human ever opening a viewer.

    +
    +

    Why a PDF is normally a blind artefact#

    +

    Most PDF generators are write-only: the library that produced the file cannot +parse it back. The only verification available is a human opening the result in +a viewer — which no pipeline, test suite or autonomous agent can do. So defects +ship silently: a table that slipped onto a second page, a template placeholder +that was never substituted, a PDF/A claim the file cannot honour, a signature +placeholder that was never actually signed.

    +

    pdfnative bundles a parser next to the writer, in the same zero-dependency +package. The read side (openPdf(), extractText(), validatePdfUA(), +listSignatures()) consumes exactly what the write side emits — and any other +PDF — which turns generation into a closed loop:

    +
      +
    1. Plan — measure pagination with inspectDocumentLayout() before a single +byte is produced.
    2. +
    3. Generate — build the document.
    4. +
    5. Inspect — read the emitted bytes back with the parser.
    6. +
    7. Assert & correct — check the facts you care about; on failure, fix the +input and regenerate. Never patch the output bytes.
    8. +
    +

    The rest of this guide walks the four verifiers with realistic +failure → fix examples, then shows the identical loop on the CLI and on MCP.

    +

    Verifier 1 — inspectDocumentLayout(): geometry, before generating#

    +

    (v1.5.0) A pagination dry run: the same planning logic as the builder, but it +returns data instead of PDF bytes — no rendering, no font embedding.

    +
    import { buildDocumentPDFBytes, inspectDocumentLayout } from 'pdfnative';
    +
    +const params = {
    +  title: 'Invoice #2026-041',
    +  blocks: [
    +    { type: 'heading', level: 1, text: 'Invoice #2026-041' },
    +    { type: 'paragraph', text: intro },
    +    { type: 'table', headers: ['Item', 'Qty', 'Total'], rows },
    +  ],
    +};
    +
    +// Plan — no PDF is produced.
    +const layout = inspectDocumentLayout(params);
    +
    +// Assert: a one-page invoice.
    +if (layout.totalPages !== 1) {
    +  throw new Error(`expected 1 page, planned ${layout.totalPages}`);
    +}
    +
    +// Assert: the table starts on page 1, not after a stray break.
    +const table = layout.pages[0].blocks.find((b) => b.type === 'table');
    +if (!table) throw new Error('table was pushed off page 1');
    +
    +// Only now generate.
    +const bytes = buildDocumentPDFBytes(params);
    +
    +

    Failure → fix. totalPages comes back as 2 because this month's rows +grew by three entries. Fixes live in the input: shorten intro, reduce the +margins in the layout options, or accept the second page and change the +assertion — the table planner repeats headers on continuation pages by default, +so a deliberate two-pager stays readable. Re-run the inspection until the +assertion holds, then build.

    +
    +

    Honest caveat. inspectDocumentLayout() shares the builder's measurement +code (estimateBlockHeight / planTable and its constants) — that is +precisely why its geometry matches where the renderer places each block, +including table slicing across page breaks. But it is a prediction from the +shared engine, not an independent re-parse of the emitted bytes. For +verification of the actual output, pair it with extractText() below. See +Layout debugging & inspection for the full data shape and +the visual overlay variant.

    +
    +

    Verifier 2 — extractText(): is the content actually in the file?#

    +

    (v1.6.0) Reading-order Unicode text per page, decoded from the content +streams of the emitted bytes — pdfnative-generated documents round-trip +losslessly, including CJK, Arabic and emoji.

    +
    import { buildDocumentPDFBytes, extractText } from 'pdfnative';
    +
    +const bytes = buildDocumentPDFBytes(params);
    +
    +const pages = extractText(bytes);               // ExtractedPageText[]
    +// Each entry: { pageIndex: number, text: string, runs?: ExtractedTextRun[] }
    +const flat = pages.map((p) => p.text).join('\n');
    +
    +for (const required of ['Invoice #2026-041', 'Total', '1 240,00 €']) {
    +  if (!flat.includes(required)) {
    +    throw new Error(`missing from the emitted PDF: "${required}"`);
    +  }
    +}
    +if (flat.includes('{{')) {
    +  throw new Error('an unresolved template placeholder reached the PDF');
    +}
    +
    +

    Failure → fix. The total is missing: the value never entered blocks — +typically a renamed data field, or a templating step that ran after the build +instead of before it. The {{ guard catches the classic half-rendered +template. Fix the data flow and regenerate. Positioned runs +({ includeRuns: true }) additionally give each string's x / y / +fontSize when you need to assert where something landed, not just that it +exists.

    +

    Extraction has documented limits (no OCR on image content, lossy conjuncts in +shaped Indic scripts) — see Text extraction before +asserting on those cases.

    +

    Verifier 3 — validatePdfUA(): structural PDF/UA#

    +

    Read-only ISO 14289-1 structural checks over the emitted bytes: /MarkInfo, +the structure tree, the parent tree, a document /Lang, per-page MCID +uniqueness.

    +
    import { buildDocumentPDFBytes, validatePdfUA } from 'pdfnative';
    +
    +const bytes = buildDocumentPDFBytes(params, { tagged: true });
    +
    +const report = validatePdfUA(bytes);   // { valid, errors, warnings }
    +if (!report.valid) {
    +  throw new Error(`PDF/UA structure: ${report.errors.join('; ')}`);
    +}
    +
    +

    Failure → fix. The most common failure is building without tagged — the +document then has no structure tree at all, and every structural check fires. +Set layout: { tagged: true } (or a 'pdfa*' level) and regenerate. Warnings +are non-blocking best-practice notes; decide explicitly whether your pipeline +tolerates them.

    +
    +

    Honest caveat. This is a structural validation, not an accessibility +audit. A passing report means the machine-checkable structure is present and +consistent — it cannot judge whether the reading order makes sense to a +human, whether alt text is meaningful, or whether colour contrast is +adequate. And it is not a PDF/A validator: for archival conformance, run +veraPDF as described in PDF/A conformance.

    +
    +

    Verifier 4 — listSignatures() + openPdf(): signatures, encryption, metadata#

    +

    (v1.7.0) listSignatures() inventories every signature field in the +document; openPdf() exposes page count, encryption state and the /Info +dictionary.

    +
    import { openPdf, listSignatures } from 'pdfnative';
    +
    +const sigs = listSignatures(signedBytes);
    +if (sigs.length === 0) throw new Error('no signature field found');
    +if (sigs.some((s) => s.isPlaceholder)) {
    +  throw new Error('a /Sig field is still an unsigned placeholder');
    +}
    +// Per entry: fieldName?, subFilter, byteRange, isDocTimestamp, isPlaceholder.
    +const pades = sigs.filter((s) => s.subFilter === 'ETSI.CAdES.detached');
    +
    +const reader = openPdf(signedBytes);
    +if (reader.encryption !== null) throw new Error('expected an unencrypted document');
    +if (reader.pageCount < 2) throw new Error('terms page missing');
    +const info = reader.getInfo();
    +
    +

    Failure → fix. isPlaceholder: true means addSignaturePlaceholder() ran +but signPdfBytes() never filled that field — usually a skipped signing step, +or a fieldName selector that targeted a different placeholder. Sign the +document (or the right field) and re-check. isDocTimestamp distinguishes +/DocTimeStamp entries when you assert on the PAdES ladder.

    +
    +

    Honest caveat. listSignatures() is an inventory, not a cryptographic +verdict — the pdfnative engine deliberately ships no signature verifier. +Full verification (byte-range digest, CMS signature value, certificate +chain, trust, timestamps, revocation) lives in pdfnative-cli verify and in +the MCP verify_pdf tool, below. See +Long-term validation (LTV).

    +
    +

    The same loop on the CLI#

    +

    pdfnative-cli turns the assertions into exit codes, which is what a shell or +CI step branches on:

    +
    # Plan (pre-flight): pagination report instead of a PDF   (v1.2.0)
    +pdfnative render --input invoice.json --inspect-layout > layout.json
    +
    +# Generate, then assert — exit 0 when every --check holds, 1 otherwise
    +pdfnative render  --input invoice.json --output invoice.pdf --tagged pdfa2b
    +pdfnative inspect --input invoice.pdf --check pdfa --check pdfua --json --summary
    +echo $?   # 0 = all assertions hold, 1 = at least one failed
    +
    +# Content presence, pipeline-style
    +pdfnative extract-text --input invoice.pdf --format text | grep -q 'Invoice #2026-041'
    +
    +
      +
    • inspect --check takes repeatable, ANDed assertions — pdfa, signed, +encrypted, pdfua (v1.1.0) — and sets the exit code accordingly; under +--json, a failed assertion also emits the stable error class +E_CHECK_FAILED in the machine-readable envelope on stderr +({ ok: false, command, error: { code, message } }).
    • +
    • --summary (v1.1.0) shrinks the stdout verdict to +{ pages, encrypted, signatures, pdfa }; --fields a,b.c projects arbitrary +dot-paths — both exist so agents branch on facts without paying for the full +report.
    • +
    • pdfnative verify --strict performs the cryptographic half: byte-range +digest, CMS signature value (RSA and ECDSA), certificate chain and trust +against --trust roots, RFC 3161 timestamps, OCSP/CRL revocation — exit 1 +and E_VERIFY_FAILED on any failure or on zero signatures.
    • +
    +

    The full command reference, including every --check flag and the E_* +catalogue, is in the CLI guide.

    +

    The same loop on MCP#

    +

    pdfnative-mcp gives a conversational agent the identical verifiers as +read-only tools — the natural final step after any document tool call:

    + + + + + + + + + + + + + + + + + + + + + + + +
    ToolVerifies
    inspect_pdfVersion, page count, encryption, PDF/A claim, signatures, attachments, /Info — plus check assertions with a boolean verdict.
    inspect_layout (v1.6.0)Pagination dry run for a prospective document — page count and block geometry, no PDF produced, no base64 output tokens spent.
    validate_pdfStructural PDF/UA — { valid, errors, warnings }.
    verify_pdfCryptographic signature verification; ltv: true (v1.6.0) reports the achieved PAdES level (B-B → B-LTA).
    +
    { "tool": "inspect_pdf",
    +  "input": { "pdfBase64": "<...>", "check": ["pdfa", "signed"] } }
    +// → { ..., "checks": { "pdfa": true, "signed": true }, "checksPassed": true }
    +
    +
      +
    • check accepts pdfa, signed, encrypted, placeholder, attachments, +and — (v1.6.0)dss, docTimestamp, trapped, annotations. +checksPassed is the AND of all requested assertions; since v1.6.0 checks +contains only the requested keys, so branch on checksPassed or on a key +you asked for, never on an absent one.
    • +
    • inspect_layout accepts the same title / blocks (plus every input that +moves a block) as generate_basic_pdf, so an agent can iterate on pagination +before spending output tokens on real bytes. Known engine gap: a toc block +is measured as 0 pt, so a document with a printed contents page may paginate +one page later than previewed.
    • +
    • validate_pdf on unparsable bytes is an error (PDF_PARSE_FAILED), not a +{ valid: false } verdict, since v1.6.0 — a parse failure is not a PDF/UA +result.
    • +
    +

    Tool-by-tool inputs and error codes are in the MCP guide.

    +

    Wire it into CI and agent loops#

    +

    CI. Chain the loop with set -e so any failed assertion fails the job — +no PDF-viewing human in the path:

    +
    set -e
    +pdfnative render  --input report.json --output report.pdf --tagged pdfa2b
    +pdfnative sign    --input report.pdf  --output signed.pdf --key k.pem --cert c.pem
    +pdfnative inspect --input signed.pdf  --check pdfa --check signed --json --summary
    +pdfnative verify  --input signed.pdf  --strict --trust ca-root.pem
    +
    +

    --dry-run on the write commands validates inputs without producing output, +which makes a cheap first CI stage; --summary / --fields keep logs and +agent context small.

    +

    Agent loops. Treat every generation as unfinished until a read tool +confirms it: after generate_basic_pdf (or any document tool), call +inspect_pdf with the check list that encodes the user's requirements and +branch on checksPassed; on failure, correct the tool input and regenerate +rather than apologising with an unverified file. Use inspect_layout before +generating and verbosity: "summary" on the read tools to keep token spend +proportional. The agent brief condenses this contract +into a paste-into-context form.

    +

    Further reading#

    + + +
    +
    + + + + + + + + + + + diff --git a/docs/guides/self-verify.md b/docs/guides/self-verify.md new file mode 100644 index 0000000..d5b92eb --- /dev/null +++ b/docs/guides/self-verify.md @@ -0,0 +1,297 @@ +# Self-verifying generation + +> **pdfnative can read what it writes, so a generated PDF never has to ship +> unchecked.** Plan pagination with `inspectDocumentLayout()` *before* building, +> then run `extractText()`, `validatePdfUA()` and `listSignatures()` / +> `openPdf()` against the emitted bytes, assert what matters, and correct the +> input until every assertion holds. The same generate → inspect → assert → +> correct loop runs on all four surfaces: in the library, on the CLI +> (`inspect --check`, stable exit codes and `E_*` error classes), and on MCP +> (`inspect_pdf` check assertions) — so a test suite, a CI job or an AI agent +> can close the loop without a human ever opening a viewer. + +## Why a PDF is normally a blind artefact + +Most PDF generators are write-only: the library that produced the file cannot +parse it back. The only verification available is a human opening the result in +a viewer — which no pipeline, test suite or autonomous agent can do. So defects +ship silently: a table that slipped onto a second page, a template placeholder +that was never substituted, a PDF/A claim the file cannot honour, a signature +placeholder that was never actually signed. + +pdfnative bundles a parser next to the writer, in the same zero-dependency +package. The read side (`openPdf()`, `extractText()`, `validatePdfUA()`, +`listSignatures()`) consumes exactly what the write side emits — and any other +PDF — which turns generation into a closed loop: + +1. **Plan** — measure pagination with `inspectDocumentLayout()` before a single + byte is produced. +2. **Generate** — build the document. +3. **Inspect** — read the emitted bytes back with the parser. +4. **Assert & correct** — check the facts you care about; on failure, fix the + *input* and regenerate. Never patch the output bytes. + +The rest of this guide walks the four verifiers with realistic +failure → fix examples, then shows the identical loop on the CLI and on MCP. + +## Verifier 1 — `inspectDocumentLayout()`: geometry, before generating + +_(v1.5.0)_ A pagination dry run: the same planning logic as the builder, but it +returns data instead of PDF bytes — no rendering, no font embedding. + +```ts +import { buildDocumentPDFBytes, inspectDocumentLayout } from 'pdfnative'; + +const params = { + title: 'Invoice #2026-041', + blocks: [ + { type: 'heading', level: 1, text: 'Invoice #2026-041' }, + { type: 'paragraph', text: intro }, + { type: 'table', headers: ['Item', 'Qty', 'Total'], rows }, + ], +}; + +// Plan — no PDF is produced. +const layout = inspectDocumentLayout(params); + +// Assert: a one-page invoice. +if (layout.totalPages !== 1) { + throw new Error(`expected 1 page, planned ${layout.totalPages}`); +} + +// Assert: the table starts on page 1, not after a stray break. +const table = layout.pages[0].blocks.find((b) => b.type === 'table'); +if (!table) throw new Error('table was pushed off page 1'); + +// Only now generate. +const bytes = buildDocumentPDFBytes(params); +``` + +**Failure → fix.** `totalPages` comes back as `2` because this month's `rows` +grew by three entries. Fixes live in the input: shorten `intro`, reduce the +margins in the layout options, or accept the second page and change the +assertion — the table planner repeats headers on continuation pages by default, +so a deliberate two-pager stays readable. Re-run the inspection until the +assertion holds, then build. + +> **Honest caveat.** `inspectDocumentLayout()` shares the builder's measurement +> code (`estimateBlockHeight` / `planTable` and its constants) — that is +> precisely why its geometry matches where the renderer places each block, +> including table slicing across page breaks. But it is a *prediction from the +> shared engine*, not an independent re-parse of the emitted bytes. For +> verification of the actual output, pair it with `extractText()` below. See +> [Layout debugging & inspection](debugging.html) for the full data shape and +> the visual overlay variant. + +## Verifier 2 — `extractText()`: is the content actually in the file? + +_(v1.6.0)_ Reading-order Unicode text per page, decoded from the content +streams of the emitted bytes — pdfnative-generated documents round-trip +losslessly, including CJK, Arabic and emoji. + +```ts +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; + +const bytes = buildDocumentPDFBytes(params); + +const pages = extractText(bytes); // ExtractedPageText[] +// Each entry: { pageIndex: number, text: string, runs?: ExtractedTextRun[] } +const flat = pages.map((p) => p.text).join('\n'); + +for (const required of ['Invoice #2026-041', 'Total', '1 240,00 €']) { + if (!flat.includes(required)) { + throw new Error(`missing from the emitted PDF: "${required}"`); + } +} +if (flat.includes('{{')) { + throw new Error('an unresolved template placeholder reached the PDF'); +} +``` + +**Failure → fix.** The total is missing: the value never entered `blocks` — +typically a renamed data field, or a templating step that ran after the build +instead of before it. The `{{` guard catches the classic half-rendered +template. Fix the data flow and regenerate. Positioned runs +(`{ includeRuns: true }`) additionally give each string's `x` / `y` / +`fontSize` when you need to assert *where* something landed, not just that it +exists. + +Extraction has documented limits (no OCR on image content, lossy conjuncts in +shaped Indic scripts) — see [Text extraction](text-extraction.html) before +asserting on those cases. + +## Verifier 3 — `validatePdfUA()`: structural PDF/UA + +Read-only ISO 14289-1 structural checks over the emitted bytes: `/MarkInfo`, +the structure tree, the parent tree, a document `/Lang`, per-page MCID +uniqueness. + +```ts +import { buildDocumentPDFBytes, validatePdfUA } from 'pdfnative'; + +const bytes = buildDocumentPDFBytes(params, { tagged: true }); + +const report = validatePdfUA(bytes); // { valid, errors, warnings } +if (!report.valid) { + throw new Error(`PDF/UA structure: ${report.errors.join('; ')}`); +} +``` + +**Failure → fix.** The most common failure is building without `tagged` — the +document then has no structure tree at all, and every structural check fires. +Set `layout: { tagged: true }` (or a `'pdfa*'` level) and regenerate. Warnings +are non-blocking best-practice notes; decide explicitly whether your pipeline +tolerates them. + +> **Honest caveat.** This is a *structural* validation, not an accessibility +> audit. A passing report means the machine-checkable structure is present and +> consistent — it cannot judge whether the reading order makes sense to a +> human, whether alt text is meaningful, or whether colour contrast is +> adequate. And it is not a PDF/A validator: for archival conformance, run +> veraPDF as described in [PDF/A conformance](pdfa.html). + +## Verifier 4 — `listSignatures()` + `openPdf()`: signatures, encryption, metadata + +_(v1.7.0)_ `listSignatures()` inventories every signature field in the +document; `openPdf()` exposes page count, encryption state and the `/Info` +dictionary. + +```ts +import { openPdf, listSignatures } from 'pdfnative'; + +const sigs = listSignatures(signedBytes); +if (sigs.length === 0) throw new Error('no signature field found'); +if (sigs.some((s) => s.isPlaceholder)) { + throw new Error('a /Sig field is still an unsigned placeholder'); +} +// Per entry: fieldName?, subFilter, byteRange, isDocTimestamp, isPlaceholder. +const pades = sigs.filter((s) => s.subFilter === 'ETSI.CAdES.detached'); + +const reader = openPdf(signedBytes); +if (reader.encryption !== null) throw new Error('expected an unencrypted document'); +if (reader.pageCount < 2) throw new Error('terms page missing'); +const info = reader.getInfo(); +``` + +**Failure → fix.** `isPlaceholder: true` means `addSignaturePlaceholder()` ran +but `signPdfBytes()` never filled that field — usually a skipped signing step, +or a `fieldName` selector that targeted a different placeholder. Sign the +document (or the right field) and re-check. `isDocTimestamp` distinguishes +`/DocTimeStamp` entries when you assert on the PAdES ladder. + +> **Honest caveat.** `listSignatures()` is an inventory, not a cryptographic +> verdict — the pdfnative engine deliberately ships no signature verifier. +> Full verification (byte-range digest, CMS signature value, certificate +> chain, trust, timestamps, revocation) lives in `pdfnative-cli verify` and in +> the MCP `verify_pdf` tool, below. See +> [Long-term validation (LTV)](ltv.html). + +## The same loop on the CLI + +`pdfnative-cli` turns the assertions into exit codes, which is what a shell or +CI step branches on: + +```bash +# Plan (pre-flight): pagination report instead of a PDF (v1.2.0) +pdfnative render --input invoice.json --inspect-layout > layout.json + +# Generate, then assert — exit 0 when every --check holds, 1 otherwise +pdfnative render --input invoice.json --output invoice.pdf --tagged pdfa2b +pdfnative inspect --input invoice.pdf --check pdfa --check pdfua --json --summary +echo $? # 0 = all assertions hold, 1 = at least one failed + +# Content presence, pipeline-style +pdfnative extract-text --input invoice.pdf --format text | grep -q 'Invoice #2026-041' +``` + +- `inspect --check` takes repeatable, ANDed assertions — `pdfa`, `signed`, + `encrypted`, `pdfua` _(v1.1.0)_ — and sets the exit code accordingly; under + `--json`, a failed assertion also emits the stable error class + `E_CHECK_FAILED` in the machine-readable envelope on stderr + (`{ ok: false, command, error: { code, message } }`). +- `--summary` _(v1.1.0)_ shrinks the stdout verdict to + `{ pages, encrypted, signatures, pdfa }`; `--fields a,b.c` projects arbitrary + dot-paths — both exist so agents branch on facts without paying for the full + report. +- `pdfnative verify --strict` performs the cryptographic half: byte-range + digest, CMS signature value (RSA and ECDSA), certificate chain and trust + against `--trust` roots, RFC 3161 timestamps, OCSP/CRL revocation — exit 1 + and `E_VERIFY_FAILED` on any failure *or on zero signatures*. + +The full command reference, including every `--check` flag and the `E_*` +catalogue, is in the [CLI guide](cli.html). + +## The same loop on MCP + +`pdfnative-mcp` gives a conversational agent the identical verifiers as +read-only tools — the natural final step after any document tool call: + +| Tool | Verifies | +|---|---| +| `inspect_pdf` | Version, page count, encryption, PDF/A claim, signatures, attachments, `/Info` — plus `check` assertions with a boolean verdict. | +| `inspect_layout` _(v1.6.0)_ | Pagination dry run for a prospective document — page count and block geometry, no PDF produced, no base64 output tokens spent. | +| `validate_pdf` | Structural PDF/UA — `{ valid, errors, warnings }`. | +| `verify_pdf` | Cryptographic signature verification; `ltv: true` _(v1.6.0)_ reports the achieved PAdES level (B-B → B-LTA). | + +```jsonc +{ "tool": "inspect_pdf", + "input": { "pdfBase64": "<...>", "check": ["pdfa", "signed"] } } +// → { ..., "checks": { "pdfa": true, "signed": true }, "checksPassed": true } +``` + +- `check` accepts `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`, + and — _(v1.6.0)_ — `dss`, `docTimestamp`, `trapped`, `annotations`. + `checksPassed` is the AND of all requested assertions; since v1.6.0 `checks` + contains **only** the requested keys, so branch on `checksPassed` or on a key + you asked for, never on an absent one. +- `inspect_layout` accepts the same `title` / `blocks` (plus every input that + moves a block) as `generate_basic_pdf`, so an agent can iterate on pagination + before spending output tokens on real bytes. Known engine gap: a `toc` block + is measured as 0 pt, so a document with a printed contents page may paginate + one page later than previewed. +- `validate_pdf` on unparsable bytes is an error (`PDF_PARSE_FAILED`), not a + `{ valid: false }` verdict, since v1.6.0 — a parse failure is not a PDF/UA + result. + +Tool-by-tool inputs and error codes are in the [MCP guide](mcp.html). + +## Wire it into CI and agent loops + +**CI.** Chain the loop with `set -e` so any failed assertion fails the job — +no PDF-viewing human in the path: + +```bash +set -e +pdfnative render --input report.json --output report.pdf --tagged pdfa2b +pdfnative sign --input report.pdf --output signed.pdf --key k.pem --cert c.pem +pdfnative inspect --input signed.pdf --check pdfa --check signed --json --summary +pdfnative verify --input signed.pdf --strict --trust ca-root.pem +``` + +`--dry-run` on the write commands validates inputs without producing output, +which makes a cheap first CI stage; `--summary` / `--fields` keep logs and +agent context small. + +**Agent loops.** Treat every generation as unfinished until a read tool +confirms it: after `generate_basic_pdf` (or any document tool), call +`inspect_pdf` with the `check` list that encodes the user's requirements and +branch on `checksPassed`; on failure, correct the tool input and regenerate +rather than apologising with an unverified file. Use `inspect_layout` before +generating and `verbosity: "summary"` on the read tools to keep token spend +proportional. The [agent brief](../agent-brief.md) condenses this contract +into a paste-into-context form. + +## Further reading + +- [Agent brief](../agent-brief.md) — the compact form of this loop for AI + agents, verified by the documentation CI. +- [Layout debugging & inspection](debugging.html) — the visual overlay and the + full `LayoutInspection` data shape. +- [Text extraction](text-extraction.html) — decoding rules, positioned runs, + documented limits. +- [PDF/A conformance](pdfa.html) — validating archival claims with veraPDF, + and the build-time `PDFA_*` diagnostics. +- [Long-term validation (LTV)](ltv.html) — the PAdES ladder the signature + assertions climb. +- [CLI guide](cli.html) · [MCP guide](mcp.html) — the complete surface + references. diff --git a/docs/learn/08-next-steps.html b/docs/learn/08-next-steps.html index 16898ac..d2897db 100644 --- a/docs/learn/08-next-steps.html +++ b/docs/learn/08-next-steps.html @@ -127,7 +127,7 @@

    Common next requirements

    Try things without installing anything

    Nine playgrounds run in your browser: 22 writing systems in one document, native charts, the full toolkit, a CLI command builder, an MCP tool explorer, the React renderer, and 100,000 pages.

    -

    All 28 reference guides are one click away. If something in them does not hold up, say so — a claim we cannot defend is a bug.

    +

    All 30 reference guides are one click away. If something in them does not hold up, say so — a claim we cannot defend is a bug.

    You are done. You know the model, the delivery paths and where the reference material lives. diff --git a/docs/learn/index.html b/docs/learn/index.html index 6d22c69..3cdbee9 100644 --- a/docs/learn/index.html +++ b/docs/learn/index.html @@ -151,7 +151,7 @@

    Learn pdfnative

    Eight short steps, in order, from installing the package to shipping a hundred-thousand-page document. Each one introduces exactly one idea and says what you should be able to do at the end of it.

    About thirty minutes, start to finish. You need Node.js 22 or newer and nothing else — no account, no key, no service.

    -

    Already know what you want? The 28 reference guides are the shelf; this is the path.

    +

    Already know what you want? The 30 reference guides are the shelf; this is the path.

    1. 1. Install pdfnative and make one PDF

      Ten lines, one file on disk. Nothing else.

    2. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 43854a6..51b5099 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,6 +62,9 @@ - [agent-brief.md](https://pdfnative.dev/agent-brief.md): A compact briefing for AI agents writing code with pdfnative — the core API, the verified pitfalls that produce wrong code, the surface decision tree, and the self-verification loop. Designed to be pasted into a coding agent's context. - [llms-full.txt](https://pdfnative.dev/llms-full.txt): This index plus the README and the full Markdown source of every guide, in one request. +- [llms-recipes.txt](https://pdfnative.dev/llms-recipes.txt): The executable recipe corpus — standalone TypeScript files importing from `pdfnative`, each executed in CI with its `@expect` assertions verified, so the code you copy is proven to compile and to produce exactly the artefact its header describes. +- [api.json](https://pdfnative.dev/assets/api.json): The public API surface, derived mechanically from the export statements of `src/index.ts` — name, kind, source module, signature and TSDoc summary where extractable. The substitute for the unpublished `index.d.ts`. +- [surfaces.json](https://pdfnative.dev/data/surfaces.json): The capability × surface matrix (library / CLI / MCP / React) behind the [Choosing your surface](https://pdfnative.dev/guides/choose.md) guide. - [llms-index.json](https://pdfnative.dev/llms-index.json): Per-page machine index — HTML and raw-Markdown URL, title, summary, section anchors, and exact byte / approximate token sizes for every artefact, so you can decide what to fetch before spending the tokens. - Every guide serves its raw Markdown at the same URL with `.md` instead of `.html`, declared via `` in each page head. All of these URLs are listed in the sitemap. @@ -2621,6 +2624,116 @@ labels shape correctly when the matching font is registered — no extra work. - [Accessibility](accessibility.html) — tagged PDF / PDF-UA - [CHANGELOG](https://github.com/Nizoka/pdfnative/blob/main/CHANGELOG.md) +--- + +# Choosing your surface + +> **Write application code → the library. Drive a shell, Makefile or CI job → +> `pdfnative-cli`. Give a conversational AI assistant tool access → +> `pdfnative-mcp`. Author documents inside a React 19 app → +> `pdfnative-react`.** All four surfaces sit on the same zero-dependency +> engine and produce the same ISO 32000-1 / PDF/A-conformant bytes, so the +> choice is about *who is calling*, not about what comes out — and you can +> switch later without re-authoring your documents. + +## The decision, in prose + +**You are writing application code** — a Node.js, Deno, Bun or browser +service, a worker, a script with logic around the PDF. Use the **library** +(`npm install pdfnative`). It is the full surface: synchronous builders, the +parser, signatures with long-term validation, streaming, Web Worker support. +Everything the other three surfaces do, they do by calling this package. + +**You are driving a shell, a CI pipeline, a container, or a build tool in +another language.** Use the **CLI** (`pdfnative-cli`, binary `pdfnative`) — +17 commands over stdin/stdout pipelines, with an agent-native automation +contract: a `--json` envelope, stable `E_*` error codes, `--dry-run`, and +compact `--summary` / `--fields` output projection. No JavaScript required. + +**You are (or you are building) a conversational assistant with tool +access** — Claude Desktop, Cursor, Continue, Zed, or any Model Context +Protocol client. Use the **MCP server** (`pdfnative-mcp`, `npx -y +pdfnative-mcp`) — 28 tools with strict JSON Schemas, read-back tools for +self-verification, and no outbound network access except the +operator-configured TSA/OCSP/CRL endpoints. + +**Your host application is React 19.** Use the **React renderer** +(`pdfnative-react`) — declarative JSX compiled on-device to pdfnative blocks +by a custom reconciler (no DOM, no headless browser), with live-preview hooks +and the token-frugal `DocSpec` for agent authoring. React 19 is a peer +dependency of this package only; the engine stays zero-dependency. + +## Capability × surface + +The same facts in machine-readable form live in +[`docs/data/surfaces.json`](../data/surfaces.json); tool, command and export +names are verified against +[`docs/assets/ecosystem.json`](../assets/ecosystem.json) and the engine's +export surface by the documentation CI. Version annotations name the release +*of that surface's own package* which introduced the capability; an em-dash +means the surface does not offer it. + +| Capability | Library (`pdfnative`) | CLI (`pdfnative-cli`) | MCP (`pdfnative-mcp`) | React (`pdfnative-react`) | +|---|---|---|---|---| +| Generate documents | `buildDocumentPDFBytes` / `buildPDFBytes` | `render` | `generate_basic_pdf` (+ the dedicated document tools) | `renderToBytes` and friends, `` tree | +| Smart tables | `table` block | `render` (`table` block, or `--variant table`) | `add_table` | `` | +| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` | +| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | +| LTV ladder (B-T → B-LTA) | `signPdfBytesWithTimestamp`, `addValidationInfo`, `addDocumentTimestamp` _(v1.7.0)_ | — (verification only: `verify --revocation`) | `add_ltv`, `timestamp_pdf` _(v1.6.0)_ | — | +| Encrypt / decrypt | build-time `encryption` layout option; existing PDFs via the page-tree `encrypt` option and `openPdf` with a password | `encrypt` / `decrypt` _(v1.3.0)_ | `encrypt_pdf` / `decrypt_pdf` _(v1.5.0)_ | build-time only, via the `layout` render option | +| Fill / flatten forms | `readFormFields`, `fillForm`, `flattenForm` _(v1.6.0)_ | `fill` _(v1.3.0)_ | `read_form_fields`, `fill_form` _(v1.5.0)_ | — | +| Extract text | `extractText` _(v1.6.0)_ | `extract-text` _(v1.3.0)_ | `extract_text` | — | +| Merge / split / extract pages | `mergePdfs`, `splitPdf`, `extractPages` _(v1.4.0)_ | `merge` / `split` / `extract` _(v1.2.0)_ | `merge_pdfs` / `split_pdf` / `extract_pages` _(v1.3.0)_ | — | +| Markup annotations | `buildAnnotation` + `PdfModifier.addAnnotation` _(v1.5.0)_ | `annotate` _(v1.2.0)_ | `annotate_pdf` _(v1.4.0)_ | — | +| Inspect layout (pagination dry run) | `inspectDocumentLayout` _(v1.5.0)_ | `render --inspect-layout` _(v1.2.0)_ | `inspect_layout` _(v1.6.0)_ | `inspectDocument` | +| Validate PDF/UA | `validatePdfUA` | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | + +## Honest notes + +- **LTV differs by surface on purpose.** The engine opens no socket: in the + **library**, the RFC 3161 / OCSP / CRL transport is *injected by your code* + (`setTimestampProvider` / `setRevocationProvider`). On **MCP**, the transport + is *operator-configured* through environment variables + (`PDFNATIVE_MCP_TSA_URL`, `PDFNATIVE_MCP_REVOCATION`, an allow-list) — never + from tool arguments. The **CLI** currently *verifies* LTV material + (`verify --revocation`, RFC 3161 timestamp validation) but does not create + it — sign-side LTV is documented as out of scope there. +- **The engine ships no cryptographic signature verifier.** `listSignatures` + is an inventory; full verification (digest, CMS, chain, trust, timestamps, + revocation) lives in `pdfnative-cli verify` and the MCP `verify_pdf` tool. +- **React is an authoring surface.** It generates documents (including charts, + barcodes, SVG, form *widgets* and build-time encryption via the `layout` + render option) but does not operate on existing PDFs — no fill, extract, + merge or signing. When a React app needs those, call the library directly: + it is already installed as the renderer's peer dependency. +- **The MCP tool names, CLI commands and library exports in the table are the + complete story for these capabilities**, not a sample — where a cell is an + em-dash, the surface genuinely lacks the capability today rather than + hiding it under another name. + +## You can switch later + +All four surfaces call the same engine, so the artefacts are interchangeable: +a PDF rendered by the React reconciler can be signed by the CLI, inspected by +an MCP tool, and have its text extracted by the library. Document *inputs* +travel too — the CLI's `render` consumes the same `DocumentParams` JSON the +library takes, the MCP `generate_basic_pdf` blocks mirror the engine's block +kinds, and `pdfnative-react` compiles JSX (or a `DocSpec`) into that same +model. Starting on the "wrong" surface costs a call-site migration, not a +document rewrite. + +## Further reading + +- [Onboarding](onboarding.html) — the 90-second install-and-first-call for + each surface. +- [Self-verifying generation](self-verify.html) — the generate → inspect → + assert → correct loop on every surface. +- [Architecture](architecture.html) — how the four packages relate. +- [CLI guide](cli.html) · [MCP guide](mcp.html) · [React guide](react.html) — + the complete per-surface references. +- [Agent brief](../agent-brief.md) — the same decision tree in + paste-into-context form for AI agents. + --- # pdfnative-cli — Command-Line Interface Guide @@ -7411,7 +7524,7 @@ const bytes = renderToBytes( | `` + flexbox styles | *(none — declarative block flow; use blocks + ``)* | | `StyleSheet` | per-component props (`align`, `color`, `fontSize`, …) | | `` / `` / `` | same names, same shape | -| `usePDF()` | `usePdf()` | +| `usePDF()` | `usePdf()` | The biggest mental shift: there is **no flexbox layout engine**. Documents are a top-to-bottom block flow. Use ``, ``, tables, and per-component alignment props instead of `` containers. @@ -7445,6 +7558,306 @@ try { - 🤖 **AI integration:** [pdfnative-mcp guide](mcp.html) · 💻 **Terminal:** [pdfnative-cli guide](cli.html) - 🐛 **Report a bug:** [Nizoka/pdfnative-react/issues](https://github.com/Nizoka/pdfnative-react/issues) +--- + +# Self-verifying generation + +> **pdfnative can read what it writes, so a generated PDF never has to ship +> unchecked.** Plan pagination with `inspectDocumentLayout()` *before* building, +> then run `extractText()`, `validatePdfUA()` and `listSignatures()` / +> `openPdf()` against the emitted bytes, assert what matters, and correct the +> input until every assertion holds. The same generate → inspect → assert → +> correct loop runs on all four surfaces: in the library, on the CLI +> (`inspect --check`, stable exit codes and `E_*` error classes), and on MCP +> (`inspect_pdf` check assertions) — so a test suite, a CI job or an AI agent +> can close the loop without a human ever opening a viewer. + +## Why a PDF is normally a blind artefact + +Most PDF generators are write-only: the library that produced the file cannot +parse it back. The only verification available is a human opening the result in +a viewer — which no pipeline, test suite or autonomous agent can do. So defects +ship silently: a table that slipped onto a second page, a template placeholder +that was never substituted, a PDF/A claim the file cannot honour, a signature +placeholder that was never actually signed. + +pdfnative bundles a parser next to the writer, in the same zero-dependency +package. The read side (`openPdf()`, `extractText()`, `validatePdfUA()`, +`listSignatures()`) consumes exactly what the write side emits — and any other +PDF — which turns generation into a closed loop: + +1. **Plan** — measure pagination with `inspectDocumentLayout()` before a single + byte is produced. +2. **Generate** — build the document. +3. **Inspect** — read the emitted bytes back with the parser. +4. **Assert & correct** — check the facts you care about; on failure, fix the + *input* and regenerate. Never patch the output bytes. + +The rest of this guide walks the four verifiers with realistic +failure → fix examples, then shows the identical loop on the CLI and on MCP. + +## Verifier 1 — `inspectDocumentLayout()`: geometry, before generating + +_(v1.5.0)_ A pagination dry run: the same planning logic as the builder, but it +returns data instead of PDF bytes — no rendering, no font embedding. + +```ts +import { buildDocumentPDFBytes, inspectDocumentLayout } from 'pdfnative'; + +const params = { + title: 'Invoice #2026-041', + blocks: [ + { type: 'heading', level: 1, text: 'Invoice #2026-041' }, + { type: 'paragraph', text: intro }, + { type: 'table', headers: ['Item', 'Qty', 'Total'], rows }, + ], +}; + +// Plan — no PDF is produced. +const layout = inspectDocumentLayout(params); + +// Assert: a one-page invoice. +if (layout.totalPages !== 1) { + throw new Error(`expected 1 page, planned ${layout.totalPages}`); +} + +// Assert: the table starts on page 1, not after a stray break. +const table = layout.pages[0].blocks.find((b) => b.type === 'table'); +if (!table) throw new Error('table was pushed off page 1'); + +// Only now generate. +const bytes = buildDocumentPDFBytes(params); +``` + +**Failure → fix.** `totalPages` comes back as `2` because this month's `rows` +grew by three entries. Fixes live in the input: shorten `intro`, reduce the +margins in the layout options, or accept the second page and change the +assertion — the table planner repeats headers on continuation pages by default, +so a deliberate two-pager stays readable. Re-run the inspection until the +assertion holds, then build. + +> **Honest caveat.** `inspectDocumentLayout()` shares the builder's measurement +> code (`estimateBlockHeight` / `planTable` and its constants) — that is +> precisely why its geometry matches where the renderer places each block, +> including table slicing across page breaks. But it is a *prediction from the +> shared engine*, not an independent re-parse of the emitted bytes. For +> verification of the actual output, pair it with `extractText()` below. See +> [Layout debugging & inspection](debugging.html) for the full data shape and +> the visual overlay variant. + +## Verifier 2 — `extractText()`: is the content actually in the file? + +_(v1.6.0)_ Reading-order Unicode text per page, decoded from the content +streams of the emitted bytes — pdfnative-generated documents round-trip +losslessly, including CJK, Arabic and emoji. + +```ts +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; + +const bytes = buildDocumentPDFBytes(params); + +const pages = extractText(bytes); // ExtractedPageText[] +// Each entry: { pageIndex: number, text: string, runs?: ExtractedTextRun[] } +const flat = pages.map((p) => p.text).join('\n'); + +for (const required of ['Invoice #2026-041', 'Total', '1 240,00 €']) { + if (!flat.includes(required)) { + throw new Error(`missing from the emitted PDF: "${required}"`); + } +} +if (flat.includes('{{')) { + throw new Error('an unresolved template placeholder reached the PDF'); +} +``` + +**Failure → fix.** The total is missing: the value never entered `blocks` — +typically a renamed data field, or a templating step that ran after the build +instead of before it. The `{{` guard catches the classic half-rendered +template. Fix the data flow and regenerate. Positioned runs +(`{ includeRuns: true }`) additionally give each string's `x` / `y` / +`fontSize` when you need to assert *where* something landed, not just that it +exists. + +Extraction has documented limits (no OCR on image content, lossy conjuncts in +shaped Indic scripts) — see [Text extraction](text-extraction.html) before +asserting on those cases. + +## Verifier 3 — `validatePdfUA()`: structural PDF/UA + +Read-only ISO 14289-1 structural checks over the emitted bytes: `/MarkInfo`, +the structure tree, the parent tree, a document `/Lang`, per-page MCID +uniqueness. + +```ts +import { buildDocumentPDFBytes, validatePdfUA } from 'pdfnative'; + +const bytes = buildDocumentPDFBytes(params, { tagged: true }); + +const report = validatePdfUA(bytes); // { valid, errors, warnings } +if (!report.valid) { + throw new Error(`PDF/UA structure: ${report.errors.join('; ')}`); +} +``` + +**Failure → fix.** The most common failure is building without `tagged` — the +document then has no structure tree at all, and every structural check fires. +Set `layout: { tagged: true }` (or a `'pdfa*'` level) and regenerate. Warnings +are non-blocking best-practice notes; decide explicitly whether your pipeline +tolerates them. + +> **Honest caveat.** This is a *structural* validation, not an accessibility +> audit. A passing report means the machine-checkable structure is present and +> consistent — it cannot judge whether the reading order makes sense to a +> human, whether alt text is meaningful, or whether colour contrast is +> adequate. And it is not a PDF/A validator: for archival conformance, run +> veraPDF as described in [PDF/A conformance](pdfa.html). + +## Verifier 4 — `listSignatures()` + `openPdf()`: signatures, encryption, metadata + +_(v1.7.0)_ `listSignatures()` inventories every signature field in the +document; `openPdf()` exposes page count, encryption state and the `/Info` +dictionary. + +```ts +import { openPdf, listSignatures } from 'pdfnative'; + +const sigs = listSignatures(signedBytes); +if (sigs.length === 0) throw new Error('no signature field found'); +if (sigs.some((s) => s.isPlaceholder)) { + throw new Error('a /Sig field is still an unsigned placeholder'); +} +// Per entry: fieldName?, subFilter, byteRange, isDocTimestamp, isPlaceholder. +const pades = sigs.filter((s) => s.subFilter === 'ETSI.CAdES.detached'); + +const reader = openPdf(signedBytes); +if (reader.encryption !== null) throw new Error('expected an unencrypted document'); +if (reader.pageCount < 2) throw new Error('terms page missing'); +const info = reader.getInfo(); +``` + +**Failure → fix.** `isPlaceholder: true` means `addSignaturePlaceholder()` ran +but `signPdfBytes()` never filled that field — usually a skipped signing step, +or a `fieldName` selector that targeted a different placeholder. Sign the +document (or the right field) and re-check. `isDocTimestamp` distinguishes +`/DocTimeStamp` entries when you assert on the PAdES ladder. + +> **Honest caveat.** `listSignatures()` is an inventory, not a cryptographic +> verdict — the pdfnative engine deliberately ships no signature verifier. +> Full verification (byte-range digest, CMS signature value, certificate +> chain, trust, timestamps, revocation) lives in `pdfnative-cli verify` and in +> the MCP `verify_pdf` tool, below. See +> [Long-term validation (LTV)](ltv.html). + +## The same loop on the CLI + +`pdfnative-cli` turns the assertions into exit codes, which is what a shell or +CI step branches on: + +```bash +# Plan (pre-flight): pagination report instead of a PDF (v1.2.0) +pdfnative render --input invoice.json --inspect-layout > layout.json + +# Generate, then assert — exit 0 when every --check holds, 1 otherwise +pdfnative render --input invoice.json --output invoice.pdf --tagged pdfa2b +pdfnative inspect --input invoice.pdf --check pdfa --check pdfua --json --summary +echo $? # 0 = all assertions hold, 1 = at least one failed + +# Content presence, pipeline-style +pdfnative extract-text --input invoice.pdf --format text | grep -q 'Invoice #2026-041' +``` + +- `inspect --check` takes repeatable, ANDed assertions — `pdfa`, `signed`, + `encrypted`, `pdfua` _(v1.1.0)_ — and sets the exit code accordingly; under + `--json`, a failed assertion also emits the stable error class + `E_CHECK_FAILED` in the machine-readable envelope on stderr + (`{ ok: false, command, error: { code, message } }`). +- `--summary` _(v1.1.0)_ shrinks the stdout verdict to + `{ pages, encrypted, signatures, pdfa }`; `--fields a,b.c` projects arbitrary + dot-paths — both exist so agents branch on facts without paying for the full + report. +- `pdfnative verify --strict` performs the cryptographic half: byte-range + digest, CMS signature value (RSA and ECDSA), certificate chain and trust + against `--trust` roots, RFC 3161 timestamps, OCSP/CRL revocation — exit 1 + and `E_VERIFY_FAILED` on any failure *or on zero signatures*. + +The full command reference, including every `--check` flag and the `E_*` +catalogue, is in the [CLI guide](cli.html). + +## The same loop on MCP + +`pdfnative-mcp` gives a conversational agent the identical verifiers as +read-only tools — the natural final step after any document tool call: + +| Tool | Verifies | +|---|---| +| `inspect_pdf` | Version, page count, encryption, PDF/A claim, signatures, attachments, `/Info` — plus `check` assertions with a boolean verdict. | +| `inspect_layout` _(v1.6.0)_ | Pagination dry run for a prospective document — page count and block geometry, no PDF produced, no base64 output tokens spent. | +| `validate_pdf` | Structural PDF/UA — `{ valid, errors, warnings }`. | +| `verify_pdf` | Cryptographic signature verification; `ltv: true` _(v1.6.0)_ reports the achieved PAdES level (B-B → B-LTA). | + +```jsonc +{ "tool": "inspect_pdf", + "input": { "pdfBase64": "<...>", "check": ["pdfa", "signed"] } } +// → { ..., "checks": { "pdfa": true, "signed": true }, "checksPassed": true } +``` + +- `check` accepts `pdfa`, `signed`, `encrypted`, `placeholder`, `attachments`, + and — _(v1.6.0)_ — `dss`, `docTimestamp`, `trapped`, `annotations`. + `checksPassed` is the AND of all requested assertions; since v1.6.0 `checks` + contains **only** the requested keys, so branch on `checksPassed` or on a key + you asked for, never on an absent one. +- `inspect_layout` accepts the same `title` / `blocks` (plus every input that + moves a block) as `generate_basic_pdf`, so an agent can iterate on pagination + before spending output tokens on real bytes. Known engine gap: a `toc` block + is measured as 0 pt, so a document with a printed contents page may paginate + one page later than previewed. +- `validate_pdf` on unparsable bytes is an error (`PDF_PARSE_FAILED`), not a + `{ valid: false }` verdict, since v1.6.0 — a parse failure is not a PDF/UA + result. + +Tool-by-tool inputs and error codes are in the [MCP guide](mcp.html). + +## Wire it into CI and agent loops + +**CI.** Chain the loop with `set -e` so any failed assertion fails the job — +no PDF-viewing human in the path: + +```bash +set -e +pdfnative render --input report.json --output report.pdf --tagged pdfa2b +pdfnative sign --input report.pdf --output signed.pdf --key k.pem --cert c.pem +pdfnative inspect --input signed.pdf --check pdfa --check signed --json --summary +pdfnative verify --input signed.pdf --strict --trust ca-root.pem +``` + +`--dry-run` on the write commands validates inputs without producing output, +which makes a cheap first CI stage; `--summary` / `--fields` keep logs and +agent context small. + +**Agent loops.** Treat every generation as unfinished until a read tool +confirms it: after `generate_basic_pdf` (or any document tool), call +`inspect_pdf` with the `check` list that encodes the user's requirements and +branch on `checksPassed`; on failure, correct the tool input and regenerate +rather than apologising with an unverified file. Use `inspect_layout` before +generating and `verbosity: "summary"` on the read tools to keep token spend +proportional. The [agent brief](../agent-brief.md) condenses this contract +into a paste-into-context form. + +## Further reading + +- [Agent brief](../agent-brief.md) — the compact form of this loop for AI + agents, verified by the documentation CI. +- [Layout debugging & inspection](debugging.html) — the visual overlay and the + full `LayoutInspection` data shape. +- [Text extraction](text-extraction.html) — decoding rules, positioned runs, + documented limits. +- [PDF/A conformance](pdfa.html) — validating archival claims with veraPDF, + and the build-time `PDFA_*` diagnostics. +- [Long-term validation (LTV)](ltv.html) — the PAdES ladder the signature + assertions climb. +- [CLI guide](cli.html) · [MCP guide](mcp.html) — the complete surface + references. + --- # Digital signatures in pdfnative diff --git a/docs/llms-index.json b/docs/llms-index.json index 05b43c4..c22aecb 100644 --- a/docs/llms-index.json +++ b/docs/llms-index.json @@ -5,8 +5,8 @@ { "url": "https://pdfnative.dev/llms.txt", "description": "Documentation index (llmstxt.org convention).", - "bytes": 14343, - "approxTokens": 3586 + "bytes": 15130, + "approxTokens": 3783 }, { "url": "https://pdfnative.dev/agent-brief.md", @@ -17,8 +17,14 @@ { "url": "https://pdfnative.dev/llms-full.txt", "description": "Full corpus: index + README + every guide, one request.", - "bytes": 463864, - "approxTokens": 115966 + "bytes": 485829, + "approxTokens": 121457 + }, + { + "url": "https://pdfnative.dev/llms-recipes.txt", + "description": "Executable recipes: CI-verified, copy-ready code for the most common tasks.", + "bytes": 24351, + "approxTokens": 6088 }, { "url": "https://github.com/Nizoka/pdfnative/blob/main/README.md", @@ -126,6 +132,21 @@ "bytes": 9748, "approxTokens": 2437 }, + { + "title": "Choosing your surface", + "summary": "Write application code → the library. Drive a shell, Makefile or CI job →", + "html": "https://pdfnative.dev/guides/choose.html", + "markdown": "https://pdfnative.dev/guides/choose.md", + "anchors": [ + "the-decision-in-prose", + "capability-surface", + "honest-notes", + "you-can-switch-later", + "further-reading" + ], + "bytes": 7177, + "approxTokens": 1794 + }, { "title": "pdfnative-cli — Command-Line Interface Guide", "summary": "Tracks the latest published `pdfnative-cli` (v1.3.0, built on the pdfnative 1.6 engine — pins `^1.6.0`, which semver-accepts 1.7.0). The CLI versions independently from the library. Live package versions — and the `pdfnative` version each one is built on — are shown at the top of the documentation h", @@ -427,8 +448,27 @@ "diagnostics", "resources" ], - "bytes": 17383, - "approxTokens": 4346 + "bytes": 17489, + "approxTokens": 4372 + }, + { + "title": "Self-verifying generation", + "summary": "pdfnative can read what it writes, so a generated PDF never has to ship", + "html": "https://pdfnative.dev/guides/self-verify.html", + "markdown": "https://pdfnative.dev/guides/self-verify.md", + "anchors": [ + "why-a-pdf-is-normally-a-blind-artefact", + "verifier-1-inspectdocumentlayout-geometry-before-generating", + "verifier-2-extracttext-is-the-content-actually-in-the-file", + "verifier-3-validatepdfua-structural-pdfua", + "verifier-4-listsignatures-openpdf-signatures-encryption-metadata", + "the-same-loop-on-the-cli", + "the-same-loop-on-mcp", + "wire-it-into-ci-and-agent-loops", + "further-reading" + ], + "bytes": 13802, + "approxTokens": 3451 }, { "title": "Digital signatures in pdfnative", diff --git a/docs/llms-recipes.txt b/docs/llms-recipes.txt new file mode 100644 index 0000000..cb6cfbd --- /dev/null +++ b/docs/llms-recipes.txt @@ -0,0 +1,680 @@ +# pdfnative — executable recipes + +> Every file below lives in the repository as `recipes/.ts`, imports only +> from 'pdfnative', and is executed in CI with each `@expect` assertion checked +> (tests/docs/recipes.test.ts). The machine index is recipes/index.json. + +--- + + +```ts +/** + * Arabic text with positional shaping and right-to-left layout. The Noto + * Naskh Arabic data module is registered as a lazy loader, resolved with + * `loadFontData`, and passed through `fontEntries`; the engine embeds a + * subsetted CIDFont (Identity-H) and shapes the letterforms. + * + * @task Render shaped right-to-left Arabic text with an embedded Noto font + * @surface library + * @since 1.3.0 + * @expect pages === 1 + * @expect pdf contains '/FontFile2' + * @expect pdf contains '/Identity-H' + */ +import { buildDocumentPDFBytes, openPdf, registerFont, loadFontData } from 'pdfnative'; +import type { DocumentParams, FontEntry, FontLoader } from 'pdfnative'; + +// The generated data modules predate the full FontData declaration, hence +// the loader cast — the runtime shape is complete. +registerFont('ar', (() => import('pdfnative/fonts/noto-arabic-data.js')) as unknown as FontLoader); + +export async function run(): Promise<{ bytes: Uint8Array; pages: number }> { + const arabic = await loadFontData('ar'); + if (!arabic) throw new Error('Arabic font data failed to load'); + + const fontEntries: FontEntry[] = [{ fontData: arabic, fontRef: '/F3', lang: 'ar' }]; + const params: DocumentParams = { + title: 'Arabic shaping', + blocks: [ + { type: 'heading', text: 'Positional forms and ligatures', level: 1 }, + { type: 'paragraph', text: 'السلام عليكم ورحمة الله وبركاته.' }, + { type: 'paragraph', text: 'النص العربي يُعرض من اليمين إلى اليسار.' }, + ], + footerText: 'Arabic recipe', + fontEntries, + }; + + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + return { bytes, pages: openPdf(bytes).pageCount }; +} +``` + +--- + + +```ts +/** + * Native vector charts (charts v2): a stacked bar chart of quarterly + * revenue and a scatter plot on a linear x-axis, one per page. Charts are + * pure PDF path operators — no rasterisation, no dependencies. + * + * @task Render stacked-bar and scatter charts as native vector graphics + * @surface library + * @since 1.7.0 + * @expect pages === 2 + */ +import { buildDocumentPDFBytes, openPdf } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Quarterly figures', + blocks: [ + { + type: 'chart', + chartType: 'stackedBar', + title: 'Revenue by region', + categories: ['Q1', 'Q2', 'Q3', 'Q4'], + series: [ + { label: 'EMEA', values: [120, 135, 128, 150] }, + { label: 'Americas', values: [90, 105, 118, 122] }, + { label: 'APAC', values: [45, 52, 61, 70] }, + ], + height: 220, + legend: 'bottom', + }, + { type: 'pageBreak' }, + { + type: 'chart', + chartType: 'scatter', + title: 'Latency against payload size', + xAxis: { type: 'linear', grid: true }, + series: [ + { label: 'Samples', values: [12, 18, 25, 31, 44], xValues: [10, 25, 50, 75, 100] }, + ], + height: 220, + dataLabels: { decimals: 0, suffix: ' ms' }, + }, + ], + footerText: 'Quarterly figures', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + return { bytes, pages: openPdf(bytes).pageCount }; +} +``` + +--- + + +```ts +/** + * AcroForm round trip: author a form, enumerate its fields, fill a value, + * and read it back. A second fill with `flatten: true` stamps the + * appearance into the page content, where plain text extraction finds it. + * + * @task Author an AcroForm, fill it, and read the value back + * @surface library + * @since 1.6.0 + * @expect field 'fullName' value === 'Ada Lovelace' + * @expect text of page 0 of the flattened document contains 'Ada Lovelace' + */ +import { buildDocumentPDFBytes, readFormFields, fillForm, extractText } from 'pdfnative'; +import type { DocumentParams, ParsedFormField } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Application', + blocks: [ + { type: 'heading', text: 'Application form', level: 1 }, + { type: 'formField', fieldType: 'text', name: 'fullName', label: 'Full name' }, + { type: 'formField', fieldType: 'checkbox', name: 'agree', label: 'I agree to the terms' }, + { type: 'formField', fieldType: 'dropdown', name: 'country', label: 'Country', options: ['France', 'Germany', 'Spain'] }, + ], + footerText: 'Application', +}; + +export async function run(): Promise<{ + bytes: Uint8Array; + fields: readonly ParsedFormField[]; + filledValue: string | readonly string[] | boolean | null; + flattenedText: string; +}> { + const blank = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + + // Enumerate the authored fields, then fill by fully-qualified name. + const values = { fullName: 'Ada Lovelace', agree: true, country: 'Germany' } as const; + const filled = fillForm(blank, values); + const fields = readFormFields(filled); + const filledValue = fields.find(f => f.name === 'fullName')?.value ?? null; + + // Flattening replaces the widgets with static page content. + const flattened = fillForm(blank, values, { flatten: true }); + const flattenedText = extractText(flattened, { pages: [0] })[0].text; + + return { bytes: filled, fields, filledValue, flattenedText }; +} +``` + +--- + + +```ts +/** + * Invoice as tagged PDF/A-2b — an itemised table with an embedded Latin + * font, so the archival claim survives validation. The creation date is + * pinned to keep the output byte-identical across runs. + * + * @task Build a PDF/A-2b invoice with an itemised line-item table + * @surface library + * @since 1.6.0 + * @expect pages === 1 + * @expect text of page 0 contains 'Invoice' + * @expect pdfA claim === 'pdfa2b' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams, FontData, FontEntry } from 'pdfnative'; +import * as notoSans from 'pdfnative/fonts/noto-sans-data.js'; + +// PDF/A requires every rendered glyph to come from an embedded font +// (ISO 19005 §6.2.11.4.1); the bundled Noto Sans data module covers Latin. +const latinFont: FontEntry = { + fontRef: '/F1', + fontData: notoSans as unknown as FontData, +}; + +const params: DocumentParams = { + title: 'Invoice INV-2026-0042', + blocks: [ + { type: 'paragraph', text: 'Billed to: Acme Widgets Ltd, 4 Foundry Lane, Sheffield' }, + { type: 'spacer', height: 8 }, + { + type: 'table', + caption: 'Line items', + headers: ['Item', 'Quantity', 'Unit price', 'Total'], + rows: [ + { cells: ['Consultancy (June)', '3 days', '650.00', '1,950.00'], type: 'debit', pointed: false }, + { cells: ['Managed hosting', '1 month', '120.00', '120.00'], type: 'debit', pointed: false }, + { cells: ['Support retainer', '1 month', '250.00', '250.00'], type: 'debit', pointed: false }, + ], + }, + { type: 'spacer', height: 8 }, + { type: 'paragraph', text: 'Total due: 2,320.00 GBP within 30 days.', align: 'right' }, + ], + footerText: 'Registered in England no. 01234567', + fontEntries: [latinFont], + metadata: { author: 'Accounts', subject: 'Invoice INV-2026-0042' }, +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; text: string }> { + const bytes = buildDocumentPDFBytes(params, { + tagged: 'pdfa2b', + creationDate: new Date('2026-08-25T00:00:00Z'), + }); + const pages = openPdf(bytes).pageCount; + const text = extractText(bytes, { pages: [0] })[0].text; + return { bytes, pages, text }; +} +``` + +--- + + +```ts +/** + * Dry-run layout inspection: `inspectDocumentLayout` reports how the + * builder will paginate and place each block — page index, x, top, width, + * height — without rendering a PDF. Useful for layout assertions and + * tooling. + * + * @task Preview pagination and block geometry without rendering a PDF + * @surface library + * @since 1.5.0 + * @expect totalPages === 2 + * @expect first block type === 'heading' on page 0 with width > 0 + */ +import { inspectDocumentLayout } from 'pdfnative'; +import type { DocumentParams, LayoutInspection } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Layout probe', + blocks: [ + { type: 'heading', text: 'Section one', level: 1 }, + { type: 'paragraph', text: 'A paragraph measured, not rendered.' }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Section two', level: 1 }, + { type: 'paragraph', text: 'Placed on the second page.' }, + ], + footerText: 'Layout probe', +}; + +export async function run(): Promise<{ inspection: LayoutInspection }> { + const inspection = inspectDocumentLayout(params); + return { inspection }; +} +``` + +--- + + +```ts +/** + * Merge two documents and encrypt the result in the same call. + * `MergeOptions.encrypt` re-protects the assembled document (AES-128 + * here); the reader then opens it with the user password and reports the + * scheme. Encryption uses random salts, so the bytes differ per run while + * the structure stays identical. + * + * @task Merge two PDFs and AES-encrypt the combined document + * @surface library + * @since 1.6.0 + * @expect pages === 2 + * @expect encryption.algorithm === 'aes128' + */ +import { buildDocumentPDFBytes, mergePdfs, openPdf } from 'pdfnative'; +import type { DocumentParams, PdfEncryptionInfo } from 'pdfnative'; + +function chapter(title: string, body: string): DocumentParams { + return { + title, + blocks: [{ type: 'paragraph', text: body }], + footerText: title, + }; +} + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; encryption: PdfEncryptionInfo | null }> { + const created = new Date('2026-08-25T00:00:00Z'); + const first = buildDocumentPDFBytes(chapter('Part one', 'Opening chapter.'), { creationDate: created }); + const second = buildDocumentPDFBytes(chapter('Part two', 'Closing chapter.'), { creationDate: created }); + + const bytes = mergePdfs([first, second], { + encrypt: { + ownerPassword: 'owner-secret', + userPassword: 'reader-secret', + algorithm: 'aes128', + permissions: { print: true, copy: false }, + }, + }); + + const reader = openPdf(bytes, { password: 'reader-secret' }); + return { bytes, pages: reader.pageCount, encryption: reader.encryption }; +} +``` + +--- + + +```ts +/** + * A table long enough to paginate. `repeatHeader` (the default, stated + * here explicitly) re-draws the header row on every continuation page, + * which the extracted text of page 1 confirms. + * + * @task Paginate a long table with the header row repeated on every page + * @surface library + * @since 1.6.0 + * @expect pages === 2 + * @expect text of page 1 contains 'Description' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams, PdfRow } from 'pdfnative'; + +const rows: PdfRow[] = Array.from({ length: 70 }, (_, i) => ({ + cells: [`2026-06-${String((i % 28) + 1).padStart(2, '0')}`, `Ledger entry ${i + 1}`, (100 + i).toFixed(2)], + type: i % 2 === 0 ? 'credit' : 'debit', + pointed: false, +})); + +const params: DocumentParams = { + title: 'June ledger', + blocks: [ + { + type: 'table', + headers: ['Date', 'Description', 'Amount'], + rows, + repeatHeader: true, + }, + ], + footerText: 'June ledger', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; page1Text: string }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const pages = openPdf(bytes).pageCount; + const page1Text = extractText(bytes, { pages: [1] })[0].text; + return { bytes, pages, page1Text }; +} +``` + +--- + + +```ts +/** + * Print production: the page is designed at trim size plus 3 mm bleed on + * every side (8.5 pt), `layout.print.bleed` derives the TrimBox and + * BleedBox, and `marks: true` draws crop and registration marks outside + * the trim area. The boxes are read back from the parsed page. + * + * @task Prepare a print-ready page with bleed, TrimBox and printer's marks + * @surface library + * @since 1.7.0 + * @expect trimBox === [8.5, 8.5, 603.78, 850.39] + * @expect bleedBox === [0, 0, 612.28, 858.89] + */ +import { buildDocumentPDFBytes, openPdf, dictGetArray } from 'pdfnative'; + +const BLEED = 8.5; // 3 mm in points +const TRIM_W = 595.28; // A4 trim width +const TRIM_H = 841.89; // A4 trim height + +export async function run(): Promise<{ + bytes: Uint8Array; + trimBox: readonly number[]; + bleedBox: readonly number[]; +}> { + const bytes = buildDocumentPDFBytes( + { + title: 'Poster', + blocks: [{ type: 'paragraph', text: 'Background art runs to the page edge; keep copy inside the trim.' }], + footerText: 'Poster', + }, + { + // Page size = trim size + 2 × bleed; backgrounds may run to the edge. + pageWidth: TRIM_W + 2 * BLEED, + pageHeight: TRIM_H + 2 * BLEED, + print: { bleed: BLEED, marks: true }, + creationDate: new Date('2026-08-25T00:00:00Z'), + }, + ); + + const page = openPdf(bytes).getPage(0); + const asNumbers = (name: string): number[] => + (dictGetArray(page, name) ?? []).filter((v): v is number => typeof v === 'number'); + + return { bytes, trimBox: asNumbers('TrimBox'), bleedBox: asNumbers('BleedBox') }; +} +``` + +--- + + +```ts +/** + * PAdES B-B digital signature. The document is built, an invisible + * signature placeholder is injected (with the CAdES subFilter and the + * descriptive entries baked in — the /Sig dictionary's byte layout is + * frozen at placeholder time), then signed. The caller supplies the + * certificate and RSA key; pdfnative never generates key material. + * + * @task Sign a document with a PAdES B-B (ETSI.CAdES.detached) signature + * @surface library + * @since 1.7.0 + * @expect signatures.length === 1 + * @expect signatures[0].subFilter === 'ETSI.CAdES.detached' + * @expect signatures[0].isPlaceholder === false + */ +import { buildDocumentPDFBytes, addSignaturePlaceholder, signPdfBytes, listSignatures } from 'pdfnative'; +import type { DocumentParams, PdfSignatureInfo, RsaPrivateKey, X509Certificate } from 'pdfnative'; + +/** Key material supplied by the caller (e.g. from a PKCS#12 store). */ +export interface SignerMaterial { + readonly cert: X509Certificate; + readonly key: RsaPrivateKey; + readonly chain?: readonly X509Certificate[]; +} + +const params: DocumentParams = { + title: 'Service agreement', + blocks: [ + { type: 'heading', text: 'Agreement', level: 1 }, + { type: 'paragraph', text: 'This agreement is executed by digital signature.' }, + ], + footerText: 'Service agreement', +}; + +export async function run(signer: SignerMaterial): Promise<{ bytes: Uint8Array; signatures: readonly PdfSignatureInfo[] }> { + const unsigned = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + + const placeheld = addSignaturePlaceholder(unsigned, { + fieldName: 'Author', + metadata: { + subFilter: 'ETSI.CAdES.detached', + reason: 'Approval', + location: 'London', + signingTime: new Date('2026-08-25T00:00:00Z'), + }, + }); + + const bytes = signPdfBytes(placeheld, { + signerCert: signer.cert, + certChain: signer.chain, + rsaKey: signer.key, + algorithm: 'rsa-sha256', + profile: 'pades', + signingTime: new Date('2026-08-25T00:00:00Z'), + }); + + return { bytes, signatures: listSignatures(bytes) }; +} +``` + +--- + + +```ts +/** + * True streaming output for a large document: `buildDocumentPDFStreamTrue` + * yields the PDF in chunks whose concatenation is byte-identical to the + * buffered builder's output for the same input (creation date pinned so + * both builds share it). + * + * @task Stream a 300-block document and match the buffered output byte for byte + * @surface library + * @since 1.3.0 + * @expect identical === true + * @expect pages === 8 + */ +import { buildDocumentPDFBytes, buildDocumentPDFStreamTrue, concatChunks, openPdf } from 'pdfnative'; +import type { DocumentParams, DocumentBlock } from 'pdfnative'; + +const blocks: DocumentBlock[] = Array.from({ length: 300 }, (_, i) => ({ + type: 'paragraph' as const, + text: `Row ${i + 1}: measurement recorded and archived.`, +})); + +const params: DocumentParams = { + title: 'Measurement log', + blocks, + footerText: 'Measurement log', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; identical: boolean }> { + const created = new Date('2026-08-25T00:00:00Z'); + + const chunks: Uint8Array[] = []; + for await (const chunk of buildDocumentPDFStreamTrue(params, { creationDate: created }, { chunkSize: 16 * 1024 })) { + chunks.push(chunk); + } + const bytes = concatChunks(chunks); + + const buffered = buildDocumentPDFBytes(params, { creationDate: created }); + const identical = bytes.length === buffered.length && bytes.every((b, i) => b === buffered[i]); + + return { bytes, pages: openPdf(bytes).pageCount, identical }; +} +``` + +--- + + +```ts +/** + * Positioned text extraction for indexing pipelines (RAG, search). With + * `includeRuns` each text-showing operation is returned with its + * device-space origin and effective font size, alongside the + * reading-order text per page. + * + * @task Extract reading-order text plus positioned runs from a document + * @surface library + * @since 1.6.0 + * @expect text of page 0 contains 'retrieval' + * @expect runs.length > 0 + * @expect every run has numeric x, y and fontSize + */ +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; +import type { DocumentParams, ExtractedPageText } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Corpus notes', + blocks: [ + { type: 'heading', text: 'Chunking strategy', level: 1 }, + { type: 'paragraph', text: 'Split documents into passages before retrieval; keep headings with their sections.' }, + { type: 'paragraph', text: 'Store the run positions so citations can point back into the page.' }, + ], + footerText: 'Corpus notes', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: readonly ExtractedPageText[] }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const pages = extractText(bytes, { includeRuns: true }); + return { bytes, pages }; +} +``` + +--- + + +```ts +/** + * Table of contents and bookmarks together: a `toc` block renders linked + * entries for every heading, while `outline: 'auto'` derives the viewer's + * bookmark panel from the same headings and adds /Outlines to the catalog. + * + * @task Generate a linked table of contents plus automatic bookmarks + * @surface library + * @since 1.6.0 + * @expect pages === 3 + * @expect catalog has /Outlines + * @expect text of page 0 contains 'Table of Contents' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Operations handbook', + blocks: [ + { type: 'toc', maxLevel: 2 }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Onboarding', level: 1 }, + { type: 'paragraph', text: 'Accounts, hardware and access requests.' }, + { type: 'heading', text: 'First week', level: 2 }, + { type: 'paragraph', text: 'Pairing schedule and reading list.' }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Incident response', level: 1 }, + { type: 'paragraph', text: 'Escalation ladder and post-incident review.' }, + ], + footerText: 'Operations handbook', + outline: 'auto', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; hasOutlines: boolean; tocText: string }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const reader = openPdf(bytes); + const hasOutlines = reader.getCatalog().get('Outlines') !== undefined; + const tocText = extractText(bytes, { pages: [0] })[0].text; + return { bytes, pages: reader.pageCount, hasOutlines, tocText }; +} +``` + +--- + + +```ts +/** + * Incremental metadata update: open an existing document, re-issue its + * /Info dictionary with a new title and a pinned modification date, and + * save. The original revision is preserved byte for byte; the reopened + * document reports the new title. + * + * @task Retitle an existing PDF via a non-destructive incremental update + * @surface library + * @since 1.7.0 + * @expect title === 'Quarterly report (revised)' + */ +import { buildDocumentPDFBytes, openPdf, createModifier } from 'pdfnative'; + +/** /Info strings may be UTF-16BE with a BOM; plain literals pass through. */ +function decodePdfText(raw: unknown): string { + if (typeof raw !== 'string') return ''; + if (raw.length >= 2 && raw.charCodeAt(0) === 0xFE && raw.charCodeAt(1) === 0xFF) { + let out = ''; + for (let i = 2; i + 1 < raw.length; i += 2) { + out += String.fromCharCode((raw.charCodeAt(i) << 8) | raw.charCodeAt(i + 1)); + } + return out; + } + return raw; +} + +export async function run(): Promise<{ bytes: Uint8Array; title: string }> { + const original = buildDocumentPDFBytes( + { + title: 'Quarterly report', + blocks: [{ type: 'paragraph', text: 'Figures under review.' }], + footerText: 'Quarterly report', + }, + { creationDate: new Date('2026-08-25T00:00:00Z') }, + ); + + const modifier = createModifier(openPdf(original)); + modifier.updateMetadata({ + title: 'Quarterly report (revised)', + modDate: new Date('2026-08-25T00:00:00Z'), + }); + const bytes = modifier.save(); + + const info = openPdf(bytes).getInfo(); + const title = decodePdfText(info?.get('Title')); + return { bytes, title }; +} +``` + +--- + + +```ts +/** + * A rotated, semi-transparent text watermark behind the content of every + * page. The watermark never disturbs the body text: extraction still + * returns the paragraph, and the watermark string itself is present too. + * + * @task Stamp a rotated DRAFT watermark behind the page content + * @surface library + * @since 1.6.0 + * @expect text of page 0 contains 'Confidential clause' + * @expect text of page 0 contains 'DRAFT' + */ +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Draft contract', + blocks: [ + { type: 'heading', text: 'Terms', level: 1 }, + { type: 'paragraph', text: 'Confidential clause: neither party discloses the commercial terms.' }, + ], + footerText: 'Draft contract', +}; + +export async function run(): Promise<{ bytes: Uint8Array; text: string }> { + const bytes = buildDocumentPDFBytes(params, { + watermark: { + text: { text: 'DRAFT', fontSize: 60, opacity: 0.15, angle: -45 }, + position: 'background', + }, + creationDate: new Date('2026-08-25T00:00:00Z'), + }); + const text = extractText(bytes, { pages: [0] })[0].text; + return { bytes, text }; +} +``` diff --git a/docs/llms.txt b/docs/llms.txt index 5701cc7..577e5f5 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -62,6 +62,9 @@ - [agent-brief.md](https://pdfnative.dev/agent-brief.md): A compact briefing for AI agents writing code with pdfnative — the core API, the verified pitfalls that produce wrong code, the surface decision tree, and the self-verification loop. Designed to be pasted into a coding agent's context. - [llms-full.txt](https://pdfnative.dev/llms-full.txt): This index plus the README and the full Markdown source of every guide, in one request. +- [llms-recipes.txt](https://pdfnative.dev/llms-recipes.txt): The executable recipe corpus — standalone TypeScript files importing from `pdfnative`, each executed in CI with its `@expect` assertions verified, so the code you copy is proven to compile and to produce exactly the artefact its header describes. +- [api.json](https://pdfnative.dev/assets/api.json): The public API surface, derived mechanically from the export statements of `src/index.ts` — name, kind, source module, signature and TSDoc summary where extractable. The substitute for the unpublished `index.d.ts`. +- [surfaces.json](https://pdfnative.dev/data/surfaces.json): The capability × surface matrix (library / CLI / MCP / React) behind the [Choosing your surface](https://pdfnative.dev/guides/choose.md) guide. - [llms-index.json](https://pdfnative.dev/llms-index.json): Per-page machine index — HTML and raw-Markdown URL, title, summary, section anchors, and exact byte / approximate token sizes for every artefact, so you can decide what to fetch before spending the tokens. - Every guide serves its raw Markdown at the same URL with `.md` instead of `.html`, declared via `` in each page head. All of these URLs are listed in the sitemap. diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 6841e4a..4f6bcd8 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -662,5 +662,45 @@ monthly 0.3 + + https://pdfnative.dev/guides/choose.html + + + 2026-08-25 + monthly + 0.7 + + + https://pdfnative.dev/guides/self-verify.html + + + 2026-08-25 + monthly + 0.7 + + + https://pdfnative.dev/guides/choose.md + + + 2026-08-25 + monthly + 0.3 + + + https://pdfnative.dev/guides/self-verify.md + + + 2026-08-25 + monthly + 0.3 + + + https://pdfnative.dev/llms-recipes.txt + + + 2026-08-25 + monthly + 0.4 + diff --git a/llms.txt b/llms.txt index 5701cc7..577e5f5 100644 --- a/llms.txt +++ b/llms.txt @@ -62,6 +62,9 @@ - [agent-brief.md](https://pdfnative.dev/agent-brief.md): A compact briefing for AI agents writing code with pdfnative — the core API, the verified pitfalls that produce wrong code, the surface decision tree, and the self-verification loop. Designed to be pasted into a coding agent's context. - [llms-full.txt](https://pdfnative.dev/llms-full.txt): This index plus the README and the full Markdown source of every guide, in one request. +- [llms-recipes.txt](https://pdfnative.dev/llms-recipes.txt): The executable recipe corpus — standalone TypeScript files importing from `pdfnative`, each executed in CI with its `@expect` assertions verified, so the code you copy is proven to compile and to produce exactly the artefact its header describes. +- [api.json](https://pdfnative.dev/assets/api.json): The public API surface, derived mechanically from the export statements of `src/index.ts` — name, kind, source module, signature and TSDoc summary where extractable. The substitute for the unpublished `index.d.ts`. +- [surfaces.json](https://pdfnative.dev/data/surfaces.json): The capability × surface matrix (library / CLI / MCP / React) behind the [Choosing your surface](https://pdfnative.dev/guides/choose.md) guide. - [llms-index.json](https://pdfnative.dev/llms-index.json): Per-page machine index — HTML and raw-Markdown URL, title, summary, section anchors, and exact byte / approximate token sizes for every artefact, so you can decide what to fetch before spending the tokens. - Every guide serves its raw Markdown at the same URL with `.md` instead of `.html`, declared via `` in each page head. All of these URLs are listed in the sitemap. diff --git a/package.json b/package.json index f088fc3..827cd4d 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "verify:docs": "npx tsx scripts/verify-docs.ts", "docs:llms": "npx tsx scripts/build-llms-full.ts", "docs:guides": "npx tsx scripts/build-guides.ts", + "docs:api": "npx tsx scripts/build-api-json.ts", "fonts:download": "npx tsx scripts/download-fonts.ts", "verify:issue": "node scripts/verify-issue.mjs", "lint": "eslint src/", diff --git a/recipes/arabic-rtl.ts b/recipes/arabic-rtl.ts new file mode 100644 index 0000000..2f96978 --- /dev/null +++ b/recipes/arabic-rtl.ts @@ -0,0 +1,39 @@ +/** + * Arabic text with positional shaping and right-to-left layout. The Noto + * Naskh Arabic data module is registered as a lazy loader, resolved with + * `loadFontData`, and passed through `fontEntries`; the engine embeds a + * subsetted CIDFont (Identity-H) and shapes the letterforms. + * + * @task Render shaped right-to-left Arabic text with an embedded Noto font + * @surface library + * @since 1.3.0 + * @expect pages === 1 + * @expect pdf contains '/FontFile2' + * @expect pdf contains '/Identity-H' + */ +import { buildDocumentPDFBytes, openPdf, registerFont, loadFontData } from 'pdfnative'; +import type { DocumentParams, FontEntry, FontLoader } from 'pdfnative'; + +// The generated data modules predate the full FontData declaration, hence +// the loader cast — the runtime shape is complete. +registerFont('ar', (() => import('pdfnative/fonts/noto-arabic-data.js')) as unknown as FontLoader); + +export async function run(): Promise<{ bytes: Uint8Array; pages: number }> { + const arabic = await loadFontData('ar'); + if (!arabic) throw new Error('Arabic font data failed to load'); + + const fontEntries: FontEntry[] = [{ fontData: arabic, fontRef: '/F3', lang: 'ar' }]; + const params: DocumentParams = { + title: 'Arabic shaping', + blocks: [ + { type: 'heading', text: 'Positional forms and ligatures', level: 1 }, + { type: 'paragraph', text: 'السلام عليكم ورحمة الله وبركاته.' }, + { type: 'paragraph', text: 'النص العربي يُعرض من اليمين إلى اليسار.' }, + ], + footerText: 'Arabic recipe', + fontEntries, + }; + + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + return { bytes, pages: openPdf(bytes).pageCount }; +} diff --git a/recipes/charts-v2.ts b/recipes/charts-v2.ts new file mode 100644 index 0000000..777b9dc --- /dev/null +++ b/recipes/charts-v2.ts @@ -0,0 +1,49 @@ +/** + * Native vector charts (charts v2): a stacked bar chart of quarterly + * revenue and a scatter plot on a linear x-axis, one per page. Charts are + * pure PDF path operators — no rasterisation, no dependencies. + * + * @task Render stacked-bar and scatter charts as native vector graphics + * @surface library + * @since 1.7.0 + * @expect pages === 2 + */ +import { buildDocumentPDFBytes, openPdf } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Quarterly figures', + blocks: [ + { + type: 'chart', + chartType: 'stackedBar', + title: 'Revenue by region', + categories: ['Q1', 'Q2', 'Q3', 'Q4'], + series: [ + { label: 'EMEA', values: [120, 135, 128, 150] }, + { label: 'Americas', values: [90, 105, 118, 122] }, + { label: 'APAC', values: [45, 52, 61, 70] }, + ], + height: 220, + legend: 'bottom', + }, + { type: 'pageBreak' }, + { + type: 'chart', + chartType: 'scatter', + title: 'Latency against payload size', + xAxis: { type: 'linear', grid: true }, + series: [ + { label: 'Samples', values: [12, 18, 25, 31, 44], xValues: [10, 25, 50, 75, 100] }, + ], + height: 220, + dataLabels: { decimals: 0, suffix: ' ms' }, + }, + ], + footerText: 'Quarterly figures', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + return { bytes, pages: openPdf(bytes).pageCount }; +} diff --git a/recipes/form-fill-roundtrip.ts b/recipes/form-fill-roundtrip.ts new file mode 100644 index 0000000..c6bda0d --- /dev/null +++ b/recipes/form-fill-roundtrip.ts @@ -0,0 +1,45 @@ +/** + * AcroForm round trip: author a form, enumerate its fields, fill a value, + * and read it back. A second fill with `flatten: true` stamps the + * appearance into the page content, where plain text extraction finds it. + * + * @task Author an AcroForm, fill it, and read the value back + * @surface library + * @since 1.6.0 + * @expect field 'fullName' value === 'Ada Lovelace' + * @expect text of page 0 of the flattened document contains 'Ada Lovelace' + */ +import { buildDocumentPDFBytes, readFormFields, fillForm, extractText } from 'pdfnative'; +import type { DocumentParams, ParsedFormField } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Application', + blocks: [ + { type: 'heading', text: 'Application form', level: 1 }, + { type: 'formField', fieldType: 'text', name: 'fullName', label: 'Full name' }, + { type: 'formField', fieldType: 'checkbox', name: 'agree', label: 'I agree to the terms' }, + { type: 'formField', fieldType: 'dropdown', name: 'country', label: 'Country', options: ['France', 'Germany', 'Spain'] }, + ], + footerText: 'Application', +}; + +export async function run(): Promise<{ + bytes: Uint8Array; + fields: readonly ParsedFormField[]; + filledValue: string | readonly string[] | boolean | null; + flattenedText: string; +}> { + const blank = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + + // Enumerate the authored fields, then fill by fully-qualified name. + const values = { fullName: 'Ada Lovelace', agree: true, country: 'Germany' } as const; + const filled = fillForm(blank, values); + const fields = readFormFields(filled); + const filledValue = fields.find(f => f.name === 'fullName')?.value ?? null; + + // Flattening replaces the widgets with static page content. + const flattened = fillForm(blank, values, { flatten: true }); + const flattenedText = extractText(flattened, { pages: [0] })[0].text; + + return { bytes: filled, fields, filledValue, flattenedText }; +} diff --git a/recipes/index.json b/recipes/index.json new file mode 100644 index 0000000..f7cd11b --- /dev/null +++ b/recipes/index.json @@ -0,0 +1,145 @@ +[ + { + "file": "arabic-rtl.ts", + "task": "Render shaped right-to-left Arabic text with an embedded Noto font", + "surface": "library", + "since": "1.3.0", + "expects": [ + "pages === 1", + "pdf contains '/FontFile2'", + "pdf contains '/Identity-H'" + ] + }, + { + "file": "charts-v2.ts", + "task": "Render stacked-bar and scatter charts as native vector graphics", + "surface": "library", + "since": "1.7.0", + "expects": [ + "pages === 2" + ] + }, + { + "file": "form-fill-roundtrip.ts", + "task": "Author an AcroForm, fill it, and read the value back", + "surface": "library", + "since": "1.6.0", + "expects": [ + "field 'fullName' value === 'Ada Lovelace'", + "text of page 0 of the flattened document contains 'Ada Lovelace'" + ] + }, + { + "file": "invoice-pdfa2b.ts", + "task": "Build a PDF/A-2b invoice with an itemised line-item table", + "surface": "library", + "since": "1.6.0", + "expects": [ + "pages === 1", + "text of page 0 contains 'Invoice'", + "pdfA claim === 'pdfa2b'" + ] + }, + { + "file": "layout-preview.ts", + "task": "Preview pagination and block geometry without rendering a PDF", + "surface": "library", + "since": "1.5.0", + "expects": [ + "totalPages === 2", + "first block type === 'heading' on page 0 with width > 0" + ] + }, + { + "file": "merge-encrypted.ts", + "task": "Merge two PDFs and AES-encrypt the combined document", + "surface": "library", + "since": "1.6.0", + "expects": [ + "pages === 2", + "encryption.algorithm === 'aes128'" + ] + }, + { + "file": "multipage-table.ts", + "task": "Paginate a long table with the header row repeated on every page", + "surface": "library", + "since": "1.6.0", + "expects": [ + "pages === 2", + "text of page 1 contains 'Description'" + ] + }, + { + "file": "print-bleed.ts", + "task": "Prepare a print-ready page with bleed, TrimBox and printer's marks", + "surface": "library", + "since": "1.7.0", + "expects": [ + "trimBox === [8.5, 8.5, 603.78, 850.39]", + "bleedBox === [0, 0, 612.28, 858.89]" + ] + }, + { + "file": "signed-pades.ts", + "task": "Sign a document with a PAdES B-B (ETSI.CAdES.detached) signature", + "surface": "library", + "since": "1.7.0", + "expects": [ + "signatures.length === 1", + "signatures[0].subFilter === 'ETSI.CAdES.detached'", + "signatures[0].isPlaceholder === false" + ] + }, + { + "file": "streaming-large.ts", + "task": "Stream a 300-block document and match the buffered output byte for byte", + "surface": "library", + "since": "1.3.0", + "expects": [ + "identical === true", + "pages === 8" + ] + }, + { + "file": "text-extraction-rag.ts", + "task": "Extract reading-order text plus positioned runs from a document", + "surface": "library", + "since": "1.6.0", + "expects": [ + "text of page 0 contains 'retrieval'", + "runs.length > 0", + "every run has numeric x, y and fontSize" + ] + }, + { + "file": "toc-outline.ts", + "task": "Generate a linked table of contents plus automatic bookmarks", + "surface": "library", + "since": "1.6.0", + "expects": [ + "pages === 3", + "catalog has /Outlines", + "text of page 0 contains 'Table of Contents'" + ] + }, + { + "file": "update-metadata.ts", + "task": "Retitle an existing PDF via a non-destructive incremental update", + "surface": "library", + "since": "1.7.0", + "expects": [ + "title === 'Quarterly report (revised)'" + ] + }, + { + "file": "watermark.ts", + "task": "Stamp a rotated DRAFT watermark behind the page content", + "surface": "library", + "since": "1.6.0", + "expects": [ + "text of page 0 contains 'Confidential clause'", + "text of page 0 contains 'DRAFT'" + ] + } +] diff --git a/recipes/invoice-pdfa2b.ts b/recipes/invoice-pdfa2b.ts new file mode 100644 index 0000000..20098be --- /dev/null +++ b/recipes/invoice-pdfa2b.ts @@ -0,0 +1,55 @@ +/** + * Invoice as tagged PDF/A-2b — an itemised table with an embedded Latin + * font, so the archival claim survives validation. The creation date is + * pinned to keep the output byte-identical across runs. + * + * @task Build a PDF/A-2b invoice with an itemised line-item table + * @surface library + * @since 1.6.0 + * @expect pages === 1 + * @expect text of page 0 contains 'Invoice' + * @expect pdfA claim === 'pdfa2b' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams, FontData, FontEntry } from 'pdfnative'; +import * as notoSans from 'pdfnative/fonts/noto-sans-data.js'; + +// PDF/A requires every rendered glyph to come from an embedded font +// (ISO 19005 §6.2.11.4.1); the bundled Noto Sans data module covers Latin. +const latinFont: FontEntry = { + fontRef: '/F1', + fontData: notoSans as unknown as FontData, +}; + +const params: DocumentParams = { + title: 'Invoice INV-2026-0042', + blocks: [ + { type: 'paragraph', text: 'Billed to: Acme Widgets Ltd, 4 Foundry Lane, Sheffield' }, + { type: 'spacer', height: 8 }, + { + type: 'table', + caption: 'Line items', + headers: ['Item', 'Quantity', 'Unit price', 'Total'], + rows: [ + { cells: ['Consultancy (June)', '3 days', '650.00', '1,950.00'], type: 'debit', pointed: false }, + { cells: ['Managed hosting', '1 month', '120.00', '120.00'], type: 'debit', pointed: false }, + { cells: ['Support retainer', '1 month', '250.00', '250.00'], type: 'debit', pointed: false }, + ], + }, + { type: 'spacer', height: 8 }, + { type: 'paragraph', text: 'Total due: 2,320.00 GBP within 30 days.', align: 'right' }, + ], + footerText: 'Registered in England no. 01234567', + fontEntries: [latinFont], + metadata: { author: 'Accounts', subject: 'Invoice INV-2026-0042' }, +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; text: string }> { + const bytes = buildDocumentPDFBytes(params, { + tagged: 'pdfa2b', + creationDate: new Date('2026-08-25T00:00:00Z'), + }); + const pages = openPdf(bytes).pageCount; + const text = extractText(bytes, { pages: [0] })[0].text; + return { bytes, pages, text }; +} diff --git a/recipes/layout-preview.ts b/recipes/layout-preview.ts new file mode 100644 index 0000000..6009d90 --- /dev/null +++ b/recipes/layout-preview.ts @@ -0,0 +1,31 @@ +/** + * Dry-run layout inspection: `inspectDocumentLayout` reports how the + * builder will paginate and place each block — page index, x, top, width, + * height — without rendering a PDF. Useful for layout assertions and + * tooling. + * + * @task Preview pagination and block geometry without rendering a PDF + * @surface library + * @since 1.5.0 + * @expect totalPages === 2 + * @expect first block type === 'heading' on page 0 with width > 0 + */ +import { inspectDocumentLayout } from 'pdfnative'; +import type { DocumentParams, LayoutInspection } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Layout probe', + blocks: [ + { type: 'heading', text: 'Section one', level: 1 }, + { type: 'paragraph', text: 'A paragraph measured, not rendered.' }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Section two', level: 1 }, + { type: 'paragraph', text: 'Placed on the second page.' }, + ], + footerText: 'Layout probe', +}; + +export async function run(): Promise<{ inspection: LayoutInspection }> { + const inspection = inspectDocumentLayout(params); + return { inspection }; +} diff --git a/recipes/merge-encrypted.ts b/recipes/merge-encrypted.ts new file mode 100644 index 0000000..f6143df --- /dev/null +++ b/recipes/merge-encrypted.ts @@ -0,0 +1,41 @@ +/** + * Merge two documents and encrypt the result in the same call. + * `MergeOptions.encrypt` re-protects the assembled document (AES-128 + * here); the reader then opens it with the user password and reports the + * scheme. Encryption uses random salts, so the bytes differ per run while + * the structure stays identical. + * + * @task Merge two PDFs and AES-encrypt the combined document + * @surface library + * @since 1.6.0 + * @expect pages === 2 + * @expect encryption.algorithm === 'aes128' + */ +import { buildDocumentPDFBytes, mergePdfs, openPdf } from 'pdfnative'; +import type { DocumentParams, PdfEncryptionInfo } from 'pdfnative'; + +function chapter(title: string, body: string): DocumentParams { + return { + title, + blocks: [{ type: 'paragraph', text: body }], + footerText: title, + }; +} + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; encryption: PdfEncryptionInfo | null }> { + const created = new Date('2026-08-25T00:00:00Z'); + const first = buildDocumentPDFBytes(chapter('Part one', 'Opening chapter.'), { creationDate: created }); + const second = buildDocumentPDFBytes(chapter('Part two', 'Closing chapter.'), { creationDate: created }); + + const bytes = mergePdfs([first, second], { + encrypt: { + ownerPassword: 'owner-secret', + userPassword: 'reader-secret', + algorithm: 'aes128', + permissions: { print: true, copy: false }, + }, + }); + + const reader = openPdf(bytes, { password: 'reader-secret' }); + return { bytes, pages: reader.pageCount, encryption: reader.encryption }; +} diff --git a/recipes/multipage-table.ts b/recipes/multipage-table.ts new file mode 100644 index 0000000..4e24132 --- /dev/null +++ b/recipes/multipage-table.ts @@ -0,0 +1,39 @@ +/** + * A table long enough to paginate. `repeatHeader` (the default, stated + * here explicitly) re-draws the header row on every continuation page, + * which the extracted text of page 1 confirms. + * + * @task Paginate a long table with the header row repeated on every page + * @surface library + * @since 1.6.0 + * @expect pages === 2 + * @expect text of page 1 contains 'Description' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams, PdfRow } from 'pdfnative'; + +const rows: PdfRow[] = Array.from({ length: 70 }, (_, i) => ({ + cells: [`2026-06-${String((i % 28) + 1).padStart(2, '0')}`, `Ledger entry ${i + 1}`, (100 + i).toFixed(2)], + type: i % 2 === 0 ? 'credit' : 'debit', + pointed: false, +})); + +const params: DocumentParams = { + title: 'June ledger', + blocks: [ + { + type: 'table', + headers: ['Date', 'Description', 'Amount'], + rows, + repeatHeader: true, + }, + ], + footerText: 'June ledger', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; page1Text: string }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const pages = openPdf(bytes).pageCount; + const page1Text = extractText(bytes, { pages: [1] })[0].text; + return { bytes, pages, page1Text }; +} diff --git a/recipes/print-bleed.ts b/recipes/print-bleed.ts new file mode 100644 index 0000000..d93a902 --- /dev/null +++ b/recipes/print-bleed.ts @@ -0,0 +1,44 @@ +/** + * Print production: the page is designed at trim size plus 3 mm bleed on + * every side (8.5 pt), `layout.print.bleed` derives the TrimBox and + * BleedBox, and `marks: true` draws crop and registration marks outside + * the trim area. The boxes are read back from the parsed page. + * + * @task Prepare a print-ready page with bleed, TrimBox and printer's marks + * @surface library + * @since 1.7.0 + * @expect trimBox === [8.5, 8.5, 603.78, 850.39] + * @expect bleedBox === [0, 0, 612.28, 858.89] + */ +import { buildDocumentPDFBytes, openPdf, dictGetArray } from 'pdfnative'; + +const BLEED = 8.5; // 3 mm in points +const TRIM_W = 595.28; // A4 trim width +const TRIM_H = 841.89; // A4 trim height + +export async function run(): Promise<{ + bytes: Uint8Array; + trimBox: readonly number[]; + bleedBox: readonly number[]; +}> { + const bytes = buildDocumentPDFBytes( + { + title: 'Poster', + blocks: [{ type: 'paragraph', text: 'Background art runs to the page edge; keep copy inside the trim.' }], + footerText: 'Poster', + }, + { + // Page size = trim size + 2 × bleed; backgrounds may run to the edge. + pageWidth: TRIM_W + 2 * BLEED, + pageHeight: TRIM_H + 2 * BLEED, + print: { bleed: BLEED, marks: true }, + creationDate: new Date('2026-08-25T00:00:00Z'), + }, + ); + + const page = openPdf(bytes).getPage(0); + const asNumbers = (name: string): number[] => + (dictGetArray(page, name) ?? []).filter((v): v is number => typeof v === 'number'); + + return { bytes, trimBox: asNumbers('TrimBox'), bleedBox: asNumbers('BleedBox') }; +} diff --git a/recipes/signed-pades.ts b/recipes/signed-pades.ts new file mode 100644 index 0000000..08c2c0e --- /dev/null +++ b/recipes/signed-pades.ts @@ -0,0 +1,57 @@ +/** + * PAdES B-B digital signature. The document is built, an invisible + * signature placeholder is injected (with the CAdES subFilter and the + * descriptive entries baked in — the /Sig dictionary's byte layout is + * frozen at placeholder time), then signed. The caller supplies the + * certificate and RSA key; pdfnative never generates key material. + * + * @task Sign a document with a PAdES B-B (ETSI.CAdES.detached) signature + * @surface library + * @since 1.7.0 + * @expect signatures.length === 1 + * @expect signatures[0].subFilter === 'ETSI.CAdES.detached' + * @expect signatures[0].isPlaceholder === false + */ +import { buildDocumentPDFBytes, addSignaturePlaceholder, signPdfBytes, listSignatures } from 'pdfnative'; +import type { DocumentParams, PdfSignatureInfo, RsaPrivateKey, X509Certificate } from 'pdfnative'; + +/** Key material supplied by the caller (e.g. from a PKCS#12 store). */ +export interface SignerMaterial { + readonly cert: X509Certificate; + readonly key: RsaPrivateKey; + readonly chain?: readonly X509Certificate[]; +} + +const params: DocumentParams = { + title: 'Service agreement', + blocks: [ + { type: 'heading', text: 'Agreement', level: 1 }, + { type: 'paragraph', text: 'This agreement is executed by digital signature.' }, + ], + footerText: 'Service agreement', +}; + +export async function run(signer: SignerMaterial): Promise<{ bytes: Uint8Array; signatures: readonly PdfSignatureInfo[] }> { + const unsigned = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + + const placeheld = addSignaturePlaceholder(unsigned, { + fieldName: 'Author', + metadata: { + subFilter: 'ETSI.CAdES.detached', + reason: 'Approval', + location: 'London', + signingTime: new Date('2026-08-25T00:00:00Z'), + }, + }); + + const bytes = signPdfBytes(placeheld, { + signerCert: signer.cert, + certChain: signer.chain, + rsaKey: signer.key, + algorithm: 'rsa-sha256', + profile: 'pades', + signingTime: new Date('2026-08-25T00:00:00Z'), + }); + + return { bytes, signatures: listSignatures(bytes) }; +} diff --git a/recipes/streaming-large.ts b/recipes/streaming-large.ts new file mode 100644 index 0000000..8fa5c27 --- /dev/null +++ b/recipes/streaming-large.ts @@ -0,0 +1,40 @@ +/** + * True streaming output for a large document: `buildDocumentPDFStreamTrue` + * yields the PDF in chunks whose concatenation is byte-identical to the + * buffered builder's output for the same input (creation date pinned so + * both builds share it). + * + * @task Stream a 300-block document and match the buffered output byte for byte + * @surface library + * @since 1.3.0 + * @expect identical === true + * @expect pages === 8 + */ +import { buildDocumentPDFBytes, buildDocumentPDFStreamTrue, concatChunks, openPdf } from 'pdfnative'; +import type { DocumentParams, DocumentBlock } from 'pdfnative'; + +const blocks: DocumentBlock[] = Array.from({ length: 300 }, (_, i) => ({ + type: 'paragraph' as const, + text: `Row ${i + 1}: measurement recorded and archived.`, +})); + +const params: DocumentParams = { + title: 'Measurement log', + blocks, + footerText: 'Measurement log', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; identical: boolean }> { + const created = new Date('2026-08-25T00:00:00Z'); + + const chunks: Uint8Array[] = []; + for await (const chunk of buildDocumentPDFStreamTrue(params, { creationDate: created }, { chunkSize: 16 * 1024 })) { + chunks.push(chunk); + } + const bytes = concatChunks(chunks); + + const buffered = buildDocumentPDFBytes(params, { creationDate: created }); + const identical = bytes.length === buffered.length && bytes.every((b, i) => b === buffered[i]); + + return { bytes, pages: openPdf(bytes).pageCount, identical }; +} diff --git a/recipes/text-extraction-rag.ts b/recipes/text-extraction-rag.ts new file mode 100644 index 0000000..e0c10c5 --- /dev/null +++ b/recipes/text-extraction-rag.ts @@ -0,0 +1,31 @@ +/** + * Positioned text extraction for indexing pipelines (RAG, search). With + * `includeRuns` each text-showing operation is returned with its + * device-space origin and effective font size, alongside the + * reading-order text per page. + * + * @task Extract reading-order text plus positioned runs from a document + * @surface library + * @since 1.6.0 + * @expect text of page 0 contains 'retrieval' + * @expect runs.length > 0 + * @expect every run has numeric x, y and fontSize + */ +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; +import type { DocumentParams, ExtractedPageText } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Corpus notes', + blocks: [ + { type: 'heading', text: 'Chunking strategy', level: 1 }, + { type: 'paragraph', text: 'Split documents into passages before retrieval; keep headings with their sections.' }, + { type: 'paragraph', text: 'Store the run positions so citations can point back into the page.' }, + ], + footerText: 'Corpus notes', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: readonly ExtractedPageText[] }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const pages = extractText(bytes, { includeRuns: true }); + return { bytes, pages }; +} diff --git a/recipes/toc-outline.ts b/recipes/toc-outline.ts new file mode 100644 index 0000000..4e6b2d4 --- /dev/null +++ b/recipes/toc-outline.ts @@ -0,0 +1,39 @@ +/** + * Table of contents and bookmarks together: a `toc` block renders linked + * entries for every heading, while `outline: 'auto'` derives the viewer's + * bookmark panel from the same headings and adds /Outlines to the catalog. + * + * @task Generate a linked table of contents plus automatic bookmarks + * @surface library + * @since 1.6.0 + * @expect pages === 3 + * @expect catalog has /Outlines + * @expect text of page 0 contains 'Table of Contents' + */ +import { buildDocumentPDFBytes, openPdf, extractText } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Operations handbook', + blocks: [ + { type: 'toc', maxLevel: 2 }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Onboarding', level: 1 }, + { type: 'paragraph', text: 'Accounts, hardware and access requests.' }, + { type: 'heading', text: 'First week', level: 2 }, + { type: 'paragraph', text: 'Pairing schedule and reading list.' }, + { type: 'pageBreak' }, + { type: 'heading', text: 'Incident response', level: 1 }, + { type: 'paragraph', text: 'Escalation ladder and post-incident review.' }, + ], + footerText: 'Operations handbook', + outline: 'auto', +}; + +export async function run(): Promise<{ bytes: Uint8Array; pages: number; hasOutlines: boolean; tocText: string }> { + const bytes = buildDocumentPDFBytes(params, { creationDate: new Date('2026-08-25T00:00:00Z') }); + const reader = openPdf(bytes); + const hasOutlines = reader.getCatalog().get('Outlines') !== undefined; + const tocText = extractText(bytes, { pages: [0] })[0].text; + return { bytes, pages: reader.pageCount, hasOutlines, tocText }; +} diff --git a/recipes/update-metadata.ts b/recipes/update-metadata.ts new file mode 100644 index 0000000..9564390 --- /dev/null +++ b/recipes/update-metadata.ts @@ -0,0 +1,47 @@ +/** + * Incremental metadata update: open an existing document, re-issue its + * /Info dictionary with a new title and a pinned modification date, and + * save. The original revision is preserved byte for byte; the reopened + * document reports the new title. + * + * @task Retitle an existing PDF via a non-destructive incremental update + * @surface library + * @since 1.7.0 + * @expect title === 'Quarterly report (revised)' + */ +import { buildDocumentPDFBytes, openPdf, createModifier } from 'pdfnative'; + +/** /Info strings may be UTF-16BE with a BOM; plain literals pass through. */ +function decodePdfText(raw: unknown): string { + if (typeof raw !== 'string') return ''; + if (raw.length >= 2 && raw.charCodeAt(0) === 0xFE && raw.charCodeAt(1) === 0xFF) { + let out = ''; + for (let i = 2; i + 1 < raw.length; i += 2) { + out += String.fromCharCode((raw.charCodeAt(i) << 8) | raw.charCodeAt(i + 1)); + } + return out; + } + return raw; +} + +export async function run(): Promise<{ bytes: Uint8Array; title: string }> { + const original = buildDocumentPDFBytes( + { + title: 'Quarterly report', + blocks: [{ type: 'paragraph', text: 'Figures under review.' }], + footerText: 'Quarterly report', + }, + { creationDate: new Date('2026-08-25T00:00:00Z') }, + ); + + const modifier = createModifier(openPdf(original)); + modifier.updateMetadata({ + title: 'Quarterly report (revised)', + modDate: new Date('2026-08-25T00:00:00Z'), + }); + const bytes = modifier.save(); + + const info = openPdf(bytes).getInfo(); + const title = decodePdfText(info?.get('Title')); + return { bytes, title }; +} diff --git a/recipes/watermark.ts b/recipes/watermark.ts new file mode 100644 index 0000000..76dd73c --- /dev/null +++ b/recipes/watermark.ts @@ -0,0 +1,34 @@ +/** + * A rotated, semi-transparent text watermark behind the content of every + * page. The watermark never disturbs the body text: extraction still + * returns the paragraph, and the watermark string itself is present too. + * + * @task Stamp a rotated DRAFT watermark behind the page content + * @surface library + * @since 1.6.0 + * @expect text of page 0 contains 'Confidential clause' + * @expect text of page 0 contains 'DRAFT' + */ +import { buildDocumentPDFBytes, extractText } from 'pdfnative'; +import type { DocumentParams } from 'pdfnative'; + +const params: DocumentParams = { + title: 'Draft contract', + blocks: [ + { type: 'heading', text: 'Terms', level: 1 }, + { type: 'paragraph', text: 'Confidential clause: neither party discloses the commercial terms.' }, + ], + footerText: 'Draft contract', +}; + +export async function run(): Promise<{ bytes: Uint8Array; text: string }> { + const bytes = buildDocumentPDFBytes(params, { + watermark: { + text: { text: 'DRAFT', fontSize: 60, opacity: 0.15, angle: -45 }, + position: 'background', + }, + creationDate: new Date('2026-08-25T00:00:00Z'), + }); + const text = extractText(bytes, { pages: [0] })[0].text; + return { bytes, text }; +} diff --git a/scripts/build-api-json.ts b/scripts/build-api-json.ts new file mode 100644 index 0000000..2a287aa --- /dev/null +++ b/scripts/build-api-json.ts @@ -0,0 +1,141 @@ +#!/usr/bin/env tsx +/** + * pdfnative — api.json generator + * ============================== + * Emits `docs/assets/api.json`: the public API surface of the engine, derived + * mechanically from the export statements of `src/index.ts` — name, kind + * (value or type), source module, and, when extractable, the declaration's + * signature line and the first sentence of its TSDoc. + * + * Why: `dist/index.d.ts` is gitignored and never served, so an agent that has + * not run `npm install` has no source of truth for what pdfnative exports or + * what a function's shape is — the root condition for hallucinated APIs. This + * file is committed, served at https://pdfnative.dev/assets/api.json, listed + * in llms.txt, and policed by the `api-json-sync` rule of verify-docs (same + * pattern as llms-full.txt): regenerate with `npm run docs:api`. + * + * Honesty rule: fields that cannot be extracted mechanically are `null`, + * never guessed. The signature text is the declaration as written in source + * (first line, normalised whitespace), not a reconstruction. + */ + +import { readFileSync, writeFileSync, existsSync } from 'node:fs'; +import { join, resolve, dirname } from 'node:path'; + +function lf(text: string): string { + return text.replace(/\r\n/g, '\n'); +} + +interface ApiEntry { + name: string; + kind: 'value' | 'type'; + module: string; + signature: string | null; + summary: string | null; +} + +/** First sentence of the JSDoc block immediately above `index` in `text`. */ +function docSummaryAbove(text: string, index: number): string | null { + const before = text.slice(0, index); + const m = before.match(/\/\*\*([\s\S]*?)\*\/\s*$/); + if (!m) return null; + const body = m[1] + .split('\n') + .map((l) => l.replace(/^\s*\*\s?/, '').trim()) + .filter((l) => l && !l.startsWith('@')) + .join(' '); + const sentence = body.match(/^(.*?[.!?])(\s|$)/); + const summary = (sentence ? sentence[1] : body).replace(/\{@link\s+([^}|]+)(?:\|[^}]*)?\}/g, '$1').trim(); + return summary.length > 0 ? summary.slice(0, 240) : null; +} + +/** Declaration line for `name` in `text`, normalised, or null. */ +function declarationOf(text: string, name: string): { signature: string | null; summary: string | null } { + const re = new RegExp( + `export\\s+(?:async\\s+)?(?:function\\*?|const|let|class|type|interface|enum)\\s+${name}\\b[^\\n]*`, + ); + const m = re.exec(text); + if (!m) return { signature: null, summary: null }; + let sig = m[0]; + // Functions: extend to the closing paren of the parameter list (may span lines). + if (/export\s+(?:async\s+)?function/.test(sig) && !/\)/.test(sig)) { + const rest = text.slice(m.index + m[0].length); + const upTo = rest.slice(0, rest.indexOf(')') + 1 + (rest.slice(rest.indexOf(')') + 1).match(/^[^\n{;]*/)?.[0].length ?? 0)); + sig += upTo; + } + sig = sig.replace(/\s+/g, ' ').replace(/\s*\{\s*$/, '').trim(); + return { signature: sig.length > 300 ? sig.slice(0, 297) + '…' : sig, summary: docSummaryAbove(text, m.index) }; +} + +/** Build the api.json content for the repo rooted at `root`. */ +export function buildApiJson(root: string): string { + const indexPath = join(root, 'src', 'index.ts'); + const index = lf(readFileSync(indexPath, 'utf8')); + const entries: ApiEntry[] = []; + const seen = new Set(); + const moduleCache = new Map(); + + const readModule = (spec: string): string => { + if (moduleCache.has(spec)) return moduleCache.get(spec)!; + const path = join(dirname(indexPath), spec.replace(/\.js$/, '.ts')); + const text = existsSync(path) ? lf(readFileSync(path, 'utf8')) : ''; + moduleCache.set(spec, text); + return text; + }; + + // `export { a, b as c } from './x.js'` and `export type { … } from './x.js'` + const RE_BLOCK = /export\s*(type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g; + let m: RegExpExecArray | null; + while ((m = RE_BLOCK.exec(index)) !== null) { + const blockIsType = Boolean(m[1]); + const spec = m[3]; + const moduleText = readModule(spec); + for (const raw of m[2].split(',')) { + const piece = raw.trim(); + if (!piece) continue; + const isType = blockIsType || piece.startsWith('type '); + const name = piece.replace(/^type\s+/, '').split(/\s+as\s+/).pop()!.trim(); + if (!name || seen.has(name)) continue; + seen.add(name); + const { signature, summary } = declarationOf(moduleText, name); + entries.push({ + name, + kind: isType ? 'type' : 'value', + module: spec.replace(/^\.\//, 'src/').replace(/\.js$/, '.ts'), + signature, + summary, + }); + } + } + + // Direct declarations in index.ts itself (if any). + for (const d of index.matchAll(/export\s+(?:async\s+)?(function\*?|const|let|class|type|interface|enum)\s+([A-Za-z_]\w*)/g)) { + const name = d[2]; + if (seen.has(name)) continue; + seen.add(name); + const isType = d[1] === 'type' || d[1] === 'interface'; + const { signature, summary } = declarationOf(index, name); + entries.push({ name, kind: isType ? 'type' : 'value', module: 'src/index.ts', signature, summary }); + } + + entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + + const out = { + $comment: + 'Public API surface of pdfnative, derived mechanically from the export statements of src/index.ts by scripts/build-api-json.ts and policed by the api-json-sync rule of scripts/verify-docs.ts. Fields that cannot be extracted mechanically are null, never guessed. Regenerate with `npm run docs:api`.', + package: 'pdfnative', + source: 'src/index.ts', + exportCount: entries.length, + exports: entries, + }; + return JSON.stringify(out, null, 2) + '\n'; +} + +const isMain = process.argv[1] && resolve(process.argv[1]).includes('build-api-json'); +if (isMain) { + const root = resolve(import.meta.dirname, '..'); + const out = join(root, 'docs', 'assets', 'api.json'); + writeFileSync(out, buildApiJson(root)); + const count = (JSON.parse(buildApiJson(root)) as { exportCount: number }).exportCount; + console.log(`build-api-json: wrote ${out} (${count} exports)`); +} diff --git a/scripts/build-llms-full.ts b/scripts/build-llms-full.ts index 361696a..e9b9282 100644 --- a/scripts/build-llms-full.ts +++ b/scripts/build-llms-full.ts @@ -55,6 +55,36 @@ export function buildLlmsFull(root: string): string { return parts.join('') + '\n'; } +// ── llms-recipes.txt ──────────────────────────────────────────────── + +/** + * Concatenation of the executable recipe corpus (`recipes/*.ts`) — every file + * is CI-executed with its `@expect` assertions verified, so an agent ingesting + * this artefact copies code that is proven to compile and produce exactly the + * artefact its header describes. The cheapest high-signal fetch for a coding + * agent. + */ +export function buildLlmsRecipes(root: string): string { + const dir = join(root, 'recipes'); + const parts: string[] = [ + '# pdfnative — executable recipes', + '', + '> Every file below lives in the repository as `recipes/.ts`, imports only', + "> from 'pdfnative', and is executed in CI with each `@expect` assertion checked", + '> (tests/docs/recipes.test.ts). The machine index is recipes/index.json.', + ]; + const files = existsSync(dir) + ? readdirSync(dir).filter((f) => f.endsWith('.ts')).sort() + : []; + for (const name of files) { + parts.push(`\n---\n\n`); + parts.push('```ts'); + parts.push(lf(readFileSync(join(dir, name), 'utf8')).trimEnd()); + parts.push('```'); + } + return parts.join('\n') + '\n'; +} + // ── llms-index.json ───────────────────────────────────────────────── /** Same GitHub-style slugger as scripts/build-guides.ts heading ids. */ @@ -127,6 +157,7 @@ export function buildLlmsIndex(root: string): string { [`${site}/llms.txt`, join(root, 'llms.txt'), 'Documentation index (llmstxt.org convention).'], [`${site}/agent-brief.md`, join(root, 'docs', 'agent-brief.md'), 'Compact paste-into-context briefing for coding agents: core API, verified pitfalls, surface decision tree, self-verification loop.'], [`${site}/llms-full.txt`, join(root, 'docs', 'llms-full.txt'), 'Full corpus: index + README + every guide, one request.'], + [`${site}/llms-recipes.txt`, join(root, 'docs', 'llms-recipes.txt'), 'Executable recipes: CI-verified, copy-ready code for the most common tasks.'], ['https://github.com/Nizoka/pdfnative/blob/main/README.md', join(root, 'README.md'), 'Complete feature and API reference (also embedded in llms-full.txt).'], ]; for (const [url, path, description] of artefactSources) { @@ -151,7 +182,10 @@ if (isMain) { const out = join(root, 'docs', 'llms-full.txt'); writeFileSync(out, buildLlmsFull(root)); console.log(`build-llms-full: wrote ${out}`); - // Index second: it reports llms-full.txt's on-disk size. + const rec = join(root, 'docs', 'llms-recipes.txt'); + writeFileSync(rec, buildLlmsRecipes(root)); + console.log(`build-llms-full: wrote ${rec}`); + // Index last: it reports the other artefacts' on-disk sizes. const idx = join(root, 'docs', 'llms-index.json'); writeFileSync(idx, buildLlmsIndex(root)); console.log(`build-llms-full: wrote ${idx}`); diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index ef68d59..3b12da3 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -211,6 +211,9 @@ const actualDerived: Record = { ).length : 0, learnSteps: manifest.learnPath.length, + recipes: existsSync(join(ROOT, 'recipes')) + ? readdirSync(join(ROOT, 'recipes')).filter((f) => f.endsWith('.ts')).length + : 0, }; // samplePdfs only counts when the samples have actually been generated; @@ -411,6 +414,98 @@ if (SRC_FILES.length > 0) { fail(rel(file), lineOf(text, m.index), 'api-exists', `"${id}" is not declared anywhere in src/`); } } + + // Generalisation of the scan above: ANY identifier written call-shaped at + // the very start of an inline code span — `name(…)` in Markdown, or + // name(…) in rendered/authored HTML — must exist somewhere in + // src/. "Exists" is the same doctrine as the build*/stream* scan: docs may + // name internal helpers and interface methods, so the reference set is + // every call- or declaration-shaped identifier in the sources, not just + // the exports. Anchoring on the span opener keeps prose and member calls + // (`mod.updateMetadata(…)`) out of scope; fenced code blocks never start + // an identifier with a backtick, so they stay out too. Platform globals + // that docs legitimately call in spans are allow-listed here. + const callable = new Set(exported); + const CALLABLE_FILES = [ + ...SRC_FILES, + // Repo tooling the agent docs legitimately reference (verify-issue.mjs & co). + ...walk(join(ROOT, 'scripts'), (p) => p.endsWith('.ts') || p.endsWith('.mjs')), + ]; + for (const srcFile of CALLABLE_FILES) { + for (const m of read(srcFile).matchAll(/\b([A-Za-z_]\w*)\s*\(/g)) { + callable.add(m[1]); + } + } + const SPAN_GLOBALS = new Set([ + // Platform / runtime globals docs legitimately call in spans. + 'fetch', 'import', 'require', 'atob', 'btoa', 'structuredClone', 'run', + // Test-framework globals (vitest) named by the testing instructions. + 'describe', 'it', 'test', 'expect', 'bench', 'vi', + 'beforeAll', 'beforeEach', 'afterAll', 'afterEach', + // Conventional-commit prefixes written call-shaped (`feat(scope):`). + 'feat', 'fix', 'chore', 'refactor', 'perf', 'style', 'ci', + // Companion-package exports named in ecosystem prose outside the + // dedicated companion guides (their drift is a companion-repo concern, + // caught by the weekly --online npm-drift run, not this offline scan). + 'validateGovernanceDraft', // pdfnative-cli + 'docSpecSchema', // pdfnative-react (README ecosystem row) + 'lintDocument', // pdfnative-react (cited by the PDF/A guide) + ]); + // Guides DEDICATED to a companion package document that package's API + // throughout — checking those names against this repo's src/ would be a + // category error, so they are out of this scan's scope entirely. + const SPAN_SKIP = /docs[\\/](?:guides|playgrounds)[\\/](react|cli|mcp)\.(md|html)$/; + // Companion-package exports are legitimate wherever the docs corpus shows + // them imported from that package (`import { usePdf } from + // 'pdfnative-react'`) — the import example is itself the documentation + // that the name exists there. + const companionImported = new Set(); + for (const file of DOC_FILES) { + for (const m of read(file).matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]pdfnative-[\w-]+['"]/g)) { + for (const raw of m[1].split(',')) { + const name = raw.replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim(); + if (name) companionImported.add(name); + } + } + } + // In Markdown a backtick opens a code span; in HTML a backtick is almost + // always a JS template literal inside an inline + + + + + + + +
      +

      Home  ›  Playgrounds  ›  Inspect & verify

      + +
      +

      Inspect & verify — drop a PDF, nothing leaves your machine

      +

      Drop any PDF below and read a full report — document metadata, extracted text, PDF/UA structure, a signature inventory, and form fields — produced by the same pdfnative reader APIs that run in Node, Deno and Bun. Open your browser's Network panel before dropping a file: the only requests this page ever makes happen while it loads — its stylesheets, the Prism highlighter, and the pdfnative engine from the CDN — before you drop anything. The file is read with the standard FileReader API and never leaves this tab. Nothing is stored either: reload the page and the report is gone.

      +

      In the spirit of the responsibility page, every card below links the source file in the repository that produced it — the report is only as trustworthy as the code you can read.

      + +

      Loading pdfnative from esm.sh… this happens once, at page load.

      + +
      +

      Drop a PDF anywhere in this box — or pick one:

      +
      + + + + +
      +

      The sample is a small two-page tagged document with form fields, built in this tab by buildDocumentPDFBytes() — so the page can demonstrate itself without needing a file of yours.

      +
      + + + + + + + +
      +
      + + + + + + + + diff --git a/docs/playgrounds/mcp.html b/docs/playgrounds/mcp.html index b869111..d0341c1 100644 --- a/docs/playgrounds/mcp.html +++ b/docs/playgrounds/mcp.html @@ -117,6 +117,7 @@ Extreme scripts All 22 scripts PDF Toolkit + Inspect & verify Charts Authoring+ Scale diff --git a/docs/playgrounds/react.html b/docs/playgrounds/react.html index 0bd5e18..6c69403 100644 --- a/docs/playgrounds/react.html +++ b/docs/playgrounds/react.html @@ -100,6 +100,7 @@ Extreme scripts All 22 scripts PDF Toolkit + Inspect & verify Charts Authoring+ Scale diff --git a/docs/playgrounds/scale.html b/docs/playgrounds/scale.html index 9a0b642..12cac44 100644 --- a/docs/playgrounds/scale.html +++ b/docs/playgrounds/scale.html @@ -131,6 +131,7 @@ Extreme scripts All 22 scripts PDF Toolkit + Inspect & verify Charts Authoring+ Scale diff --git a/docs/playgrounds/toolkit.html b/docs/playgrounds/toolkit.html index 497d12e..a0d21d0 100644 --- a/docs/playgrounds/toolkit.html +++ b/docs/playgrounds/toolkit.html @@ -7,10 +7,10 @@ - + - + @@ -75,6 +75,7 @@ Extreme scripts All 22 scripts PDF Toolkit + Inspect & verify Charts Authoring+ Scale diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 4f6bcd8..3086552 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -702,5 +702,13 @@ monthly 0.4 + + https://pdfnative.dev/playgrounds/inspect.html + + + 2026-08-25 + monthly + 0.6 + diff --git a/docs/style.css b/docs/style.css index 61e5408..ecbc774 100644 --- a/docs/style.css +++ b/docs/style.css @@ -820,3 +820,13 @@ textarea:focus-visible, .pn-preview-pane { min-height: 320px; } .pn-preview-frame { min-height: 320px; } } + +.demo-mode { display: inline-flex; gap: 4px; margin-right: 10px; border: 1px solid var(--c-border); border-radius: 6px; padding: 2px; } +.demo-mode .tab-btn { padding: 4px 12px; font-size: 13px; border: none; background: transparent; border-radius: 4px; cursor: pointer; color: var(--c-text-dim); font-weight: 600; } +.demo-mode .tab-btn.active { background: var(--c-primary); color: var(--c-primary-fg); } + +.bench-live { margin-top: 18px; } +.bench-live-grid { margin-top: 14px; display: grid; gap: 10px; } +.bench-live-label { font-size: 13px; color: var(--c-text-dim); } +.bench-live-value { font-size: 13px; font-weight: 600; color: var(--c-text); font-variant-numeric: tabular-nums; } +.bench-live-bar { background: var(--c-success, #16a34a); } diff --git a/scripts/build-guides.ts b/scripts/build-guides.ts index e2f611f..126e73a 100644 --- a/scripts/build-guides.ts +++ b/scripts/build-guides.ts @@ -57,11 +57,14 @@ function stripTags(html: string): string { } function slugify(text: string): string { + // GitHub convention: strip punctuation, then EVERY whitespace character + // becomes its own hyphen ("Flag & ZWJ" → "flag--zwj", not "flag-zwj") — + // existing deep links across the guides were written against that shape. return decodeEntities(stripTags(text)) .trim() .toLowerCase() .replace(/[^\p{L}\p{N}\s_-]/gu, '') - .replace(/\s+/g, '-'); + .replace(/\s/g, '-'); } /** diff --git a/scripts/build-llms-full.ts b/scripts/build-llms-full.ts index e9b9282..5ddf06e 100644 --- a/scripts/build-llms-full.ts +++ b/scripts/build-llms-full.ts @@ -94,7 +94,7 @@ function slugify(text: string): string { .trim() .toLowerCase() .replace(/[^\p{L}\p{N}\s_-]/gu, '') - .replace(/\s+/g, '-'); + .replace(/\s/g, '-'); } function approxTokens(bytes: number): number { diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index 3b12da3..80edc06 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -24,7 +24,7 @@ */ import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; -import { join, relative, resolve, dirname, posix } from 'node:path'; +import { join, relative, resolve, dirname, posix, sep } from 'node:path'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; @@ -1034,6 +1034,55 @@ if (!existsSync(LLMS_RECIPES)) { fail('docs/llms-recipes.txt', 1, 'llms-sync', 'stale — regenerate with `npm run docs:llms`'); } +// ── Rule: anchor-parity ───────────────────────────────────────────── + +/** + * `internal-links` deliberately strips `#fragments` — so a deep link to a + * renamed section rots invisibly, which matters twice as much now that AI + * answers cite section-level URLs. With every guide pre-rendered, anchors are + * plain `id="…"` attributes in committed HTML: every internal link that + * carries a fragment must point at an id that exists in its target page. + * Links written against a `.md` target are checked against the paired `.html` + * (that is where the pre-rendered ids live). + */ +{ + const anchorCache = new Map | null>(); + const anchorsOf = (absPath: string): Set | null => { + if (anchorCache.has(absPath)) return anchorCache.get(absPath)!; + if (!existsSync(absPath) || !absPath.endsWith('.html')) { + anchorCache.set(absPath, null); + return null; + } + const ids = new Set(); + for (const m of read(absPath).matchAll(/\bid=["']([^"']+)["']/g)) ids.add(m[1]); + anchorCache.set(absPath, ids); + return ids; + }; + const LINK_WITH_FRAG = /(?:\]\(|href=["'])([^)"'#\s]*)#([\p{L}][\p{L}\p{N}_-]*)/gu; + for (const file of DOC_FILES) { + if (!file.endsWith('.md') && !file.endsWith('.html')) continue; + const text = read(file); + const lines = text.split(/\r?\n/); + LINK_WITH_FRAG.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = LINK_WITH_FRAG.exec(text)) !== null) { + let target = m[1]; + const frag = m[2]; + if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue; // external / mailto + if (target === '') target = rel(file).split('/').pop()!; // same-page link + let abs = resolve(dirname(file), target); + if (abs.endsWith(sep)) abs = join(abs, 'index.html'); + if (abs.endsWith('.md')) abs = abs.slice(0, -3) + '.html'; + const ids = anchorsOf(abs); + if (ids === null) continue; // non-HTML target or missing file: internal-links' turf + if (ids.has(frag)) continue; + const line = lineOf(text, m.index); + if (isSuppressed(lines, line, 'anchor-parity')) continue; + fail(rel(file), line, 'anchor-parity', `link fragment "#${frag}" has no matching id in ${rel(abs)}`); + } + } +} + // ── Rule: llms-index-sync ─────────────────────────────────────────── /** @@ -1198,6 +1247,7 @@ const OFFLINE_RULES = [ 'contrast', 'llms-sync', 'llms-index-sync', + 'anchor-parity', 'guide-render-sync', 'api-json-sync', 'playground-syntax', diff --git a/tests/docs/verify-docs.test.ts b/tests/docs/verify-docs.test.ts index 0723980..f623544 100644 --- a/tests/docs/verify-docs.test.ts +++ b/tests/docs/verify-docs.test.ts @@ -139,6 +139,16 @@ describe('verify-docs', () => { }); }, 120_000); + it('anchor-parity catches a deep link to a renamed section', () => { + withSandbox((dir) => { + patch(dir, 'docs/guides/streaming.md', '#streaming-merge--split', '#streaming-merge--split-renamed'); + const run = runVerifier(dir); + expect(run.output).toContain('anchor-parity'); + expect(run.output).toContain('streaming-merge--split-renamed'); + expect(run.status).toBe(1); + }); + }, 120_000); + it('api-json-sync catches a stale API surface', () => { withSandbox((dir) => { patch(dir, 'docs/assets/api.json', '"package": "pdfnative"', '"package": "perturbed"'); From c15354330a14ccabf264d3028c8318d1efceea07 Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:11:20 +0200 Subject: [PATCH 10/15] docs(site): honest comparison quiz and the engine diagnostic registry Above the "How It Compares" table, a small quiz derives its verdicts from the table itself: tick what your project needs and the matching rows highlight while a factual conclusion appears - including the cases where another library is the better fit ("if reading and modifying existing PDFs is your whole need, pdf-lib is a solid, widely used choice"; "pdfkit, jsPDF and pdfmake also offer AES"). A comparison that sometimes recommends the competitor is the only kind that is believed - the direct translation of responsibility.html''s doctrine into an interactive component. Rows are tagged data-cap; no data is duplicated outside the table the caption already sources and dates. docs/data/errors.json is the served registry of the engine''s diagnostic codes (the three PDFA_* conformance guards from pdf-diagnostics.ts, with severity, trigger, ISO clause, remedy, sink semantics), and the new error-parity rule (22 rules total) checks it both ways: every PDFA_* token in the docs must exist in the registry AND in src/, and every registry entry must still be emitted by src/ - a registry entry for a ghost code would teach agents an API that no longer exists. The CLI''s E_* and the MCP codes stay in their own repositories by design, noted in the registry''s $comment. Perturbation test added; docs suites 39/39; served-content smoke check: a guide fetched without JavaScript carries its full article, TechArticle JSON-LD and heading anchors. --- .github/copilot-instructions.md | 2 +- CONTRIBUTING.md | 2 +- docs/app.js | 39 +++++++++++++++++++++++++++++++++ docs/data/errors.json | 39 +++++++++++++++++++++++++++++++++ docs/index.html | 26 ++++++++++++++++------ docs/style.css | 9 ++++++++ scripts/verify-docs.ts | 36 ++++++++++++++++++++++++++++++ tests/docs/verify-docs.test.ts | 10 +++++++++ 8 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 docs/data/errors.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2b5cde8..2ea1fe2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -113,7 +113,7 @@ npm run typecheck:scripts # tsc --project tsconfig.scripts.json --noEmit npm run typecheck:all # typecheck src/ + tests/ + scripts/ npm run lint # eslint src/ (ESLint 9 + typescript-eslint strict) npm run validate:pdfa # veraPDF over every PDF/A-claiming sample (auto-detected; coverage canary vs declared.pdfaSamples; skips with exit 0 when veraPDF is absent, blocking in CI) -npm run verify:docs # 21 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth) +npm run verify:docs # 22 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth) ``` - Build tool: **tsup** (dual ESM/CJS, tree-shakeable, sourcemaps) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b16d961..9d9cb2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ npm run test:watch # vitest (watch mode) npm run test:coverage # vitest with v8 coverage (95%+ stmts measured at the v1.6.0 release; CI enforces ≥88%) npm run test:generate # Generate 242 sample PDFs → test-output/ npm run validate:pdfa # veraPDF validation of every PDF/A-claiming sample (see below) -npm run verify:docs # 21 offline rules over docs/, playgrounds, README, llms files +npm run verify:docs # 22 offline rules over docs/, playgrounds, README, llms files npm run bench # Performance benchmarks (vitest bench) ``` diff --git a/docs/app.js b/docs/app.js index 3833c68..844c051 100644 --- a/docs/app.js +++ b/docs/app.js @@ -86,6 +86,45 @@ }); }); + // ── Comparison quiz — verdicts derived from the table itself ── + var quiz = document.getElementById('cmp-quiz'); + if (quiz) { + var quizBoxes = quiz.querySelectorAll('input[data-quiz-cap]'); + var verdictEl = document.getElementById('cmp-quiz-verdict'); + var updateQuiz = function () { + var caps = []; + quizBoxes.forEach(function (b) { + if (b.checked) b.getAttribute('data-quiz-cap').split(',').forEach(function (c) { caps.push(c); }); + }); + document.querySelectorAll('.cmp-table tr[data-cap]').forEach(function (tr) { + tr.classList.toggle('cmp-hot', caps.indexOf(tr.getAttribute('data-cap')) !== -1); + }); + if (!caps.length) { verdictEl.hidden = true; return; } + // Facts, straight from the table rows above: which needs are + // pdfnative-only, and which are served elsewhere too. + var only = []; + if (caps.indexOf('bidi') !== -1) only.push('BiDi shaping'); + if (caps.indexOf('pdfa') !== -1) only.push('built-in PDF/A'); + if (caps.indexOf('sign') !== -1) only.push('digital signatures'); + var msg; + if (only.length) { + msg = 'Of the libraries in this table, only pdfnative offers ' + only.join(', ') + + ' built in (highlighted rows). The others can sometimes get there with extra work — a different claim.'; + } else if (caps.indexOf('parse') !== -1 && caps.length === 1) { + msg = 'Both pdf-lib and pdfnative read and modify existing PDFs. If that is your whole need, pdf-lib is a solid, widely used choice; pdfnative adds the generation, extraction and verification stack around it.'; + } else if (caps.indexOf('encrypt') !== -1 && caps.length === 1) { + msg = 'pdfkit, jsPDF and pdfmake also offer AES encryption (pdf-lib does not). pdfnative adds AES-256 write plus RC4/AES read-and-decrypt if you also work with existing files.'; + } else if (caps.indexOf('barcodes') !== -1 && caps.length === 1) { + msg = 'pdfmake offers QR codes; the five-format barcode set (QR, Code 128, EAN-13, Data Matrix, PDF417) is pdfnative-only in this table.'; + } else { + msg = 'Several libraries in the table cover parts of this combination — the highlighted rows show who covers what. pdfnative covers all of the ticked rows in one dependency-free package.'; + } + verdictEl.textContent = msg; + verdictEl.hidden = false; + }; + quizBoxes.forEach(function (b) { b.addEventListener('change', updateQuiz); }); + } + // ── Code tabs ───────────────────────────────────────────── var tabBtns = document.querySelectorAll('.tab-btn'); var tabPanels = document.querySelectorAll('.tab-panel'); diff --git a/docs/data/errors.json b/docs/data/errors.json new file mode 100644 index 0000000..a725d90 --- /dev/null +++ b/docs/data/errors.json @@ -0,0 +1,39 @@ +{ + "$comment": "Machine-readable registry of the diagnostic codes the pdfnative ENGINE emits (src/core/pdf-diagnostics.ts). Meanings and remedies are restated from the source messages; the error-parity rule of scripts/verify-docs.ts checks that every code named in the documentation exists here and in src/, and vice versa. The CLI's E_* codes and the MCP server's tool error codes belong to their own repositories (pdfnative-cli llms.txt / schema manifest, and the MCP guide's error table, checked against the published package by the weekly --online run) and are deliberately not mirrored here.", + "package": "pdfnative", + "verifiedOn": "2026-08-25", + "diagnostics": [ + { + "code": "PDFA_NO_FONT_ENTRIES", + "severity": "warning", + "since": "1.7.0", + "raisedWhen": "A document claims a PDF/A level (layout.tagged) while rendering text through the unembedded standard-14 Helvetica — no fontEntries were supplied.", + "standard": "ISO 19005 §6.2.11.4.1 (every font embedded; veraPDF rejects the file)", + "remedy": "Register an embedded Latin font (e.g. Noto Sans via registerFont('latin', …) + fontEntries) or drop the PDF/A level.", + "guide": "https://pdfnative.dev/guides/pdfa.md" + }, + { + "code": "PDFA_UNEMBEDDED_FORM_FONT", + "severity": "warning", + "since": "1.7.0", + "raisedWhen": "An AcroForm field appears under a PDF/A claim — field appearances render through the unembedded base-14 /Helv font.", + "standard": "ISO 19005 §6.2.11.4.1 (veraPDF rejects the file)", + "remedy": "Drop the PDF/A level for form documents, or flatten the form before claiming conformance.", + "guide": "https://pdfnative.dev/guides/pdfa.md" + }, + { + "code": "PDFA_DEVICE_CMYK_IMAGE", + "severity": "warning", + "since": "1.7.0", + "raisedWhen": "A DeviceCMYK image is embedded under a PDF/A claim whose OutputIntent is sRGB.", + "standard": "ISO 19005-2 §6.2.4.3 (device colour must match the output intent; veraPDF rejects the file)", + "remedy": "Convert the image to RGB or drop the PDF/A level.", + "guide": "https://pdfnative.dev/guides/pdfa.md" + } + ], + "sink": { + "default": "console.warn, deduplicated once per code per build", + "custom": "layout.onDiagnostic receives every diagnostic (no deduplication)", + "strict": "layout.strict: true throws instead (onDiagnostic is then ignored)" + } +} diff --git a/docs/index.html b/docs/index.html index 82610cd..d018c2b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -533,6 +533,18 @@

      React v1.1.0

      How It Compares

      Feature-by-feature comparison with popular PDF libraries.

      +
      +

      What does your project need? Tick what applies — the matching rows light up and the verdict below is derived from the table, including the cases where another library is the better fit.

      +
      + + + + + + +
      + +
      - + @@ -576,7 +588,7 @@

      How It Compares

      - + @@ -584,7 +596,7 @@

      How It Compares

      - + @@ -592,7 +604,7 @@

      How It Compares

      - + @@ -600,7 +612,7 @@

      How It Compares

      - + @@ -608,7 +620,7 @@

      How It Compares

      - + @@ -640,7 +652,7 @@

      How It Compares

      - + diff --git a/docs/style.css b/docs/style.css index ecbc774..0255c73 100644 --- a/docs/style.css +++ b/docs/style.css @@ -830,3 +830,12 @@ textarea:focus-visible, .bench-live-label { font-size: 13px; color: var(--c-text-dim); } .bench-live-value { font-size: 13px; font-weight: 600; color: var(--c-text); font-variant-numeric: tabular-nums; } .bench-live-bar { background: var(--c-success, #16a34a); } + +/* ── Comparison quiz ─────────────────────────────────────────────── */ + +.cmp-quiz { margin: 0 auto 22px; max-width: 860px; padding: 16px 20px; border: 1px solid var(--c-border); border-radius: 10px; background: var(--c-bg-card); } +.cmp-quiz-label { font-size: 14px; color: var(--c-text-dim); margin: 0 0 12px; } +.cmp-quiz-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 8px 18px; } +.cmp-quiz-options label { font-size: 14px; color: var(--c-text); display: flex; gap: 8px; align-items: baseline; cursor: pointer; } +.cmp-quiz-verdict { margin: 14px 0 0; font-size: 14px; color: var(--c-text); border-left: 3px solid var(--c-primary); background: var(--c-surface); padding: 10px 14px; border-radius: 0 8px 8px 0; } +.cmp-table tr.cmp-hot td { background: color-mix(in srgb, var(--c-primary) 10%, transparent); } diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index 80edc06..512eb15 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -1034,6 +1034,41 @@ if (!existsSync(LLMS_RECIPES)) { fail('docs/llms-recipes.txt', 1, 'llms-sync', 'stale — regenerate with `npm run docs:llms`'); } +// ── Rule: error-parity ────────────────────────────────────────────── + +/** + * `docs/data/errors.json` is the served registry of the engine's diagnostic + * codes. Two-way check against reality: every `PDFA_*` token the docs mention + * must exist both in the registry and in src/, and every registry entry must + * exist in src/ — a registry entry for a code the engine no longer emits + * teaches agents a ghost. + */ +{ + const ERRORS_JSON = join(ROOT, 'docs', 'data', 'errors.json'); + if (!existsSync(ERRORS_JSON)) { + fail('docs/data/errors.json', 1, 'error-parity', 'missing — the engine diagnostic registry must be served'); + } else { + const registry = JSON.parse(read(ERRORS_JSON)) as { diagnostics: Array<{ code: string }> }; + const registered = new Set(registry.diagnostics.map((d) => d.code)); + const srcCodes = new Set(); + for (const f of walk(join(ROOT, 'src'), (p) => p.endsWith('.ts'))) { + for (const m of read(f).matchAll(/\bPDFA_[A-Z_]+\b/g)) srcCodes.add(m[0]); + } + for (const code of registered) { + if (!srcCodes.has(code)) { + fail('docs/data/errors.json', 1, 'error-parity', `registry lists "${code}" but src/ never emits it`); + } + } + for (const file of DOC_FILES) { + const text = read(file); + for (const m of text.matchAll(/\bPDFA_[A-Z_]+\b/g)) { + if (registered.has(m[0])) continue; + fail(rel(file), lineOf(text, m.index!), 'error-parity', `diagnostic "${m[0]}" is not in docs/data/errors.json`); + } + } + } +} + // ── Rule: anchor-parity ───────────────────────────────────────────── /** @@ -1247,6 +1282,7 @@ const OFFLINE_RULES = [ 'contrast', 'llms-sync', 'llms-index-sync', + 'error-parity', 'anchor-parity', 'guide-render-sync', 'api-json-sync', diff --git a/tests/docs/verify-docs.test.ts b/tests/docs/verify-docs.test.ts index f623544..7b69aa1 100644 --- a/tests/docs/verify-docs.test.ts +++ b/tests/docs/verify-docs.test.ts @@ -139,6 +139,16 @@ describe('verify-docs', () => { }); }, 120_000); + it('error-parity rejects a diagnostic code the docs invent', () => { + withSandbox((dir) => { + patch(dir, 'docs/guides/pdfa.md', 'PDFA_NO_FONT_ENTRIES', 'PDFA_IMAGINARY_CODE'); + const run = runVerifier(dir); + expect(run.output).toContain('error-parity'); + expect(run.output).toContain('PDFA_IMAGINARY_CODE'); + expect(run.status).toBe(1); + }); + }, 120_000); + it('anchor-parity catches a deep link to a renamed section', () => { withSandbox((dir) => { patch(dir, 'docs/guides/streaming.md', '#streaming-merge--split', '#streaming-merge--split-renamed'); From dde2ea0d5c1bbadd76cb0f0cd0847e44e95675f5 Mon Sep 17 00:00:00 2001 From: Kuzino <129803615+Nizoka@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:21:19 +0200 Subject: [PATCH 11/15] docs(site): fix the three reported UI bugs and the blockers the review found User-reported bugs, root-caused by a three-auditor review: - Live benchmark crashed with "Cannot read properties of undefined (reading ''length'')": makeParams omitted four required PdfParams fields (infoItems/balanceText/countText/footerText - the engine boundary only validates rows/headers). Params completed; errors now land in an aria-live note instead of destroying the button label. The engine-side boundary guard is deferred to an engine PR to keep this branch docs-only. - "Copy link" half-hidden behind the PDF preview - three stacked causes: .demo-actions had no flex-wrap; the demo container was 780px wide (382px per split pane); and [hidden] was silently defeated by author display rules (.btn{display:inline-flex}), so the Copy-as-CLI/MCP buttons were visible in Code mode too. Fixed with a global [hidden]{display:none!important}, flex-wrap, a full-width status line, and an 1120px demo container. - The re-encrypt example previewed an AES-256-protected PDF, cramming the browser viewer''s password prompt into a half-width iframe. The example now declares encryptedOutput: the preview is skipped for an honest note (password: rotated, use Download), such presets never auto-run, and a /Encrypt tail-sniff covers user-authored code. Review blockers, worse than the reported bugs: - The new Code/JSON mode buttons reused .tab-btn, and the homepage tablist binds every .tab-btn on the page - clicking "JSON" emptied the Examples section entirely and arrow keys jumped between the two groups. The tablist is now scoped to #examples and the mode switch is a proper aria-pressed button group (.demo-mode-btn). - docs/assets/api.json attributed each module''s banner comment as the summary of every export (leftmost-match regex): 77 distinct summaries for 445 exports. A tempered regex now binds to the JSDoc immediately above each declaration - 389 summaries, all distinct, verified. - llms-index.json measured artefact sizes with statSync on the working tree - CRLF-inflated on autocrlf Windows clones, making a pristine checkout fail verify:docs. Sizes now come from the LF-normalised content, the bytes GitHub Pages actually serves. Also in this pass: the invoice recipe used the reserved /F1 fontRef its own documentation forbids (now /F3 + lang); the exact test count is re-measured at 2686 tests across 123 files and updated everywhere with the test-count canonical (the old literal validated its own staleness); README''s playground list gains inspect and says ten. --- .github/copilot-instructions.md | 8 +- .github/instructions/testing.instructions.md | 2 +- .github/prompts/quality-gate.prompt.md | 2 +- AGENTS.md | 4 +- CONTRIBUTING.md | 4 +- README.md | 10 +- docs/agent-brief.md | 11 +- docs/app.js | 130 +++- docs/assets/api.json | 778 +++++++++---------- docs/assets/ecosystem.json | 4 +- docs/data/surfaces.json | 6 +- docs/guides/accessibility.html | 2 +- docs/guides/agentic-workflows.html | 2 +- docs/guides/ai-governance.html | 2 +- docs/guides/annotations.html | 2 +- docs/guides/architecture.html | 2 +- docs/guides/charts.html | 2 +- docs/guides/choose.html | 8 +- docs/guides/choose.md | 6 +- docs/guides/cli.html | 2 +- docs/guides/colour-emoji-cli.html | 2 +- docs/guides/colour-emoji.html | 2 +- docs/guides/debugging.html | 2 +- docs/guides/faq.html | 2 +- docs/guides/font-validation.html | 2 +- docs/guides/form-filling.html | 2 +- docs/guides/guide.css | 20 + docs/guides/guide.js | 18 +- docs/guides/ltv.html | 2 +- docs/guides/mcp.html | 2 +- docs/guides/onboarding.html | 2 +- docs/guides/outlines.html | 2 +- docs/guides/pdf-manipulation.html | 2 +- docs/guides/pdfa.html | 2 +- docs/guides/print.html | 2 +- docs/guides/quickstart.html | 2 +- docs/guides/react.html | 2 +- docs/guides/self-verify.html | 8 +- docs/guides/self-verify.md | 6 +- docs/guides/signatures.html | 2 +- docs/guides/streaming.html | 2 +- docs/guides/tables.html | 2 +- docs/guides/text-extraction.html | 2 +- docs/guides/troubleshooting.html | 2 +- docs/guides/viewer-preferences.html | 2 +- docs/index.html | 10 +- docs/llms-full.txt | 24 +- docs/llms-index.json | 24 +- docs/llms-recipes.txt | 4 +- docs/llms.txt | 2 +- docs/playgrounds/inspect.html | 31 +- docs/playgrounds/mcp.html | 6 +- docs/style.css | 144 ++-- llms.txt | 2 +- recipes/invoice-pdfa2b.ts | 4 +- scripts/build-api-json.ts | 7 +- scripts/build-guides.ts | 20 +- scripts/build-llms-full.ts | 8 +- scripts/verify-docs.ts | 4 +- tests/docs/verify-docs.test.ts | 15 +- 60 files changed, 778 insertions(+), 608 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2ea1fe2..66bc4c8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -77,7 +77,7 @@ fonts/ # Pre-built font data modules (.js/.d.ts) — 22 scripts + mat tools/ # CLI tools: build-font-data.cjs (TTF → data module); build-emoji-font (bundled via tsup from scripts/build-emoji-font.ts → dist/tools/, npx pdfnative-build-emoji-font — generates colour-emoji data modules up to the full ~3,600-glyph set) scripts/ # Modular sample PDF generation (48 generators; persian-showcase.ts + print-showcase.ts, signature-ltv.ts + incremental-metadata.ts added in v1.7.0; text-extract-showcase.ts added in v1.6.0; math-symbols.ts + svg-text-labels.ts + layout-debug-overlay.ts + annotations-showcase.ts + font-compiler-demo.ts added in v1.5.0; outline-bookmarks.ts + pdf-manipulation.ts added in v1.4.0; currency-symbols.ts + color-emoji-showcase real-world rewrite added in v1.3.0; signature-placeholder.ts, bidi-embeddings-showcase.ts, document-table-parity.ts, use-lite-showcase.ts added in v1.2.0/v1.3.0). scripts/lib/ holds the shared deterministic emoji-build core (emoji-font-core.ts, curated-emoji.ts, emoji-cli.ts) used by both build-color-emoji-data.ts and the bundled build-emoji-font CLI test-output/extreme/ # Visual regression baselines for extreme scripts (extreme-bidi.pdf, extreme-tamil.pdf, extreme-bengali-devanagari.pdf, extreme-arabic-harakat.pdf, extreme-bidi-isolates.pdf) -tests/ # 2665+ tests (122 files: unit/integration/fuzz/parser/visual) mirroring src/ structure +tests/ # 2686+ tests (123 files: unit/integration/fuzz/parser/visual/docs) mirroring src/ structure bench/ # Performance benchmarks (vitest bench) docs/ # GitHub Pages landing site (pdfnative.dev) — pure HTML/CSS/JS, zero build deps └── playgrounds/ # 9 interactive browser playgrounds (extreme-scripts, all-scripts, toolkit, charts, authoring-plus, scale, cli, mcp, react) @@ -103,7 +103,7 @@ docs/ # GitHub Pages landing site (pdfnative.dev) — pure HTML/CSS/ ```bash npm run build # tsup → dist/ (ESM + CJS + .d.ts) -npm run test # vitest run (2665+ tests, 122 files) +npm run test # vitest run (2686+ tests, 123 files) npm run test:watch # vitest (watch mode) npm run test:coverage # vitest with v8 coverage (thresholds: 88/80/85/90) npm run test:generate # Generate ~242 sample PDFs → test-output/ (incl. extreme/, emoji/, pdfa-latin/, forms/, charts/, parser/ baselines) @@ -120,7 +120,7 @@ npm run verify:docs # 22 offline rules over docs/, playgrounds, README, llms - Test runner: **vitest** (fast, native ESM, watch mode, v8 coverage) - CI: GitHub Actions — lint/typecheck/test/build on Node 22/24 - Publish: GitHub Actions OIDC Trusted Publishing (`npm publish --access public`; provenance is attached automatically via the workflow's `id-token: write`) -- All new code must have tests. 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90. 2665+ tests (122 files) +- All new code must have tests. 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90. 2686+ tests (123 files) ## Conventions @@ -305,7 +305,7 @@ npm run verify:docs # 22 offline rules over docs/, playgrounds, README, llms - **PDF /Info metadata** — Title, Producer (pdfnative), CreationDate in D:YYYYMMDDHHmmss format - **Input validation** — at `buildPDF()` boundary: null/undefined/type checks, 100K row limit - **URL validation** — at `validateURL()`: blocks javascript:, file:, data: schemes -- **Test coverage** — 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90 (statements/branches/functions/lines). 2665+ tests (122 files), 89 fuzz tests across 5 files (including recursion/zip-bomb/xref-chain hardening), dual-mode visual-regression suite, performance benchmarks +- **Test coverage** — 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90 (statements/branches/functions/lines). 2686+ tests (123 files), 89 fuzz tests across 5 files (including recursion/zip-bomb/xref-chain hardening), dual-mode visual-regression suite, performance benchmarks - **NPM provenance** — signed builds via GitHub Actions OIDC - Security: no `eval()`, no `Function()`, no dynamic code execution - No `console.log` in library code (only in tools/ and scripts/). `console.warn` is allowed **only** inside `src/core/pdf-diagnostics.ts` — the single sanctioned sink for conformance diagnostics (silence or redirect via `onDiagnostic`, escalate via `strict`) diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 7197014..a2fa9fb 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -36,7 +36,7 @@ scripts/ ``` ## Current State (maintain these thresholds) -- **2665+ tests** across 122 test files + 1 benchmark file +- **2686+ tests** across 123 test files + 1 benchmark file - Statements: ~95% (threshold: 88%) - Branches: ~88% (threshold: 80%) - Functions: ~98% (threshold: 85%) diff --git a/.github/prompts/quality-gate.prompt.md b/.github/prompts/quality-gate.prompt.md index a2db685..b12300c 100644 --- a/.github/prompts/quality-gate.prompt.md +++ b/.github/prompts/quality-gate.prompt.md @@ -10,7 +10,7 @@ Run the full pdfnative quality gate and report results. 1. `npm run typecheck:all` — verify zero TypeScript errors across src/, tests/, and scripts/ 2. `npm run lint` — verify zero ESLint warnings/errors -3. `npm run test` — run all unit tests (2665+ expected) +3. `npm run test` — run all unit tests (2686+ expected) 4. `npm run test:coverage` — verify coverage thresholds (88% statements / 80% branches / 85% functions / 90% lines) 5. `npm run build` — verify clean build (ESM + CJS + .d.ts) 6. Verify `dist/` output contains: `index.js`, `index.cjs`, `index.d.ts`, `worker/index.js`, `tools/index.js`, `tools/build-emoji-font.js` (the last two are required by package.json `exports`/`bin`) diff --git a/AGENTS.md b/AGENTS.md index fc66c08..f2ded95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Guidance for AI coding agents (Cursor, Aider, Claude Code, Continue, Zed, Cline, pdfnative is a **zero-runtime-dependency** TypeScript library that generates ISO 32000-1 (PDF 1.7) and ISO 19005 (PDF/A) compliant PDFs. Pure native — no Cairo, no PDFKit, no node-forge, no fontkit, no anything. -Quality bar: GAFAM-grade. 2665+ tests, 95%+ statement coverage measured at v1.6.0 (CI enforces ≥88%), blocking veraPDF validation in CI, SLSA provenance on npm. +Quality bar: GAFAM-grade. 2686+ tests, 95%+ statement coverage measured at v1.6.0 (CI enforces ≥88%), blocking veraPDF validation in CI, SLSA provenance on npm. Since v1.6.0 the parser side also **decrypts** (Standard Security Handler, RC4 + AES-128/256), **streams** page-tree merge/split/extract, **fills & flattens** AcroForms — including encrypted sources via encrypted incremental update — **extracts text** with positions (`extractText`), renders native **vector charts**, and **re-encrypts** output (`MergeOptions.encrypt`). All exported from `src/index.ts` like everything else. @@ -16,7 +16,7 @@ Since v1.6.0 the parser side also **decrypts** (Standard Security Handler, RC4 + ```bash npm run build # tsup → dist/ (ESM + CJS + .d.ts) -npm run test # vitest run (2665+ tests) +npm run test # vitest run (2686+ tests) npm run typecheck:all # src/ + tests/ + scripts/ npm run lint # eslint npm run test:generate # produce ~242 sample PDFs → test-output/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d9cb2d..1dd91a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ Then open: ## Test ```bash -npm run test # vitest run (2665+ tests) +npm run test # vitest run (2686+ tests) npm run test:watch # vitest (watch mode) npm run test:coverage # vitest with v8 coverage (95%+ stmts measured at the v1.6.0 release; CI enforces ≥88%) npm run test:generate # Generate 242 sample PDFs → test-output/ @@ -146,7 +146,7 @@ src/ fonts/ # 26 pre-built font-data modules (22 scripts + Latin + math + mono and colour emoji) tools/ # CLI tool for converting TTF → importable data modules scripts/ # Modular sample PDF generation (48 generators, 242 PDFs) -tests/ # 2665+ tests (122 files: unit + integration + fuzz + parser), mirrors src/ structure +tests/ # 2686+ tests (123 files: unit + integration + fuzz + parser + docs), mirrors src/ structure bench/ # Performance benchmarks (vitest bench) ``` diff --git a/README.md b/README.md index df5eecd..9307dd5 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Detailed docs: [CLI guide](docs/guides/cli.md) · [MCP guide](docs/guides/mcp.md - **FlateDecode compression** — zlib stream compression (50–90% size reduction), zero-dependency, platform-native - **Web Worker support** — off-main-thread generation for large datasets - **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations -- **Heavily tested** — 2665+ tests across 122 files, fuzz suite, dual-mode visual-regression suite, performance benchmarks; 95.41% statement coverage measured at the v1.6.0 release, with CI enforcing ≥88% statements / 80% branches / 85% functions / 90% lines (vitest.config.ts) +- **Heavily tested** — 2686+ tests across 123 files, fuzz suite, dual-mode visual-regression suite, performance benchmarks; 95.41% statement coverage measured at the v1.6.0 release, with CI enforcing ≥88% statements / 80% branches / 85% functions / 90% lines (vitest.config.ts) - **NPM provenance** — signed builds via GitHub Actions OIDC - **On-device generation** — runs in Node, browsers, Workers, Deno, Bun. No SaaS round-trip; documents never leave the calling process unless your application explicitly sends them - **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home @@ -102,7 +102,7 @@ npm install pdfnative - ❓ **FAQ:** [docs/guides/faq.md](docs/guides/faq.md) — fonts, encryption, signatures, comparisons. - 🤖 **Agentic workflows:** [docs/guides/agentic-workflows.md](docs/guides/agentic-workflows.md) — extend the engine at runtime (register fonts without a release) and embed agent-generated images. - 🛠️ **Troubleshooting:** [docs/guides/troubleshooting.md](docs/guides/troubleshooting.md) — common pitfalls. -- 🎮 **Playgrounds:** nine interactive demos at [docs/playgrounds/](docs/playgrounds/) — [extreme-scripts](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests), [all-scripts](docs/playgrounds/all-scripts.html) (every Unicode script), [scale](docs/playgrounds/scale.html) (1k-100k pages, Web Worker + true streaming), [authoring-plus](docs/playgrounds/authoring-plus.html) (SVG text, layout overlay, annotations), [toolkit](docs/playgrounds/toolkit.html) (bookmarks, page labels, viewer prefs, merge/split/extract, and v1.6.0 form fill/flatten), [charts](docs/playgrounds/charts.html) (native vector charts — all 9 v1.7.0 types), plus [cli](docs/playgrounds/cli.html), [mcp](docs/playgrounds/mcp.html) and [react](docs/playgrounds/react.html) ecosystem explorers. +- 🎮 **Playgrounds:** ten interactive demos at [docs/playgrounds/](docs/playgrounds/) — [extreme-scripts](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests), [all-scripts](docs/playgrounds/all-scripts.html) (every Unicode script), [scale](docs/playgrounds/scale.html) (1k-100k pages, Web Worker + true streaming), [authoring-plus](docs/playgrounds/authoring-plus.html) (SVG text, layout overlay, annotations), [toolkit](docs/playgrounds/toolkit.html) (bookmarks, page labels, viewer prefs, merge/split/extract, and v1.6.0 form fill/flatten), [charts](docs/playgrounds/charts.html) (native vector charts — all 9 v1.7.0 types), [inspect](docs/playgrounds/inspect.html) (drop any PDF and read a full report, entirely client-side), plus [cli](docs/playgrounds/cli.html), [mcp](docs/playgrounds/mcp.html) and [react](docs/playgrounds/react.html) ecosystem explorers. - 🧪 **Sample PDFs:** [scripts/generators/](scripts/generators/) — 242 sample PDFs across 37 categories (48 generators) (see [Sample PDFs](#sample-pdfs) below). - 🎓 **Learn:** [docs/learn/](docs/learn/) — an eight-step guided path from installing the package to generating 100,000 pages, live at [pdfnative.dev/learn/](https://pdfnative.dev/learn/). - 🤖 **Machine-readable:** [llms.txt](llms.txt) (doc index for LLMs), [AGENTS.md](AGENTS.md) (agent guidance), and [docs/assets/ecosystem.json](docs/assets/ecosystem.json) — the canonical manifest of ecosystem versions and counts, enforced by `npm run verify:docs`. @@ -1212,7 +1212,7 @@ src/ fonts/ # Pre-built font data modules (22 scripts) tools/ # CLI: build-font-data.cjs (TTF → JS module) scripts/ # Modular sample PDF generation (48 generators, 242 PDFs) -tests/ # 2665+ tests (122 files: unit + integration + fuzz + parser) +tests/ # 2686+ tests (123 files: unit + integration + fuzz + parser + docs) bench/ # Performance benchmarks (vitest bench) ``` @@ -1224,7 +1224,7 @@ cd pdfnative npm install npm run build # tsup → dist/ (ESM + CJS + .d.ts) -npm run test # vitest run (2665+ tests) +npm run test # vitest run (2686+ tests) npm run test:coverage # vitest with v8 coverage (95.41% statements at the v1.6.0 release; CI gates: 88/80/85/90) npm run test:generate # Generate 242 sample PDFs → test-output/ npm run lint # ESLint 9 + typescript-eslint strict @@ -1239,7 +1239,7 @@ npm run bench # Performance benchmarks (vitest bench) | Metric | Value | |--------|-------| -| Tests | 2665+ (122 files) | +| Tests | 2686+ (123 files) | | Statement coverage | 95.41% (measured at the v1.6.0 release; CI enforces ≥88%, vitest.config.ts) | | Branch coverage | 87.79% (measured at the v1.6.0 release; CI enforces ≥80%) | | Function coverage | 98.5% (measured at the v1.6.0 release; CI enforces ≥85%; lines gate: ≥90%) | diff --git a/docs/agent-brief.md b/docs/agent-brief.md index d3100c4..2908cdf 100644 --- a/docs/agent-brief.md +++ b/docs/agent-brief.md @@ -15,8 +15,9 @@ Node ≥ 22, browsers, Deno, Bun, Web Workers. No SaaS round-trip, no telemetry, no sockets. Current version: 1.7.0. It writes (documents, tables, charts, barcodes, SVG, forms, watermarks, signatures with long-term validation, print production) and reads (parse, decrypt, extract text, read/fill/flatten forms, -merge/split/extract pages, verify structure) — 22 Unicode scripts with real -OpenType shaping and UAX #9 BiDi. +merge/split/extract pages, verify structure) — 22 Unicode scripts, with +OpenType GSUB/GPOS shaping for the complex ones (Thai, Arabic, Devanagari, +Bengali, Tamil, Telugu, Sinhala, Tibetan, Khmer, Myanmar) and full UAX #9 BiDi. ## Choose your surface @@ -35,7 +36,7 @@ import { buildDocumentPDFBytes } from 'pdfnative'; // Synchronous — returns a Uint8Array, not a Promise. const bytes = buildDocumentPDFBytes({ title: 'Invoice 42', // top-level, not inside metadata - metadata: { author: 'Me' }, // author / subject / keywords only + metadata: { author: 'Me' }, // author / subject / keywords / trapped (v1.7.0) blocks: [ { type: 'heading', text: 'Invoice 42', level: 1 }, { type: 'paragraph', text: 'Thank you for your order.' }, @@ -72,8 +73,8 @@ Functions an agent reaches for most, all exported from `'pdfnative'`: 1. **`buildDocumentPDFBytes` is synchronous.** It returns a `Uint8Array`, not a Promise — do not `await` it (harmless) and do not `.then()` it (breaks). -2. **`title` is top-level**, not inside `metadata` (`metadata` takes only - `author` / `subject` / `keywords`). +2. **`title` is top-level**, not inside `metadata` (`metadata` takes + `author` / `subject` / `keywords`, plus `trapped` since v1.7.0). 3. **`registerFont` alone is a no-op.** You must also `await loadFontData(lang)` and pass the result in `fontEntries: [{ fontData, fontRef, lang }]`. 4. **`/F1` and `/F2` are reserved font refs** — start custom `fontRef` at `/F3`. diff --git a/docs/app.js b/docs/app.js index 844c051..ee98642 100644 --- a/docs/app.js +++ b/docs/app.js @@ -54,11 +54,32 @@ } // ── Copy to clipboard ───────────────────────────────────── + // Single helper for every copy affordance: the Clipboard API only exists + // in secure contexts, so guard it and fall back to execCommand on a + // temporary textarea instead of throwing synchronously in the handler. + function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (res, rej) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (_) { /* fall through */ } + ta.remove(); + if (ok) res(); else rej(new Error('Clipboard unavailable')); + }); + } + document.querySelectorAll('.copy-btn').forEach(function (btn) { btn.addEventListener('click', function () { var text = btn.getAttribute('data-copy'); if (!text) return; - navigator.clipboard.writeText(text).then(function () { + copyText(text).then(function () { btn.classList.add('copied'); var prev = btn.innerHTML; btn.innerHTML = '✓'; @@ -66,6 +87,14 @@ btn.innerHTML = prev; btn.classList.remove('copied'); }, 1500); + }, function () { + btn.classList.add('copied'); + var prev = btn.innerHTML; + btn.innerHTML = '✗'; + setTimeout(function () { + btn.innerHTML = prev; + btn.classList.remove('copied'); + }, 1500); }); }); }); @@ -76,7 +105,7 @@ var url = btn.getAttribute('data-copy-url'); fetch(url, { cache: 'no-cache' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }) - .then(function (text) { return navigator.clipboard.writeText(text); }) + .then(function (text) { return copyText(text); }) .then(function () { var prev = btn.textContent; btn.textContent = '✓ Copied'; @@ -126,7 +155,10 @@ } // ── Code tabs ───────────────────────────────────────────── - var tabBtns = document.querySelectorAll('.tab-btn'); + // Scoped to the Examples section: other page controls (e.g. the demo's + // Code/JSON mode switch) must never be captured by this tablist. + var exampleTabBar = document.querySelector('#examples .tab-bar'); + var tabBtns = exampleTabBar ? exampleTabBar.querySelectorAll('.tab-btn') : []; var tabPanels = document.querySelectorAll('.tab-panel'); function activateTab(btn) { @@ -594,6 +626,8 @@ label: 'Extract text & re-encrypt (v1.6.0)', description: 'The 1.6.0 parser round trip: build a PDF, extract its text with positions (open the browser console), then merge it with an AES-256 encrypted annex and re-encrypt the result with a new password.', source: GENERATORS_BASE + 'text-extract-showcase.ts', + encryptedOutput: true, + previewNote: 'This example produces an AES-256 encrypted PDF (password: rotated). The inline preview is skipped — the browser viewer would ask for the password inside a cramped frame. Use Download and open the file in your PDF reader.', code: [ "import { buildDocumentPDFBytes, extractText, mergePdfs, downloadBlob } from 'pdfnative';", '', @@ -696,7 +730,10 @@ // Same Blob → object-URL pattern as the React playground. Some browsers // (most mobile ones) cannot render PDFs in an iframe; when the engine // says so, be honest about it instead of showing an empty frame. - var canPreview = navigator.pdfViewerEnabled !== false; + // Explicit opt-in: on browsers that predate the API the property is + // undefined, and most of those cannot render a PDF in an iframe anyway — + // an honest note beats an empty grey frame. + var canPreview = navigator.pdfViewerEnabled === true; if (!canPreview && demoPreview && demoPreviewNote) { demoPreview.hidden = true; demoPreviewNote.hidden = false; @@ -710,11 +747,35 @@ demoPreview.src = lastPreviewUrl; } + // An encrypted PDF in the preview iframe makes the browser's viewer ask + // for the password inside a cramped frame. Primary signal: the example's + // own declaration; safety net for user-authored code: the /Encrypt token + // in the byte tail (trailer region). + function looksEncrypted(bytes) { + var tail = bytes.subarray(Math.max(0, bytes.length - 2048)); + var text = ''; + for (var i = 0; i < tail.length; i++) text += String.fromCharCode(tail[i]); + return text.indexOf('/Encrypt') !== -1; + } + // The demo module calls `downloadBlob` like real pdfnative code; the demo // routes those bytes to the preview pane and only downloads on request. function captureSink(bytes, name) { lastPdf = { bytes: bytes, name: name || 'document.pdf' }; - showPreview(bytes); + var ex = EXAMPLES.find(function (e) { return e.id === currentId; }); + var encrypted = (demoMode === 'code' && ex && ex.encryptedOutput) || looksEncrypted(bytes); + if (encrypted) { + if (demoPreview) { demoPreview.hidden = true; demoPreview.removeAttribute('src'); } + if (demoPreviewNote) { + demoPreviewNote.textContent = (ex && ex.previewNote) || + 'This PDF is encrypted — the inline viewer would prompt for its password in a cramped frame. Use Download and open it in your PDF reader.'; + demoPreviewNote.hidden = false; + } + } else { + if (demoPreview && canPreview) demoPreview.hidden = false; + if (demoPreviewNote && canPreview) demoPreviewNote.hidden = true; + showPreview(bytes); + } if (demoDownload) demoDownload.disabled = false; } @@ -743,6 +804,8 @@ function setMode(mode) { demoMode = mode; var json = mode === 'json'; + if (demoModeCode) demoModeCode.setAttribute('aria-pressed', String(!json)); + if (demoModeJson) demoModeJson.setAttribute('aria-pressed', String(json)); if (demoCode) demoCode.hidden = json; if (demoJson) { demoJson.hidden = !json; @@ -753,14 +816,8 @@ if (demoJsonNote) demoJsonNote.hidden = !json; if (demoCopyCli) demoCopyCli.hidden = !json; if (demoCopyMcp) demoCopyMcp.hidden = !json; - if (demoModeCode) { - demoModeCode.classList.toggle('active', !json); - demoModeCode.setAttribute('aria-selected', String(!json)); - } - if (demoModeJson) { - demoModeJson.classList.toggle('active', json); - demoModeJson.setAttribute('aria-selected', String(json)); - } + if (demoModeCode) demoModeCode.classList.toggle('active', !json); + if (demoModeJson) demoModeJson.classList.toggle('active', json); } if (demoModeCode) demoModeCode.addEventListener('click', function () { setMode('code'); }); if (demoModeJson) demoModeJson.addEventListener('click', function () { setMode('json'); }); @@ -830,7 +887,7 @@ } else { url = base + '#preset=' + (currentId || DEFAULT_ID); } - await navigator.clipboard.writeText(url); + await copyText(url); var prev = demoShare.textContent; demoShare.textContent = '✓ Copied'; setTimeout(function () { demoShare.textContent = prev; }, 1500); @@ -842,7 +899,7 @@ } function copyWithFeedback(btn, text) { - navigator.clipboard.writeText(text).then(function () { + copyText(text).then(function () { var prev = btn.textContent; btn.textContent = '✓ Copied'; setTimeout(function () { btn.textContent = prev; }, 1500); @@ -871,14 +928,20 @@ }); } - // Restore a shared document from the URL hash. + // Restore a shared document from the URL hash. `hashReady` lets the + // autorun observer wait for the (async) decode instead of racing it and + // rendering the default example over the shared one; #doc= is not an + // element id, so we also bring the visitor to the demo ourselves. + var hashReady = Promise.resolve(); var docMatch = /(?:^|[#&])doc=([\w.~-]+)/.exec(location.hash); if (docMatch && demoJson) { - decodeDocHash(docMatch[1]) + hashReady = decodeDocHash(docMatch[1]) .then(function (text) { var doc = parseDocJson(text); setMode('json'); demoJson.value = JSON.stringify(doc, null, 2); + var demoSection = document.getElementById('demo'); + if (demoSection) demoSection.scrollIntoView(); }) .catch(function () { /* off-shape payloads are silently ignored */ }); } @@ -960,7 +1023,17 @@ for (var i = 0; i < n; i++) { rows.push({ cells: ['2026-08-' + ((i % 28) + 1), 'Line item ' + i, '$' + (i * 3.5).toFixed(2)] }); } - return { title: 'Benchmark ' + n, headers: ['Date', 'Description', 'Amount'], rows: rows }; + // PdfParams requires infoItems/balanceText/countText/footerText — + // the engine's boundary validation does not (yet) guard them. + return { + title: 'Benchmark ' + n, + infoItems: [{ label: 'Rows', value: String(n) }], + balanceText: 'Synthetic dataset', + countText: n + ' rows', + headers: ['Date', 'Description', 'Amount'], + rows: rows, + footerText: 'pdfnative.dev live benchmark' + }; }; pdfnativeModule.buildPDFBytes(makeParams(50)); // warm-up var sizes = [100, 500, 1000]; @@ -982,18 +1055,23 @@ }).join(''); rowsEl.hidden = false; noteEl.hidden = false; - benchBtn.textContent = '↺ Run again'; } catch (e) { - benchBtn.textContent = 'Failed — ' + (e.message || e); + // Never destroy the button label with an error message. + noteEl.textContent = 'Benchmark failed: ' + (e.message || e); + noteEl.setAttribute('role', 'alert'); + noteEl.hidden = false; } finally { + benchBtn.textContent = '↺ Run again'; benchBtn.disabled = false; } }); } // First render without a click, once the demo scrolls into view — the - // visitor sees a real PDF instead of an empty pane. One shot only. - if ('IntersectionObserver' in window && canPreview) { + // visitor sees a real PDF instead of an empty pane. One shot only, and + // desktop only: auto-loading the CDN engine on mobile data without a + // click would be presumptuous. + if ('IntersectionObserver' in window && canPreview && matchMedia('(min-width: 901px)').matches) { var demoSection = document.getElementById('demo'); if (demoSection) { var ran = false; @@ -1002,7 +1080,13 @@ if (entry.isIntersecting && !ran) { ran = true; io.disconnect(); - runDemo(); + // Never auto-run an example whose output is encrypted — the + // visitor would face a password prompt without having clicked. + var ex = EXAMPLES.find(function (e) { return e.id === DEFAULT_ID; }); + if (ex && ex.encryptedOutput) return; + // A shared #doc= link may still be decoding: wait for it so the + // first render shows the shared document, not the default. + hashReady.then(runDemo); } }); }, { rootMargin: '200px' }); diff --git a/docs/assets/api.json b/docs/assets/api.json index 7bceb0a..9a236a1 100644 --- a/docs/assets/api.json +++ b/docs/assets/api.json @@ -9,35 +9,35 @@ "kind": "type", "module": "src/core/pdf-doc-timestamp.ts", "signature": "export interface AddDocumentTimestampOptions", - "summary": "pdfnative — Document Timestamps (PAdES B-LTA, v1.7.0) ====================================================== `addDocumentTimestamp` appends a document-timestamp revision per ISO 32000-2 §12.8.5: a signature field whose dictionary is `/Type " + "summary": "Options for addDocumentTimestamp." }, { "name": "AddSignaturePlaceholderOptions", "kind": "type", "module": "src/core/pdf-sig-placeholder.ts", "signature": "export interface AddSignaturePlaceholderOptions", - "summary": "pdfnative — Signature Placeholder Injector ============================================ Inject an AcroForm + invisible signature widget placeholder into an existing PDF via incremental update (ISO 32000-1 §7.5.6, §12.7.4.5, §12.8)." + "summary": "Options for addSignaturePlaceholder." }, { "name": "Annotation", "kind": "type", "module": "src/core/pdf-annot.ts", "signature": "export type Annotation = LinkAnnotation | InternalLink;", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Union of all annotation types." }, { "name": "AnnotationBase", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Fields shared by every markup / drawing annotation." }, { "name": "AnnotationRect", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export type AnnotationRect = readonly [number, number, number, number];", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Rectangle `[x1, y1, x2, y2]` in PDF user space (points)." }, { "name": "Asn1Node", @@ -58,63 +58,63 @@ "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface BarcodeBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Barcode block — renders a 1D or 2D barcode using PDF path operators." }, { "name": "BarcodeFormat", "kind": "type", "module": "src/core/pdf-barcode.ts", "signature": "export type BarcodeFormat = 'code128' | 'ean13' | 'qr' | 'datamatrix' | 'pdf417';", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Supported barcode formats." }, { "name": "BidiRun", "kind": "type", "module": "src/shaping/bidi.ts", "signature": "export interface BidiRun", - "summary": "pdfnative — Simplified Unicode Bidirectional Algorithm ======================================================= Pure JS implementation of a subset of UAX #9 (Unicode Bidirectional Algorithm)." + "summary": "A run of text with a resolved embedding level." }, { "name": "CellBorders", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface CellBorders", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Per-cell border configuration for a TableBlock." }, { "name": "ChartBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ChartBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Chart block — native vector charts rendered as pure PDF path operators (zero dependencies, no rasterisation)." }, { "name": "ChartSeries", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ChartSeries", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "A single data series in a ChartBlock." }, { "name": "ChartType", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export type ChartType = 'bar' | 'barH' | 'line' | 'pie' | 'donut'", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Supported chart types (v1.6.0; `stackedBar`/`stackedBarH`/`area`/`scatter` since v1.7.0)." }, { "name": "CmsDigestAlgorithm", "kind": "type", "module": "src/crypto/cms.ts", "signature": "export type CmsDigestAlgorithm = 'sha256' | 'sha384' | 'sha512';", - "summary": "pdfnative — CMS SignedData Builder (RFC 5652) ============================================== Builds CMS/PKCS#7 SignedData structures for PDF digital signatures." + "summary": "Message-digest algorithm used across the CMS structure (digestAlgorithms set, message-digest attribute, SignerInfo digestAlgorithm, certHash)." }, { "name": "CmsProfile", "kind": "type", "module": "src/crypto/cms.ts", "signature": "export type CmsProfile = 'pkcs7' | 'pades';", - "summary": "pdfnative — CMS SignedData Builder (RFC 5652) ============================================== Builds CMS/PKCS#7 SignedData structures for PDF digital signatures." + "summary": "CMS profile: - `'pkcs7'` (default) — classic adbe.pkcs7.detached signed attributes: content-type, message-digest, signing-time." }, { "name": "CmsSignOptions", @@ -128,91 +128,91 @@ "kind": "type", "module": "src/core/pdf-dss.ts", "signature": "export interface CollectLtvOptions", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Options for collectValidationInfo / addValidationInfo." }, { "name": "ColorGlyph", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ColorGlyph", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A resolved colour glyph: ordered layers painted back-to-front." }, { "name": "ColorGlyphForm", "kind": "type", "module": "src/core/pdf-color-glyph.ts", "signature": "export interface ColorGlyphForm", - "summary": "pdfnative — Colour Glyph Renderer (COLR/CPAL → native PDF) ========================================================== Renders a resolved ColorGlyph as a native PDF Form XObject: each layer's base-glyph outline is emitted as a vector path an" + "summary": "A rendered colour glyph ready to be assembled into a Form XObject." }, { "name": "ColorLayer", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ColorLayer", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A single colour-glyph layer: a base outline filled by a paint." }, { "name": "ColorPaint", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type ColorPaint = SolidPaint | LinearGradientPaint | RadialGradientPaint | SweepGradientPaint;", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A paint used to fill a colour-glyph layer." }, { "name": "ColorStop", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ColorStop", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A gradient colour stop: `offset` in [0,1] with a resolved colour." }, { "name": "ColumnDef", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ColumnDef", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Column definition for the table layout." }, { "name": "Contour", "kind": "type", "module": "src/fonts/glyf-outline.ts", "signature": "export type Contour = OutlinePoint[];", - "summary": "pdfnative — TrueType `glyf` Outline Extractor ============================================== Pure-JS reader that turns a TrueType glyph outline into ordered contours of quadratic on/off-curve points." + "summary": "A closed contour: an ordered ring of on/off-curve points." }, { "name": "CpalColor", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type CpalColor = readonly [number, number, number, number];", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "An sRGB colour with alpha, each channel 0–255." }, { "name": "CrlRevokedEntry", "kind": "type", "module": "src/crypto/crl.ts", "signature": "export interface CrlRevokedEntry", - "summary": "pdfnative — CRL Parser (RFC 5280 §5) ===================================== Parses X.509 CertificateList DER for LTV revocation data collection (PAdES B-LT — /DSS /CRLs streams)." + "summary": "One revokedCertificates entry." }, { "name": "CryptFilterMethod", "kind": "type", "module": "src/parser/pdf-decrypt.ts", "signature": "export type CryptFilterMethod = 'V2' | 'AESV2' | 'AESV3' | 'Identity';", - "summary": "pdfnative — Standard Security Handler reader/decryptor ========================================================= Password authentication and transparent object decryption for encrypted PDFs (ISO 32000-1 §7.6 / ISO 32000-2 §7.6), closing the" + "summary": "Crypt-filter method resolved for streams or strings." }, { "name": "CryptoProvider", "kind": "type", "module": "src/crypto/crypto-provider.ts", "signature": "export interface CryptoProvider", - "summary": "pdfnative — Pluggable Signature Crypto Provider ================================================ pdfnative ships its own pure-TypeScript RSA (PKCS#1 v1.5) and ECDSA (P-256) implementations so that signing works in any runtime with **zero de" + "summary": "A pluggable signer that replaces pdfnative's pure-JS RSA/ECDSA math with a native, constant-time implementation." }, { "name": "CustomOutputIntent", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface CustomOutputIntent", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Caller-supplied OutputIntent for tagged/PDF-A documents (v1.7.0): replaces the built-in minimal sRGB profile with a real ICC profile (e.g." }, { "name": "DEFAULT_COLORS", @@ -254,28 +254,28 @@ "kind": "value", "module": "src/core/pdf-layout.ts", "signature": "export const DEFAULT_MAX_BLOCKS = 100_000;", - "summary": "pdfnative — PDF Layout Constants ================================== A4 page dimensions and default layout settings." + "summary": "Default ceiling for `buildDocumentPDF` block count, overridable per call via `PdfLayoutOptions.maxBlocks`." }, { "name": "DEFAULT_MAX_INFLATE_OUTPUT", "kind": "value", "module": "src/parser/pdf-inflate.ts", "signature": "export const DEFAULT_MAX_INFLATE_OUTPUT = 100 * 1024 * 1024;", - "summary": "pdfnative — DEFLATE Decompressor (FlateDecode reader) ====================================================== Zero-dependency DEFLATE decompression for reading FlateDecode streams." + "summary": "Default maximum decompressed size (bytes) for inflate operations." }, { "name": "DecryptionContext", "kind": "type", "module": "src/parser/pdf-decrypt.ts", "signature": "export interface DecryptionContext", - "summary": "pdfnative — Standard Security Handler reader/decryptor ========================================================= Password authentication and transparent object decryption for encrypted PDFs (ISO 32000-1 §7.6 / ISO 32000-2 §7.6), closing the" + "summary": "Everything needed to decrypt the objects of one document, produced by a successful authenticate call." }, { "name": "DocumentBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export type DocumentBlock =", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Union of all supported document blocks." }, { "name": "DocumentMetadata", @@ -289,42 +289,42 @@ "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface DocumentParams", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Parameters for the free-form document PDF builder." }, { "name": "EcPrivateKey", "kind": "type", "module": "src/crypto/ecdsa.ts", "signature": "export interface EcPrivateKey", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "ECDSA P-256 private key." }, { "name": "EcPublicKey", "kind": "type", "module": "src/crypto/ecdsa.ts", "signature": "export interface EcPublicKey", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "ECDSA P-256 public key." }, { "name": "EmbeddedFilesResult", "kind": "type", "module": "src/core/pdf-tags.ts", "signature": "export interface EmbeddedFilesResult", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Result of building embedded file objects for PDF/A-3." }, { "name": "EncodingContext", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface EncodingContext", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Encoding context encapsulating text encoding and font reference logic." }, { "name": "EncryptionOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface EncryptionOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Options for PDF encryption (password protection)." }, { "name": "ExtractTextOptions", @@ -345,7 +345,7 @@ "kind": "type", "module": "src/parser/pdf-text-extract.ts", "signature": "export interface ExtractedTextRun", - "summary": "pdfnative — PDF Text Extraction ================================= Decodes page content streams (ISO 32000-1 §9) into per-page Unicode text with optional positioned runs." + "summary": "One text-showing operation (`Tj`, one `TJ` string, `'` or `\"`)." }, { "name": "FT_H", @@ -359,84 +359,84 @@ "kind": "type", "module": "src/core/pdf-form-fill.ts", "signature": "export interface FillFormOptions", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "Options for fillForm." }, { "name": "FlattenFormOptions", "kind": "type", "module": "src/core/pdf-form-fill.ts", "signature": "export interface FlattenFormOptions", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "Options for flattenForm." }, { "name": "FontData", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface FontData", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Pre-built font data loaded from font data modules." }, { "name": "FontEntry", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface FontEntry", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A font entry binding FontData to a PDF font reference." }, { "name": "FontLoader", "kind": "type", "module": "src/fonts/font-loader.ts", "signature": "export type FontLoader = () => Promise;", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Font loader function type — returns a FontData or module with default export" }, { "name": "FontMetrics", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface FontMetrics", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Font metrics embedded in font data modules." }, { "name": "FontRun", "kind": "type", "module": "src/shaping/multi-font.ts", "signature": "export interface FontRun", - "summary": "pdfnative — Multi-Font Text Run Splitter ========================================== Split text into runs, each assigned to the font whose cmap covers it." + "summary": "A text run with its assigned font entry" }, { "name": "FontValidationResult", "kind": "type", "module": "src/fonts/font-validator.ts", "signature": "export interface FontValidationResult", - "summary": "pdfnative — Font Data Validator ================================ `validateFontData()` performs a read-only structural sanity check on a FontData module before it is used for rendering." + "summary": "Result of validateFontData." }, { "name": "FormField", "kind": "type", "module": "src/core/pdf-form.ts", "signature": "export interface FormField", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Resolved form field descriptor ready for PDF object emission." }, { "name": "FormFieldBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface FormFieldBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Form field block — interactive AcroForm widget (ISO 32000-1 §12.7)." }, { "name": "FormFieldNotFoundError", "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export class FormFieldNotFoundError extends Error", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "A named field was not found in the document." }, { "name": "FormFieldType", "kind": "type", "module": "src/core/pdf-form.ts", "signature": "export type FormFieldType =", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Supported AcroForm field types (ISO 32000-1 §12.7.3–12.7.4)." }, { "name": "FormFillValue", @@ -450,42 +450,42 @@ "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export class FormUnsupportedError extends Error", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "The field or document uses a feature fill/flatten does not support." }, { "name": "FormValueTypeError", "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export class FormValueTypeError extends Error", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "A supplied value has the wrong type for its field, or is not a valid option." }, { "name": "FormWidgetResult", "kind": "type", "module": "src/core/pdf-form.ts", "signature": "export interface FormWidgetResult", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Result of building a form field: widget annotation dict + appearance stream(s)." }, { "name": "FreeTextAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface FreeTextAnnotation extends AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Free-text (typewriter) annotation (`/Subtype /FreeText`)." }, { "name": "GlyfFont", "kind": "type", "module": "src/fonts/glyf-outline.ts", "signature": "export interface GlyfFont", - "summary": "pdfnative — TrueType `glyf` Outline Extractor ============================================== Pure-JS reader that turns a TrueType glyph outline into ordered contours of quadratic on/off-curve points." + "summary": "A parsed TrueType font exposing just enough to read glyph outlines." }, { "name": "GradientExtend", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type GradientExtend = 'pad' | 'repeat' | 'reflect';", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "How a gradient extends beyond its [0,1] range (COLR Extend / PDF Extend)." }, { "name": "HEADER_H", @@ -499,7 +499,7 @@ "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface HeadingBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Heading block — rendered at a predefined size based on level." }, { "name": "INFO_LN", @@ -513,175 +513,175 @@ "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ImageBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Image block — embeds a JPEG or PNG image." }, { "name": "InspectedBlock", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface InspectedBlock", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "One block's laid-out footprint, as reported by inspectDocumentLayout." }, { "name": "InspectedPage", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface InspectedPage", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "One page's worth of InspectedBlocks." }, { "name": "InternalLink", "kind": "type", "module": "src/core/pdf-annot.ts", "signature": "export interface InternalLink", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Internal link annotation (GoTo action — page destination)." }, { "name": "KNOWN_DECODE_FILTERS", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export const KNOWN_DECODE_FILTERS = new Set([", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Known non-Flate decode filter names (for membership checks)." }, { "name": "LayoutDebugOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface LayoutDebugOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Fine-grained control over the PdfLayoutOptions.debug overlay layers." }, { "name": "LayoutInspection", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface LayoutInspection", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Deterministic, read-only description of how inspectDocumentLayout expects the document builder to paginate and place a set of blocks." }, { "name": "LineAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface LineAnnotation extends AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Straight-line annotation (`/Subtype /Line`)." }, { "name": "LinearGradientPaint", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface LinearGradientPaint", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A linear (axial) gradient fill (COLR PaintLinearGradient → PDF Shading 2)." }, { "name": "LinkAnnotation", "kind": "type", "module": "src/core/pdf-annot.ts", "signature": "export interface LinkAnnotation", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "External link annotation (URI action)." }, { "name": "LinkBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface LinkBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Link block — clickable hyperlink text." }, { "name": "ListBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ListBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "List block — bullet or numbered items." }, { "name": "ListItem", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ListItem", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "A single hierarchical list entry: text plus an optional nested sub-list." }, { "name": "LtvData", "kind": "type", "module": "src/core/pdf-dss.ts", "signature": "export interface LtvData", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Transport-free validation material, serializable and replayable." }, { "name": "MAX_PARSE_DEPTH", "kind": "value", "module": "src/parser/pdf-object-parser.ts", "signature": "export const MAX_PARSE_DEPTH = 1000;", - "summary": "pdfnative — PDF Object Parser ================================ Parses PDF indirect objects from a tokenized stream (ISO 32000-1 §7.3)." + "summary": "Maximum depth for recursive PDF object parsing (nested arrays/dicts)." }, { "name": "MAX_XREF_CHAIN", "kind": "value", "module": "src/parser/pdf-xref-parser.ts", "signature": "export const MAX_XREF_CHAIN = 100;", - "summary": "pdfnative — PDF Cross-Reference Table Parser =============================================== Parses traditional xref tables and xref streams (ISO 32000-1 §7.5)." + "summary": "Maximum depth of the xref `/Prev` chain." }, { "name": "MarkupAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export type MarkupAnnotation =", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Any builder-supported annotation." }, { "name": "MergeOptions", "kind": "type", "module": "src/parser/pdf-pagetree.ts", "signature": "export interface MergeOptions", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Options for the page-tree manipulation API (mergePdfs, splitPdf, extractPages)." }, { "name": "OcspCertStatus", "kind": "type", "module": "src/crypto/ocsp.ts", "signature": "export type OcspCertStatus = 'good' | 'revoked' | 'unknown';", - "summary": "pdfnative — OCSP Request Builder / Response Parser (RFC 6960) ============================================================== Builds unsigned OCSPRequest DER and parses OCSPResponse / BasicOCSPResponse for LTV revocation data collection (PAd" + "summary": "SingleResponse certStatus." }, { "name": "OcspRequestOptions", "kind": "type", "module": "src/crypto/ocsp.ts", "signature": "export interface OcspRequestOptions", - "summary": "pdfnative — OCSP Request Builder / Response Parser (RFC 6960) ============================================================== Builds unsigned OCSPRequest DER and parses OCSPResponse / BasicOCSPResponse for LTV revocation data collection (PAd" + "summary": "Options for buildOcspRequest." }, { "name": "OcspResponse", "kind": "type", "module": "src/crypto/ocsp.ts", "signature": "export interface OcspResponse", - "summary": "pdfnative — OCSP Request Builder / Response Parser (RFC 6960) ============================================================== Builds unsigned OCSPRequest DER and parses OCSPResponse / BasicOCSPResponse for LTV revocation data collection (PAd" + "summary": "Parsed OCSPResponse (RFC 6960 §4.2.1)." }, { "name": "OpenPdfOptions", "kind": "type", "module": "src/parser/pdf-reader.ts", "signature": "export interface OpenPdfOptions", - "summary": "pdfnative — PDF Document Reader ================================== High-level reader that provides lazy object resolution and page access." + "summary": "Options for openPdf." }, { "name": "OutlineItem", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface OutlineItem", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "A document outline (bookmark) entry — ISO 32000-1 §12.3.3." }, { "name": "OutlinePoint", "kind": "type", "module": "src/fonts/glyf-outline.ts", "signature": "export interface OutlinePoint", - "summary": "pdfnative — TrueType `glyf` Outline Extractor ============================================== Pure-JS reader that turns a TrueType glyph outline into ordered contours of quadratic on/off-curve points." + "summary": "A single outline point in font units." }, { "name": "OutlineProvider", "kind": "type", "module": "src/core/pdf-color-glyph.ts", "signature": "export type OutlineProvider = (glyphId: number) => Contour[];", - "summary": "pdfnative — Colour Glyph Renderer (COLR/CPAL → native PDF) ========================================================== Renders a resolved ColorGlyph as a native PDF Form XObject: each layer's base-glyph outline is emitted as a vector path an" + "summary": "Provider of glyph outlines (decoupled from the font parser for testing)." }, { "name": "PAGE_SIZES", @@ -695,7 +695,7 @@ "kind": "value", "module": "src/core/pdf-tags.ts", "signature": "export const PDF_A_CONFORMANCE_TARGETS = ['pdfa1b', 'pdfa2b', 'pdfa2u', 'pdfa3b'] as const;", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Canonical list of PDF/A conformance targets accepted by the `tagged` layout option." }, { "name": "PG_H", @@ -716,14 +716,14 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PageBox = readonly [number, number, number, number];", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A page box rectangle `[x0, y0, x1, y1]` in points, PDF user space." }, { "name": "PageBreakBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface PageBreakBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Page break block — forces a new page." }, { "name": "PageLabelRange", @@ -744,28 +744,28 @@ "kind": "type", "module": "src/parser/pdf-pagetree.ts", "signature": "export interface PageRange", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "A contiguous, inclusive page range (0-based)." }, { "name": "PageTemplate", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PageTemplate", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Template for page headers and footers with placeholder support." }, { "name": "ParagraphBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface ParagraphBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Paragraph block — text wrapping with configurable alignment." }, { "name": "ParsedAnnotation", "kind": "type", "module": "src/parser/pdf-reader.ts", "signature": "export interface ParsedAnnotation", - "summary": "pdfnative — PDF Document Reader ================================== High-level reader that provides lazy object resolution and page access." + "summary": "A page annotation parsed by PdfReader.getAnnotations." }, { "name": "ParsedArray", @@ -779,14 +779,14 @@ "kind": "type", "module": "src/crypto/cms-utils.ts", "signature": "export interface ParsedCms", - "summary": "pdfnative — CMS SignedData Parsing & DER Surgery (RFC 5652) ============================================================ Parses CMS/PKCS#7 SignedData structures (the inverse of cms.ts) and performs the one mutation LTV workflows need: appen" + "summary": "Structural view over a DER-encoded CMS ContentInfo/SignedData." }, { "name": "ParsedCrl", "kind": "type", "module": "src/crypto/crl.ts", "signature": "export interface ParsedCrl", - "summary": "pdfnative — CRL Parser (RFC 5280 §5) ===================================== Parses X.509 CertificateList DER for LTV revocation data collection (PAdES B-LT — /DSS /CRLs streams)." + "summary": "Parsed CertificateList (RFC 5280 §5.1)." }, { "name": "ParsedDict", @@ -807,91 +807,91 @@ "kind": "type", "module": "src/core/pdf-form-fill.ts", "signature": "export interface ParsedFormField", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "A form field parsed from an existing document by readFormFields." }, { "name": "ParsedImage", "kind": "type", "module": "src/core/pdf-image.ts", "signature": "export interface ParsedImage", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Parsed image data ready for PDF embedding." }, { "name": "PdfAConfig", "kind": "type", "module": "src/core/pdf-tags.ts", "signature": "export interface PdfAConfig", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Resolved PDF/A configuration from the `tagged` layout option." }, { "name": "PdfAConformanceTarget", "kind": "type", "module": "src/core/pdf-tags.ts", "signature": "export type PdfAConformanceTarget = typeof PDF_A_CONFORMANCE_TARGETS[number];", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Type alias for the string literal members of PDF_A_CONFORMANCE_TARGETS." }, { "name": "PdfAttachment", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfAttachment", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Embedded file attachment for PDF/A-3 (ISO 19005-3)." }, { "name": "PdfAttachmentRelationship", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfAttachmentRelationship = 'Source' | 'Data' | 'Alternative' | 'Supplement' | 'Unspecified';", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Relationship of an embedded file to the PDF document (ISO 19005-3 §6.8)." }, { "name": "PdfColor", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfColor = PdfRgbString | PdfRgbTuple | (string & {});", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Color input accepted by pdfnative." }, { "name": "PdfColors", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfColors", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Color palette for the PDF." }, { "name": "PdfDiagnostic", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfDiagnostic", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A single conformance diagnostic surfaced by the builders." }, { "name": "PdfDiagnosticCode", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfDiagnosticCode =", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Machine-readable conformance diagnostic codes (stable API — additions only)." }, { "name": "PdfDiagnosticHandler", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfDiagnosticHandler = (diagnostic: PdfDiagnostic) => void;", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Sink for conformance diagnostics." }, { "name": "PdfEncryptionInfo", "kind": "type", "module": "src/parser/pdf-reader.ts", "signature": "export interface PdfEncryptionInfo", - "summary": "pdfnative — PDF Document Reader ================================== High-level reader that provides lazy object resolution and page access." + "summary": "Encryption details of an opened document, or `null` when it is not encrypted." }, { "name": "PdfEncryptionUnsupportedError", "kind": "value", "module": "src/parser/pdf-decrypt.ts", "signature": "export class PdfEncryptionUnsupportedError extends Error", - "summary": "pdfnative — Standard Security Handler reader/decryptor ========================================================= Password authentication and transparent object decryption for encrypted PDFs (ISO 32000-1 §7.6 / ISO 32000-2 §7.6), closing the" + "summary": "Thrown when the document uses an encryption scheme pdfnative cannot read." }, { "name": "PdfIndirectObject", @@ -905,21 +905,21 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfInfoItem", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "An info key/value pair displayed in the header section." }, { "name": "PdfLayoutOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfLayoutOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Layout options (all optional, A4 defaults applied)." }, { "name": "PdfMetadataUpdate", "kind": "type", "module": "src/parser/pdf-modifier.ts", "signature": "export interface PdfMetadataUpdate", - "summary": "pdfnative — PDF Document Modifier ==================================== Modify existing PDF documents using incremental save (ISO 32000-1 §7.5.6)." + "summary": "Metadata fields applied by PdfModifier.updateMetadata." }, { "name": "PdfModifier", @@ -940,14 +940,14 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfParams", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Parameters for PDF generation." }, { "name": "PdfPasswordError", "kind": "value", "module": "src/parser/pdf-decrypt.ts", "signature": "export class PdfPasswordError extends Error", - "summary": "pdfnative — Standard Security Handler reader/decryptor ========================================================= Password authentication and transparent object decryption for encrypted PDFs (ISO 32000-1 §7.6 / ISO 32000-2 §7.6), closing the" + "summary": "Thrown when a password is missing or rejected." }, { "name": "PdfReader", @@ -968,21 +968,21 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfRgbString = `${number} ${number} ${number}`;", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "PDF RGB color string in operator format: \"R G B\" (values 0.0–1.0)." }, { "name": "PdfRgbTuple", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type PdfRgbTuple = readonly [r: number, g: number, b: number];", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "RGB color as a 3-tuple of values 0–255." }, { "name": "PdfRow", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PdfRow", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A single row in the PDF table." }, { "name": "PdfSignOptions", @@ -996,21 +996,21 @@ "kind": "type", "module": "src/core/pdf-sign-timestamp.ts", "signature": "export interface PdfSignTimestampOptions extends PdfSignOptions", - "summary": "pdfnative — Timestamped PDF Signing (PAdES B-T, v1.7.0) ======================================================== `signPdfBytesWithTimestamp` signs exactly like signPdfBytes and then obtains an RFC 3161 timestamp over the CMS signature value" + "summary": "Options for signPdfBytesWithTimestamp." }, { "name": "PdfSignatureInfo", "kind": "type", "module": "src/core/pdf-sig-utils.ts", "signature": "export interface PdfSignatureInfo", - "summary": "pdfnative — Signature Enumeration Utilities (v1.7.0) ===================================================== Read-only inspection of the signature fields in a PDF: which signatures exist, their `/SubFilter`, their `/ByteRange`, the raw `/Cont" + "summary": "One signature field entry, in AcroForm /Fields order." }, { "name": "PdfSourceInput", "kind": "type", "module": "src/parser/pdf-pagetree.ts", "signature": "export type PdfSourceInput = Uint8Array | { readonly bytes: Uint8Array; readonly password?: string };", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "A merge source: raw PDF bytes, or bytes paired with the password needed to decrypt an encrypted source." }, { "name": "PdfStream", @@ -1038,35 +1038,35 @@ "kind": "type", "module": "src/parser/pdf-ua-validator.ts", "signature": "export interface PdfUAValidationResult", - "summary": "pdfnative — PDF/UA structural validator (ISO 14289-1) ====================================================== A lightweight, read-only conformance checker for tagged-PDF (PDF/UA) structural prerequisites." + "summary": "Result of a PDF/UA structural validation." }, { "name": "PdfValue", "kind": "type", "module": "src/parser/pdf-object-parser.ts", "signature": "export type PdfValue =", - "summary": "pdfnative — PDF Object Parser ================================ Parses PDF indirect objects from a tokenized stream (ISO 32000-1 §7.3)." + "summary": "Union of all PDF object types (ISO 32000-1 §7.3)." }, { "name": "PrintOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PrintOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Professional print-production options (`layout.print`, v1.7.0): page geometry boxes (ISO 32000-1 §14.11.2), printer's marks (§14.11.3) and large-format `/UserUnit`." }, { "name": "PrinterMarksOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface PrinterMarksOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Printer's-marks options for PrintOptions.marks." }, { "name": "QRErrorLevel", "kind": "type", "module": "src/core/pdf-barcode.ts", "signature": "export type QRErrorLevel = 'L' | 'M' | 'Q' | 'H';", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "QR Code error correction levels." }, { "name": "ROW_H", @@ -1080,70 +1080,70 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface RadialGradientPaint", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A radial gradient fill (COLR PaintRadialGradient → PDF Shading 3)." }, { "name": "RadioGroupContext", "kind": "type", "module": "src/core/pdf-form.ts", "signature": "export interface RadioGroupContext", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Context for building a radio button child widget within a group." }, { "name": "ResolvedPrintBoxes", "kind": "type", "module": "src/core/pdf-print.ts", "signature": "export interface ResolvedPrintBoxes", - "summary": "pdfnative — Print Production (v1.7.0) ====================================== Professional-printing page geometry and decoration: - Page boxes — `/TrimBox`, `/BleedBox`, `/ArtBox`, `/CropBox` (ISO 32000-1 §14.11.2) plus large-format `/UserUn" + "summary": "Resolved print geometry: the page-dict fragment and the trim rectangle." }, { "name": "RevocationProvider", "kind": "type", "module": "src/crypto/revocation-provider.ts", "signature": "export interface RevocationProvider", - "summary": "pdfnative — Pluggable OCSP/CRL Revocation Provider =================================================== pdfnative builds OCSP requests and parses OCSP/CRL responses itself (ocsp.ts / crl.ts) but **never touches the network**: the HTTP round-" + "summary": "A pluggable revocation-data transport." }, { "name": "RsaDigest", "kind": "type", "module": "src/crypto/rsa.ts", "signature": "export type RsaDigest = 'sha256' | 'sha384' | 'sha512';", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Digest algorithm selector for the RSA PKCS#1 v1.5 entry points." }, { "name": "RsaPrivateKey", "kind": "type", "module": "src/crypto/rsa.ts", "signature": "export interface RsaPrivateKey", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "RSA private key (CRT form for efficiency)." }, { "name": "RsaPublicKey", "kind": "type", "module": "src/crypto/rsa.ts", "signature": "export interface RsaPublicKey", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "RSA public key." }, { "name": "ShapeAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface ShapeAnnotation extends AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Rectangle / ellipse drawing annotation." }, { "name": "ShapedGlyph", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ShapedGlyph", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A single positioned glyph output from the Thai shaper." }, { "name": "SigDictMetadata", "kind": "type", "module": "src/core/pdf-signature.ts", "signature": "export interface SigDictMetadata", - "summary": "pdfnative — PDF Digital Signature Support ========================================== ISO 32000-1 §12.8 compliant PDF signing." + "summary": "Metadata-only subset of PdfSignOptions used by buildSigDict and addSignaturePlaceholder." }, { "name": "SignatureAlgorithm", @@ -1157,63 +1157,63 @@ "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface SolidPaint", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A flat colour fill (COLR PaintSolid / COLRv0 layer)." }, { "name": "SpacerBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface SpacerBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Spacer block — vertical whitespace." }, { "name": "SplitPdfStream", "kind": "type", "module": "src/parser/pdf-pagetree.ts", "signature": "export interface SplitPdfStream", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "One emitted document from streamSplitPdf." }, { "name": "StreamMergeOptions", "kind": "type", "module": "src/parser/pdf-pagetree.ts", "signature": "export interface StreamMergeOptions extends MergeOptions", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Options for the streaming page-tree variants." }, { "name": "StreamOptions", "kind": "type", "module": "src/core/pdf-stream-writer.ts", "signature": "export interface StreamOptions", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Options for streaming PDF output." }, { "name": "StreamToFileResult", "kind": "type", "module": "src/core/pdf-stream-writer.ts", "signature": "export interface StreamToFileResult", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Result of streamToFile." }, { "name": "SvgBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface SvgBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "SVG block — renders vector graphics via PDF path operators." }, { "name": "SvgRenderOptions", "kind": "type", "module": "src/core/pdf-svg.ts", "signature": "export interface SvgRenderOptions", - "summary": "pdfnative — SVG Path Rendering ================================ Parses SVG path data and basic SVG elements, renders them as PDF path operators using coordinate matrix transformation." + "summary": "Options for SVG rendering to PDF operators." }, { "name": "SvgSegment", "kind": "type", "module": "src/core/pdf-svg.ts", "signature": "export interface SvgSegment", - "summary": "pdfnative — SVG Path Rendering ================================ Parses SVG path data and basic SVG elements, renders them as PDF path operators using coordinate matrix transformation." + "summary": "Parsed SVG path segment in absolute coordinates (M, L, C, Z only)." }, { "name": "TH_H", @@ -1234,56 +1234,56 @@ "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface TableBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Table block — reuses existing PdfRow/ColumnDef types." }, { "name": "TextAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface TextAnnotation extends AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Sticky-note text annotation (`/Subtype /Text`)." }, { "name": "TextMarkupAnnotation", "kind": "type", "module": "src/core/pdf-annot-markup.ts", "signature": "export interface TextMarkupAnnotation extends AnnotationBase", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Text-markup annotation (highlight / underline / strike-out / squiggly)." }, { "name": "TextRun", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface TextRun", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "A text run produced by the encoding context's textRuns() method." }, { "name": "TimestampProvider", "kind": "type", "module": "src/crypto/timestamp-provider.ts", "signature": "export interface TimestampProvider", - "summary": "pdfnative — Pluggable RFC 3161 Timestamp Provider ================================================== pdfnative builds and parses RFC 3161 structures itself (rfc3161.ts) but **never touches the network**: the single HTTP round-trip to a Time" + "summary": "A pluggable RFC 3161 transport: sends a DER `TimeStampReq` to a TSA and resolves with the DER `TimeStampResp`." }, { "name": "TimestampRequestOptions", "kind": "type", "module": "src/crypto/rfc3161.ts", "signature": "export interface TimestampRequestOptions", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Options for buildTimestampRequest." }, { "name": "TimestampResponse", "kind": "type", "module": "src/crypto/rfc3161.ts", "signature": "export interface TimestampResponse", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Parsed TimeStampResp (RFC 3161 §2.4.2)." }, { "name": "TocBlock", "kind": "type", "module": "src/types/pdf-document-types.ts", "signature": "export interface TocBlock", - "summary": "pdfnative — Document Content Model Types ========================================== Types for the free-form document builder API (Phase 4)." + "summary": "Table of Contents block — auto-generated from heading blocks." }, { "name": "TokenType", @@ -1297,98 +1297,98 @@ "kind": "type", "module": "src/crypto/rfc3161.ts", "signature": "export interface TstInfo", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Parsed TSTInfo from a TimeStampToken (RFC 3161 §2.4.2)." }, { "name": "UseCategory", "kind": "type", "module": "src/shaping/use-lite.ts", "signature": "export type UseCategory =", - "summary": "pdfnative — Universal Shaping Engine (USE) lite ================================================= Cluster classification utility for Indic and related complex scripts (Devanagari, Bengali, Tamil, Gujarati, Gurmukhi, Telugu, Kannada, Malayal" + "summary": "USE-lite cluster categories." }, { "name": "UseClassifiedCp", "kind": "type", "module": "src/shaping/use-lite.ts", "signature": "export interface UseClassifiedCp", - "summary": "pdfnative — Universal Shaping Engine (USE) lite ================================================= Cluster classification utility for Indic and related complex scripts (Devanagari, Bengali, Tamil, Gujarati, Gurmukhi, Telugu, Kannada, Malayal" + "summary": "Classified code point with its USE-lite category." }, { "name": "UseCluster", "kind": "type", "module": "src/shaping/use-lite.ts", "signature": "export interface UseCluster", - "summary": "pdfnative — Universal Shaping Engine (USE) lite ================================================= Cluster classification utility for Indic and related complex scripts (Devanagari, Bengali, Tamil, Gujarati, Gurmukhi, Telugu, Kannada, Malayal" + "summary": "A USE-lite cluster: a base plus its prefixed/suffixed marks and signs." }, { "name": "ViewerPreferences", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface ViewerPreferences", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Viewer presentation preferences (ISO 32000-1 §12.2, Table 150 + §7.7.2)." }, { "name": "WORKER_THRESHOLD", "kind": "value", "module": "src/worker/worker-api.ts", "signature": "export const WORKER_THRESHOLD = 500;", - "summary": "pdfnative — Worker API ======================== High-level API for generating PDFs using Web Workers or main thread fallback." + "summary": "Default threshold: use Worker for datasets above this row count." }, { "name": "WORKER_TIMEOUT_MS", "kind": "value", "module": "src/worker/worker-api.ts", "signature": "export const WORKER_TIMEOUT_MS = 60000;", - "summary": "pdfnative — Worker API ======================== High-level API for generating PDFs using Web Workers or main thread fallback." + "summary": "Worker timeout in milliseconds (used when options.timeout is not supplied)." }, { "name": "WatermarkImage", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface WatermarkImage", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Image watermark configuration." }, { "name": "WatermarkOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface WatermarkOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Watermark options for PDF pages." }, { "name": "WatermarkState", "kind": "type", "module": "src/core/pdf-watermark.ts", "signature": "export interface WatermarkState", - "summary": "pdfnative — PDF Watermark Rendering ====================================== Builds PDF content operators for text and image watermarks." + "summary": "Resolved watermark state for use during PDF assembly." }, { "name": "WatermarkText", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface WatermarkText", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Text watermark configuration." }, { "name": "WorkerGenerationOptions", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface WorkerGenerationOptions", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Options for generating a PDF in a Web Worker via `generatePDFInWorker()`." }, { "name": "WorkerInputMessage", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export interface WorkerInputMessage", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Message sent to the PDF Worker." }, { "name": "WorkerOutputMessage", "kind": "type", "module": "src/types/pdf-types.ts", "signature": "export type WorkerOutputMessage =", - "summary": "pdfnative — Public Type Definitions ==================================== All types exported by the library for consumers." + "summary": "Messages received from the PDF Worker." }, { "name": "X509Certificate", @@ -1423,553 +1423,553 @@ "kind": "value", "module": "src/core/pdf-doc-timestamp.ts", "signature": "export async function addDocumentTimestamp( pdfBytes: Uint8Array, options: AddDocumentTimestampOptions = {}, ): Promise", - "summary": "pdfnative — Document Timestamps (PAdES B-LTA, v1.7.0) ====================================================== `addDocumentTimestamp` appends a document-timestamp revision per ISO 32000-2 §12.8.5: a signature field whose dictionary is `/Type " + "summary": "Append a document-timestamp revision covering every byte of the current document (all earlier revisions stay byte-identical)." }, { "name": "addSignaturePlaceholder", "kind": "value", "module": "src/core/pdf-sig-placeholder.ts", "signature": "export function addSignaturePlaceholder( pdfBytes: Uint8Array, options: AddSignaturePlaceholderOptions = {}, ): Uint8Array", - "summary": "pdfnative — Signature Placeholder Injector ============================================ Inject an AcroForm + invisible signature widget placeholder into an existing PDF via incremental update (ISO 32000-1 §7.5.6, §12.7.4.5, §12.8)." + "summary": "Inject an AcroForm + signature widget placeholder into an existing PDF via incremental update." }, { "name": "addUnsignedAttribute", "kind": "value", "module": "src/crypto/cms-utils.ts", "signature": "export function addUnsignedAttribute(cmsDer: Uint8Array, attributeTlv: Uint8Array): Uint8Array", - "summary": "pdfnative — CMS SignedData Parsing & DER Surgery (RFC 5652) ============================================================ Parses CMS/PKCS#7 SignedData structures (the inverse of cms.ts) and performs the one mutation LTV workflows need: appen" + "summary": "Append an attribute to the `[1] IMPLICIT unsignedAttrs` slot of the first SignerInfo in a CMS SignedData, creating the slot when absent." }, { "name": "addValidationInfo", "kind": "value", "module": "src/core/pdf-dss.ts", "signature": "export async function addValidationInfo( pdfBytes: Uint8Array, options: CollectLtvOptions = {}, ): Promise", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Convenience: collectValidationInfo + embedValidationInfo." }, { "name": "applyDecodeFilter", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export function applyDecodeFilter(name: string, data: Uint8Array): Uint8Array", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Apply a single PDF stream filter by name." }, { "name": "buildAcroFormDict", "kind": "value", "module": "src/core/pdf-form.ts", "signature": "export function buildAcroFormDict(fieldObjNums: readonly number[], fontObjNum?: number): string", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Build the /AcroForm dictionary content for the catalog." }, { "name": "buildAnnotation", "kind": "value", "module": "src/core/pdf-annot-markup.ts", "signature": "export function buildAnnotation(annot: MarkupAnnotation, objNum: number): string", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Build a markup / drawing annotation as a PDF indirect object." }, { "name": "buildAnnotationBody", "kind": "value", "module": "src/core/pdf-annot-markup.ts", "signature": "export function buildAnnotationBody(annot: MarkupAnnotation): string", - "summary": "pdfnative — Markup & Drawing Annotations (ISO 32000-1 §12.5) ============================================================ Typed builders for the common non-link annotation types: text (sticky note), the text-markup family (highlight / under" + "summary": "Build just the annotation dictionary (`<< … >>`), without the `obj`/`endobj` wrapper." }, { "name": "buildAppearanceStreamDict", "kind": "value", "module": "src/core/pdf-form.ts", "signature": "export function buildAppearanceStreamDict(w: number, h: number, streamLength: number, fontObjNum?: number): string", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Build the Form XObject dictionary header for an appearance stream." }, { "name": "buildAttribute", "kind": "value", "module": "src/crypto/cms-utils.ts", "signature": "export function buildAttribute(oidBytes: Uint8Array, ...values: Uint8Array[]): Uint8Array", - "summary": "pdfnative — CMS SignedData Parsing & DER Surgery (RFC 5652) ============================================================ Parses CMS/PKCS#7 SignedData structures (the inverse of cms.ts) and performs the one mutation LTV workflows need: appen" + "summary": "Build a CMS Attribute TLV: `SEQUENCE { attrType OID, attrValues SET OF }`." }, { "name": "buildCmsSignedData", "kind": "value", "module": "src/crypto/cms.ts", "signature": "export function buildCmsSignedData(options: CmsSignOptions): Uint8Array", - "summary": "pdfnative — CMS SignedData Builder (RFC 5652) ============================================== Builds CMS/PKCS#7 SignedData structures for PDF digital signatures." + "summary": "Build a CMS SignedData structure for a PDF detached signature." }, { "name": "buildDocTimeStampDict", "kind": "value", "module": "src/core/pdf-signature.ts", "signature": "export function buildDocTimeStampDict(contentsSize: number = DEFAULT_CONTENTS_SIZE): string", - "summary": "pdfnative — PDF Digital Signature Support ========================================== ISO 32000-1 §12.8 compliant PDF signing." + "summary": "Build a `/DocTimeStamp` dictionary string (ISO 32000-2 §12.8.5) with the same byte-patchable /Contents + /ByteRange placeholders as buildSigDict." }, { "name": "buildDocumentPDF", "kind": "value", "module": "src/core/pdf-document.ts", "signature": "export function buildDocumentPDF(params: DocumentParams, layoutOptions?: Partial): string", - "summary": "pdfnative — Free-form Document Builder ======================================== Generates PDF 1.4 documents from a sequence of content blocks (headings, paragraphs, lists, tables, spacers, page breaks)." + "summary": "Build a free-form PDF document from content blocks." }, { "name": "buildDocumentPDFBytes", "kind": "value", "module": "src/core/pdf-document.ts", "signature": "export function buildDocumentPDFBytes(params: DocumentParams, layoutOptions?: Partial): Uint8Array", - "summary": "pdfnative — Free-form Document Builder ======================================== Generates PDF 1.4 documents from a sequence of content blocks (headings, paragraphs, lists, tables, spacers, page breaks)." + "summary": "Build a free-form PDF document and return as Uint8Array." }, { "name": "buildDocumentPDFStream", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildDocumentPDFStream( params: DocumentParams, layoutOptions?: Partial, streamOptions?: StreamOptions, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a free-form PDF document and yield Uint8Array chunks progressively." }, { "name": "buildDocumentPDFStreamPageByPage", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildDocumentPDFStreamPageByPage( params: DocumentParams, layoutOptions?: Partial, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a free-form PDF document and yield Uint8Array chunks aligned at PDF object boundaries (one indirect object per chunk, plus a header chunk and a trailing xref/trailer chunk)." }, { "name": "buildDocumentPDFStreamTrue", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildDocumentPDFStreamTrue( params: DocumentParams, layoutOptions?: Partial, streamOptions?: StreamOptions, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a free-form PDF document, streaming it out **without ever joining the binary**." }, { "name": "buildEmbeddedFiles", "kind": "value", "module": "src/core/pdf-tags.ts", "signature": "export function buildEmbeddedFiles(attachments: readonly PdfAttachment[], startObjNum: number): EmbeddedFilesResult", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Build PDF objects for embedded file attachments (PDF/A-3)." }, { "name": "buildFormWidget", "kind": "value", "module": "src/core/pdf-form.ts", "signature": "export function buildFormWidget(field: FormField, apObjNum: number, radioCtx?: RadioGroupContext): FormWidgetResult", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Build a form field widget annotation dictionary and its appearance stream." }, { "name": "buildImageOperators", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function buildImageOperators( imgRef: string, x: number, y: number, width: number, height: number, ): string", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Build the content stream operators to paint an image at the given position and size." }, { "name": "buildImageXObject", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function buildImageXObject(img: ParsedImage, smaskObjNum?: number): string", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Build a PDF Image XObject dictionary + stream for an image." }, { "name": "buildInternalLinkAnnotation", "kind": "value", "module": "src/core/pdf-annot.ts", "signature": "export function buildInternalLinkAnnotation(annot: InternalLink, pageObjNum: number, objNum: number): string", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Build a PDF link annotation object for an internal page link (GoTo action)." }, { "name": "buildLinkAnnotation", "kind": "value", "module": "src/core/pdf-annot.ts", "signature": "export function buildLinkAnnotation(annot: LinkAnnotation, objNum: number): string", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Build a PDF link annotation object for an external URI." }, { "name": "buildOcspRequest", "kind": "value", "module": "src/crypto/ocsp.ts", "signature": "export function buildOcspRequest(cert: X509Certificate, issuer: X509Certificate, options?: OcspRequestOptions): Uint8Array", - "summary": "pdfnative — OCSP Request Builder / Response Parser (RFC 6960) ============================================================== Builds unsigned OCSPRequest DER and parses OCSPResponse / BasicOCSPResponse for LTV revocation data collection (PAd" + "summary": "Build an unsigned DER OCSPRequest (RFC 6960 §4.1.1) for one certificate: ``` CertID ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier (SHA-1), issuerNameHash OCTET STRING, -- SHA-1 of issuer Name DER issuerKeyHash OCTET STRING, -- SHA-1 " }, { "name": "buildPDF", "kind": "value", "module": "src/core/pdf-builder.ts", "signature": "export function buildPDF(params: PdfParams, layoutOptions?: Partial): string", - "summary": "pdfnative — PDF Document Builder =================================== Main entry point: builds a complete PDF 1.4 document as a byte string." + "summary": "Build a complete PDF document as a single-byte string." }, { "name": "buildPDFBytes", "kind": "value", "module": "src/core/pdf-builder.ts", "signature": "export function buildPDFBytes(params: PdfParams, layoutOptions?: Partial): Uint8Array", - "summary": "pdfnative — PDF Document Builder =================================== Main entry point: builds a complete PDF 1.4 document as a byte string." + "summary": "Build a PDF and return it as a Uint8Array (ready for download or Blob)." }, { "name": "buildPDFStream", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildPDFStream( params: PdfParams, layoutOptions?: Partial, streamOptions?: StreamOptions, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a table-centric PDF and yield Uint8Array chunks progressively." }, { "name": "buildPDFStreamPageByPage", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildPDFStreamPageByPage( params: PdfParams, layoutOptions?: Partial, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a table-centric PDF and yield Uint8Array chunks aligned at PDF object boundaries." }, { "name": "buildPDFStreamTrue", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function* buildPDFStreamTrue( params: PdfParams, layoutOptions?: Partial, streamOptions?: StreamOptions, ): AsyncGenerator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Build a table-centric PDF, streaming it out **without ever joining the binary**." }, { "name": "buildPrinterMarksOps", "kind": "value", "module": "src/core/pdf-print.ts", "signature": "export function buildPrinterMarksOps( trim: PageBox, pgW: number, pgH: number, marks: boolean | PrinterMarksOptions, ): string", - "summary": "pdfnative — Print Production (v1.7.0) ====================================== Professional-printing page geometry and decoration: - Page boxes — `/TrimBox`, `/BleedBox`, `/ArtBox`, `/CropBox` (ISO 32000-1 §14.11.2) plus large-format `/UserUn" + "summary": "Build the printer's-marks operator block for one page geometry." }, { "name": "buildRadioGroupParent", "kind": "value", "module": "src/core/pdf-form.ts", "signature": "export function buildRadioGroupParent( name: string, selectedValue: string, childObjNums: readonly number[], readOnly: boolean, required: boolean, ): string", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Build the parent field dictionary for a radio button group (ISO 32000-1 §12.7.4.2.4)." }, { "name": "buildSMaskXObject", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function buildSMaskXObject(smaskData: string, width: number, height: number): string", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Build a PDF SMask (soft mask) XObject for PNG alpha channel." }, { "name": "buildSigDict", "kind": "value", "module": "src/core/pdf-signature.ts", "signature": "export function buildSigDict(options: SigDictMetadata, contentsSize: number = DEFAULT_CONTENTS_SIZE): string", - "summary": "pdfnative — PDF Digital Signature Support ========================================== ISO 32000-1 §12.8 compliant PDF signing." + "summary": "Build a /Sig signature dictionary string for embedding in a PDF." }, { "name": "buildTimestampRequest", "kind": "value", "module": "src/crypto/rfc3161.ts", "signature": "export function buildTimestampRequest(messageImprint: Uint8Array, options?: TimestampRequestOptions): Uint8Array", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Build a DER-encoded TimeStampReq (RFC 3161 §2.4.1, version 1): ``` TimeStampReq ::= SEQUENCE { version INTEGER { v1(1) }, messageImprint MessageImprint, reqPolicy TSAPolicyId OPTIONAL, nonce INTEGER " }, { "name": "buildWatermarkState", "kind": "value", "module": "src/core/pdf-watermark.ts", "signature": "export function buildWatermarkState( watermark: WatermarkOptions, pgW: number, pgH: number, enc: EncodingContext, ): WatermarkState", - "summary": "pdfnative — PDF Watermark Rendering ====================================== Builds PDF content operators for text and image watermarks." + "summary": "Build watermark content operators and collect resources." }, { "name": "certHasEku", "kind": "value", "module": "src/crypto/x509.ts", "signature": "export function certHasEku(cert: X509Certificate, oidBytes: Uint8Array): boolean", - "summary": "pdfnative — X.509 Certificate Parser ===================================== Parses X.509 v3 certificates (RFC 5280) from DER-encoded bytes." + "summary": "Check whether a certificate carries a given Extended Key Usage purpose (2.5.29.37), e.g." }, { "name": "chunkBinaryString", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export function* chunkBinaryString(str: string, chunkSize: number): Generator", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Convert a binary string to Uint8Array chunks without allocating the full Uint8Array at once." }, { "name": "classifyClusters", "kind": "value", "module": "src/shaping/use-lite.ts", "signature": "export function classifyClusters(codePoints: readonly number[]): UseCluster[]", - "summary": "pdfnative — Universal Shaping Engine (USE) lite ================================================= Cluster classification utility for Indic and related complex scripts (Devanagari, Bengali, Tamil, Gujarati, Gurmukhi, Telugu, Kannada, Malayal" + "summary": "Split a code-point sequence into USE-lite clusters." }, { "name": "classifyUseCategory", "kind": "value", "module": "src/shaping/use-lite.ts", "signature": "export function classifyUseCategory(cp: number): UseCategory", - "summary": "pdfnative — Universal Shaping Engine (USE) lite ================================================= Cluster classification utility for Indic and related complex scripts (Devanagari, Bengali, Tamil, Gujarati, Gurmukhi, Telugu, Kannada, Malayal" + "summary": "Classify a single Unicode code point into a USE-lite category." }, { "name": "clearFontCache", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function clearFontCache(): void", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Clear font cache (useful for testing)." }, { "name": "collectValidationInfo", "kind": "value", "module": "src/core/pdf-dss.ts", "signature": "export async function collectValidationInfo( pdfBytes: Uint8Array, options: CollectLtvOptions = {}, ): Promise", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Collect validation material for every signed signature in the document." }, { "name": "computeColumnPositions", "kind": "value", "module": "src/core/pdf-layout.ts", "signature": "export function computeColumnPositions( columns: readonly ColumnDef[], marginLeft: number, contentWidth: number ):", - "summary": "pdfnative — PDF Layout Constants ================================== A4 page dimensions and default layout settings." + "summary": "Compute column X positions and widths given columns and content width." }, { "name": "concatChunks", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export function concatChunks(chunks: readonly Uint8Array[]): Uint8Array", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Concatenate an array of Uint8Array chunks into a single Uint8Array." }, { "name": "containsArabic", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsArabic(text: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if text contains Arabic characters requiring shaping." }, { "name": "containsBengali", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsBengali(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Bengali characters." }, { "name": "containsDevanagari", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsDevanagari(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Devanagari characters." }, { "name": "containsEthiopic", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsEthiopic(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Ethiopic characters." }, { "name": "containsHebrew", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsHebrew(text: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if text contains Hebrew characters." }, { "name": "containsKhmer", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsKhmer(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Khmer characters." }, { "name": "containsMath", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsMath(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any mathematical symbols." }, { "name": "containsMyanmar", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsMyanmar(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Myanmar characters." }, { "name": "containsRTL", "kind": "value", "module": "src/shaping/bidi.ts", "signature": "export function containsRTL(text: string): boolean", - "summary": "pdfnative — Simplified Unicode Bidirectional Algorithm ======================================================= Pure JS implementation of a subset of UAX #9 (Unicode Bidirectional Algorithm)." + "summary": "Check if text contains any RTL characters (Arabic or Hebrew) or an explicit directional override (LRO U+202D / RLO U+202E)." }, { "name": "containsSinhala", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsSinhala(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Sinhala characters." }, { "name": "containsTamil", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsTamil(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Tamil characters." }, { "name": "containsTelugu", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsTelugu(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Telugu characters." }, { "name": "containsThai", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsThai(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Thai characters." }, { "name": "containsTibetan", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function containsTibetan(str: string): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check whether a string contains any Tibetan characters." }, { "name": "contoursToPath", "kind": "value", "module": "src/core/pdf-color-glyph.ts", "signature": "export function contoursToPath(contours: Contour[], m: Mat = ID): string", - "summary": "pdfnative — Colour Glyph Renderer (COLR/CPAL → native PDF) ========================================================== Renders a resolved ColorGlyph as a native PDF Form XObject: each layer's base-glyph outline is emitted as a vector path an" + "summary": "Convert a set of TrueType quadratic contours into PDF path operators (cubic Béziers), applying matrix `m`." }, { "name": "createEncodingContext", "kind": "value", "module": "src/core/encoding-context.ts", "signature": "export function createEncodingContext(fontEntries: FontEntry[], pdfA: boolean = false, normalize: 'NFC' | 'NFD' | 'NFKC' | 'NFKD' | false = false): EncodingContext", - "summary": "pdfnative — Encoding Context Factory ====================================== Creates encoding contexts that bridge font encoding (WinAnsi/CIDFont) with text shaping (Thai, Arabic, BiDi, multi-font fallback)." + "summary": "Create an encoding context that encapsulates text encoding and font reference logic." }, { "name": "createModifier", "kind": "value", "module": "src/parser/pdf-modifier.ts", "signature": "export function createModifier(reader: PdfReader): PdfModifier", - "summary": "pdfnative — PDF Document Modifier ==================================== Modify existing PDF documents using incremental save (ISO 32000-1 §7.5.6)." + "summary": "Create a modifier for an existing PDF document." }, { "name": "createPDF", "kind": "value", "module": "src/worker/worker-api.ts", "signature": "export async function createPDF( pdfParams: PdfParams, options?: { workerUrl?: string | URL; threshold?: number; timeout?: number; onProgress?: (percent: number) => void", - "summary": "pdfnative — Worker API ======================== High-level API for generating PDFs using Web Workers or main thread fallback." + "summary": "Smart PDF generation: uses Worker for large datasets, main thread for small ones." }, { "name": "createTokenizer", "kind": "value", "module": "src/parser/pdf-tokenizer.ts", "signature": "export function createTokenizer(buf: Uint8Array, startPos: number = 0): PdfTokenizer", - "summary": "pdfnative — PDF Token Scanner =============================== Scans PDF byte stream into typed tokens (ISO 32000-1 §7.2)." + "summary": "Create a PDF tokenizer for the given byte buffer." }, { "name": "decodeASCII85", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export function decodeASCII85(data: Uint8Array): Uint8Array", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Decode an ASCII85Decode stream (Adobe variant)." }, { "name": "decodeASCIIHex", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export function decodeASCIIHex(data: Uint8Array): Uint8Array", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Decode an ASCIIHexDecode stream." }, { "name": "decodeEcPublicKey", "kind": "value", "module": "src/crypto/ecdsa.ts", "signature": "export function decodeEcPublicKey(bytes: Uint8Array): EcPublicKey", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "Decode an uncompressed EC public key (65 bytes: 0x04 || x || y)." }, { "name": "decodeLZW", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export function decodeLZW(data: Uint8Array): Uint8Array", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Decode an LZWDecode stream with variable-width codes (9–12 bits) and automatic table reset on the CLEAR code (256)." }, { "name": "decodeRunLength", "kind": "value", "module": "src/parser/pdf-decode-filters.ts", "signature": "export function decodeRunLength(data: Uint8Array): Uint8Array", - "summary": "pdfnative — PDF Stream Filter Decoders ======================================== Pure, zero-dependency decoders for the standard PDF stream filters that are not handled by inflateSync (FlateDecode)." + "summary": "Decode a RunLengthDecode stream." }, { "name": "defaultFieldHeight", "kind": "value", "module": "src/core/pdf-form.ts", "signature": "export function defaultFieldHeight(fieldType: FormFieldType): number", - "summary": "pdfnative — AcroForm Interactive Fields (ISO 32000-1 §12.7) ============================================================= Builds AcroForm dictionaries, field widgets, and appearance streams." + "summary": "Get default widget height for a given field type." }, { "name": "derBitString", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derBitString(data: Uint8Array): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode a BIT STRING (with 0 unused bits)." }, { "name": "derDecode", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derDecode(buf: Uint8Array): Asn1Node", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Parse a DER-encoded ASN.1 structure." }, { "name": "derGeneralizedTime", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derGeneralizedTime(date: Date): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode a GeneralizedTime from a Date." }, { "name": "derInteger", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derInteger(value: bigint | Uint8Array): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode an INTEGER from a BigInt or Uint8Array (big-endian)." }, { "name": "derOctetString", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derOctetString(data: Uint8Array): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode an OCTET STRING." }, { "name": "derOid", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derOid(oidBytes: Uint8Array): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode an OID from byte values (already encoded arc values)." }, { "name": "derSequence", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derSequence(...children: Uint8Array[]): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode a SEQUENCE (0x30) from child TLVs." }, { "name": "derSetOf", "kind": "value", "module": "src/crypto/asn1.ts", "signature": "export function derSetOf(...children: Uint8Array[]): Uint8Array", - "summary": "pdfnative — ASN.1 DER Encoder / Decoder ======================================== Minimal DER (Distinguished Encoding Rules) codec for X.509, CMS, and PKCS." + "summary": "Encode a DER SET OF (0x31) from child TLVs with the canonical X.690 §11.6 ordering: the encodings are sorted as octet strings in ascending lexicographic order, and when one encoding is a prefix of another the shorter one sorts first." }, { "name": "detectCharLang", "kind": "value", "module": "src/shaping/script-detect.ts", "signature": "export function detectCharLang(cp: number): string | null", - "summary": "pdfnative — Script Detection ============================= Detects Unicode script ranges in text to determine which fonts are needed." + "summary": "Detect the preferred font language for a single Unicode codepoint." }, { "name": "detectFallbackLangs", "kind": "value", "module": "src/shaping/script-detect.ts", "signature": "export function detectFallbackLangs(texts: string[], primaryLang: string): Set", - "summary": "pdfnative — Script Detection ============================= Detects Unicode script ranges in text to determine which fonts are needed." + "summary": "Detect which additional font languages are needed to render user text containing scripts foreign to the primary language." }, { "name": "detectImageFormat", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function detectImageFormat(bytes: Uint8Array): 'jpeg' | 'png' | null", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Detect the image format from magic bytes." }, { "name": "dictGet", @@ -2018,196 +2018,196 @@ "kind": "value", "module": "src/core/pdf-stream.ts", "signature": "export function downloadBlob(bytes: Uint8Array, filename: string): void", - "summary": "pdfnative — PDF Stream Utilities ================================== Binary conversion and PDF byte-level helpers." + "summary": "Trigger a file download in the browser via a temporary element." }, { "name": "ean13CheckDigit", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function ean13CheckDigit(digits12: string): number", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Calculate EAN-13 check digit." }, { "name": "ecPublicKeyFromPrivate", "kind": "value", "module": "src/crypto/ecdsa.ts", "signature": "export function ecPublicKeyFromPrivate(privateKey: EcPrivateKey): EcPublicKey", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "Derive the public key from a private key." }, { "name": "ecdsaSign", "kind": "value", "module": "src/crypto/ecdsa.ts", "signature": "export function ecdsaSign(message: Uint8Array, privateKey: EcPrivateKey): { r: bigint; s: bigint }", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "Sign a message with ECDSA P-256 + SHA-256, using RFC 6979 deterministic k." }, { "name": "ecdsaVerify", "kind": "value", "module": "src/crypto/ecdsa.ts", "signature": "export function ecdsaVerify(message: Uint8Array, r: bigint, s: bigint, publicKey: EcPublicKey): boolean", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "Verify an ECDSA P-256 + SHA-256 signature." }, { "name": "embedValidationInfo", "kind": "value", "module": "src/core/pdf-dss.ts", "signature": "export function embedValidationInfo(pdfBytes: Uint8Array, data: LtvData): Uint8Array", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Embed pre-collected LtvData as a `/DSS` dictionary via incremental update." }, { "name": "encodeCode128", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function encodeCode128(data: string): number[]", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Encode a string as Code 128 symbol values." }, { "name": "encodeEcPublicKey", "kind": "value", "module": "src/crypto/ecdsa.ts", "signature": "export function encodeEcPublicKey(key: EcPublicKey): Uint8Array", - "summary": "pdfnative — ECDSA P-256 Sign / Verify ====================================== Pure BigInt implementation of ECDSA on the NIST P-256 curve (secp256r1)." + "summary": "Encode an ECDSA public key as uncompressed point (65 bytes: 0x04 || x || y)." }, { "name": "encodePDF417", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function encodePDF417(data: string, ecLevel: number = 2): { codewords: number[]; rows: number; cols: number }", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Encode data as PDF417 codewords (text compaction)." }, { "name": "encodePdfTextString", "kind": "value", "module": "src/core/pdf-text.ts", "signature": "export function encodePdfTextString(str: string): string", - "summary": "pdfnative — PDF Text Primitives ================================= Low-level PDF content stream operators for text rendering." + "summary": "Encode a string for the PDF /Info dictionary (ISO 32000-1 §7.9.2)." }, { "name": "estimateChartHeight", "kind": "value", "module": "src/core/pdf-chart.ts", "signature": "export function estimateChartHeight(block: ChartBlock): number", - "summary": "pdfnative — Native vector charts (v1.6.0, extended v1.7.0) ============================================ Renders ChartBlocks as pure PDF path operators — rectangles, line segments and cubic-Bézier arcs — with zero dependencies and no rasteri" + "summary": "Total footprint of a chart block (title + plot + axis labels + legend)." }, { "name": "estimateCmsSize", "kind": "value", "module": "src/crypto/cms.ts", "signature": "export function estimateCmsSize(certSizes: readonly number[], algorithm: SignatureAlgorithm): number", - "summary": "pdfnative — CMS SignedData Builder (RFC 5652) ============================================== Builds CMS/PKCS#7 SignedData structures for PDF digital signatures." + "summary": "Estimate the DER-encoded size of a CMS SignedData for allocation." }, { "name": "estimateContentsSize", "kind": "value", "module": "src/core/pdf-signature.ts", "signature": "export function estimateContentsSize( certSizes: readonly number[], algorithm: SignatureAlgorithm = 'rsa-sha256', options?: { readonly timestamp?: boolean }, ): number", - "summary": "pdfnative — PDF Digital Signature Support ========================================== ISO 32000-1 §12.8 compliant PDF signing." + "summary": "Estimate the allocation size needed for /Contents placeholder." }, { "name": "extractGlyphContours", "kind": "value", "module": "src/fonts/glyf-outline.ts", "signature": "export function extractGlyphContours(font: GlyfFont, gid: number, depth = 0): Contour[]", - "summary": "pdfnative — TrueType `glyf` Outline Extractor ============================================== Pure-JS reader that turns a TrueType glyph outline into ordered contours of quadratic on/off-curve points." + "summary": "Extract the contours of a single glyph in font units." }, { "name": "extractPages", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export function extractPages( src: PdfSourceInput, pageIndices: readonly number[], opts?: MergeOptions, ): Uint8Array", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Extract a subset of pages from a single document into a new PDF." }, { "name": "extractText", "kind": "value", "module": "src/parser/pdf-text-extract.ts", "signature": "export function extractText(bytes: Uint8Array, options?: ExtractTextOptions): ExtractedPageText[]", - "summary": "pdfnative — PDF Text Extraction ================================= Decodes page content streams (ISO 32000-1 §9) into per-page Unicode text with optional positioned runs." + "summary": "Extract Unicode text from a PDF document, page by page." }, { "name": "fillForm", "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export function fillForm( pdfBytes: Uint8Array, values: Record, opts?: FillFormOptions, ): Uint8Array", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "Fill the form fields of an existing PDF and return a new PDF (incremental update — original bytes are preserved, so any existing signature stays valid for its revision)." }, { "name": "findStartxref", "kind": "value", "module": "src/parser/pdf-xref-parser.ts", "signature": "export function findStartxref(buf: Uint8Array): number", - "summary": "pdfnative — PDF Cross-Reference Table Parser =============================================== Parses traditional xref tables and xref streams (ISO 32000-1 §7.5)." + "summary": "Find the startxref offset from the end of the PDF file." }, { "name": "flattenForm", "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export function flattenForm(pdfBytes: Uint8Array, opts?: FlattenFormOptions): Uint8Array", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "Flatten an existing PDF's form: stamp each widget's normal appearance into its page content and remove the interactive fields (`/AcroForm`, widget `/Annots`)." }, { "name": "generateDataMatrix", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function generateDataMatrix(data: string): boolean[][]", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Generate a Data Matrix ECC 200 module matrix." }, { "name": "generatePDFInWorker", "kind": "value", "module": "src/worker/worker-api.ts", "signature": "export function generatePDFInWorker( workerUrl: string | URL, pdfParams: PdfParams, options?: WorkerGenerationOptions ): Promise", - "summary": "pdfnative — Worker API ======================== High-level API for generating PDFs using Web Workers or main thread fallback." + "summary": "Generate PDF in a Web Worker (off-main-thread)." }, { "name": "generatePDFMainThread", "kind": "value", "module": "src/worker/worker-api.ts", "signature": "export function generatePDFMainThread(pdfParams: PdfParams, layoutOptions?: Partial): Uint8Array", - "summary": "pdfnative — Worker API ======================== High-level API for generating PDFs using Web Workers or main thread fallback." + "summary": "Main-thread fallback: generate PDF synchronously." }, { "name": "generateQR", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function generateQR(data: string, ecLevel: QRErrorLevel = 'M'): boolean[][]", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Generate a QR Code module matrix." }, { "name": "getCryptoProvider", "kind": "value", "module": "src/crypto/crypto-provider.ts", "signature": "export function getCryptoProvider(): CryptoProvider | null", - "summary": "pdfnative — Pluggable Signature Crypto Provider ================================================ pdfnative ships its own pure-TypeScript RSA (PKCS#1 v1.5) and ECDSA (P-256) implementations so that signing works in any runtime with **zero de" + "summary": "The currently-installed global CryptoProvider, or `null` if none." }, { "name": "getMaxInflateOutputSize", "kind": "value", "module": "src/parser/pdf-inflate.ts", "signature": "export function getMaxInflateOutputSize(): number", - "summary": "pdfnative — DEFLATE Decompressor (FlateDecode reader) ====================================================== Zero-dependency DEFLATE decompression for reading FlateDecode streams." + "summary": "Get the current maximum decompressed output size." }, { "name": "getRegisteredLangs", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function getRegisteredLangs(): string[]", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Get all registered language codes." }, { "name": "getRevocationProvider", "kind": "value", "module": "src/crypto/revocation-provider.ts", "signature": "export function getRevocationProvider(): RevocationProvider | null", - "summary": "pdfnative — Pluggable OCSP/CRL Revocation Provider =================================================== pdfnative builds OCSP requests and parses OCSP/CRL responses itself (ocsp.ts / crl.ts) but **never touches the network**: the HTTP round-" + "summary": "The currently-installed global RevocationProvider, or `null` if none." }, { "name": "getTimestampProvider", "kind": "value", "module": "src/crypto/timestamp-provider.ts", "signature": "export function getTimestampProvider(): TimestampProvider | null", - "summary": "pdfnative — Pluggable RFC 3161 Timestamp Provider ================================================== pdfnative builds and parses RFC 3161 structures itself (rfc3161.ts) but **never touches the network**: the single HTTP round-trip to a Time" + "summary": "The currently-installed global TimestampProvider, or `null` if none." }, { "name": "getTrailerRef", @@ -2228,49 +2228,49 @@ "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function hasFontLoader(lang: string): boolean", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Check if a font loader is registered for the given language." }, { "name": "helveticaBoldWidth", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function helveticaBoldWidth(str: string, sz: number): number", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Approximate text width in points using **Helvetica-Bold** character metrics." }, { "name": "helveticaWidth", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function helveticaWidth(str: string, sz: number): number", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Approximate text width in points using Helvetica character metrics." }, { "name": "hmacSha256", "kind": "value", "module": "src/crypto/sha.ts", "signature": "export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array", - "summary": "pdfnative — SHA-1, SHA-384, SHA-512, HMAC-SHA256 ================================================= SHA-2 64-bit variants using paired 32-bit words (FIPS 180-4 §6.4)." + "summary": "HMAC-SHA256." }, { "name": "inflateSync", "kind": "value", "module": "src/parser/pdf-inflate.ts", "signature": "export function inflateSync(data: Uint8Array): Uint8Array", - "summary": "pdfnative — DEFLATE Decompressor (FlateDecode reader) ====================================================== Zero-dependency DEFLATE decompression for reading FlateDecode streams." + "summary": "Decompress zlib-format data (RFC 1950 + RFC 1951)." }, { "name": "initCrypto", "kind": "value", "module": "src/crypto/index.ts", "signature": "export async function initCrypto(): Promise", - "summary": "pdfnative — Crypto Module Index ================================= Re-exports all cryptographic primitives for PDF digital signatures." + "summary": "Initialize all crypto module cross-dependencies." }, { "name": "initNodeCompression", "kind": "value", "module": "src/core/pdf-compress.ts", "signature": "export async function initNodeCompression(): Promise", - "summary": "pdfnative — PDF Stream Compression (FlateDecode) =================================================== Zero-dependency FlateDecode compression using platform-native APIs." + "summary": "Initialize Node.js compression for ESM contexts." }, { "name": "initNodeDecompression_parser", @@ -2284,14 +2284,14 @@ "kind": "value", "module": "src/core/pdf-layout-inspect.ts", "signature": "export function inspectDocumentLayout( params: DocumentParams, layoutOptions?: Partial, ): LayoutInspection", - "summary": "pdfnative — Layout Inspection (development / tooling aid) ======================================================== `inspectDocumentLayout()` reports, without rendering a PDF, how the document builder is expected to paginate a set of blocks " + "summary": "Inspect how DocumentParams.blocks will paginate and where each block is placed, without building a PDF." }, { "name": "isArmenianCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isArmenianCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Armenian Unicode block." }, { "name": "isArray", @@ -2305,21 +2305,21 @@ "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isBengaliCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in the Bengali Unicode block." }, { "name": "isCyrillicCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isCyrillicCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Cyrillic Unicode block." }, { "name": "isDevanagariCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isDevanagariCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Devanagari Unicode block." }, { "name": "isDict", @@ -2333,42 +2333,42 @@ "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isEthiopicCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Ethiopic Unicode block." }, { "name": "isGeorgianCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isGeorgianCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Georgian Unicode block." }, { "name": "isKhmerCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isKhmerCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Khmer Unicode block." }, { "name": "isLinkAnnotation", "kind": "value", "module": "src/core/pdf-annot.ts", "signature": "export function isLinkAnnotation(annot: Annotation): annot is LinkAnnotation", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Check if an annotation is an external link (has `url` property)." }, { "name": "isMathCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isMathCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint should be routed to a mathematical font (e.g." }, { "name": "isMyanmarCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isMyanmarCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in any Myanmar Unicode block." }, { "name": "isName", @@ -2389,21 +2389,21 @@ "kind": "value", "module": "src/crypto/x509.ts", "signature": "export function isSelfSigned(cert: X509Certificate): boolean", - "summary": "pdfnative — X.509 Certificate Parser ===================================== Parses X.509 v3 certificates (RFC 5280) from DER-encoded bytes." + "summary": "Check if a certificate is self-signed." }, { "name": "isSerialRevoked", "kind": "value", "module": "src/crypto/crl.ts", "signature": "export function isSerialRevoked(crl: ParsedCrl, serial: bigint): boolean", - "summary": "pdfnative — CRL Parser (RFC 5280 §5) ===================================== Parses X.509 CertificateList DER for LTV revocation data collection (PAdES B-LT — /DSS /CRLs streams)." + "summary": "Check whether a serial number appears in a CRL's revokedCertificates list." }, { "name": "isSinhalaCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isSinhalaCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in the Sinhala Unicode block." }, { "name": "isStream", @@ -2417,700 +2417,700 @@ "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isTamilCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in the Tamil Unicode block." }, { "name": "isTeluguCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isTeluguCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in the Telugu Unicode block." }, { "name": "isTibetanCodepoint", "kind": "value", "module": "src/shaping/script-registry.ts", "signature": "export function isTibetanCodepoint(cp: number): boolean", - "summary": "pdfnative — Script Registry ============================ Centralised Unicode range constants and script detection predicates." + "summary": "Check if a codepoint falls in the Tibetan Unicode block." }, { "name": "isValidPdfRgb", "kind": "value", "module": "src/core/pdf-color.ts", "signature": "export function isValidPdfRgb(str: string): boolean", - "summary": "pdfnative — Color Parsing, Validation & Normalization ======================================================= Accepts multiple color input formats and converts them into safe PDF operator strings (\"R G B\", values 0.0–1.0)." + "summary": "Check whether a string is a valid PDF RGB operator color." }, { "name": "listSignatures", "kind": "value", "module": "src/core/pdf-sig-utils.ts", "signature": "export function listSignatures(pdfBytes: Uint8Array): readonly PdfSignatureInfo[]", - "summary": "pdfnative — Signature Enumeration Utilities (v1.7.0) ===================================================== Read-only inspection of the signature fields in a PDF: which signatures exist, their `/SubFilter`, their `/ByteRange`, the raw `/Cont" + "summary": "List every signature field in the document, in AcroForm order — signed signatures, document timestamps, and unsigned placeholders." }, { "name": "loadFontData", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export async function loadFontData(lang: string): Promise", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Lazy-load pre-built font data for a language." }, { "name": "mergePdfs", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export function mergePdfs(sources: readonly PdfSourceInput[], opts?: MergeOptions): Uint8Array", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Concatenate multiple PDF documents into one, preserving page order." }, { "name": "nameValue", "kind": "value", "module": "src/parser/pdf-object-parser.ts", "signature": "export function nameValue(v: PdfValue): string | undefined", - "summary": "pdfnative — PDF Object Parser ================================ Parses PDF indirect objects from a tokenized stream (ISO 32000-1 §7.3)." + "summary": "Extract the string value from a PdfName, or undefined if not a name." }, { "name": "needsUnicodeFont", "kind": "value", "module": "src/shaping/script-detect.ts", "signature": "export function needsUnicodeFont(lang: string): boolean", - "summary": "pdfnative — Script Detection ============================= Detects Unicode script ranges in text to determine which fonts are needed." + "summary": "Languages requiring Unicode font embedding (non-WinAnsi scripts)." }, { "name": "niceTicks", "kind": "value", "module": "src/core/pdf-chart.ts", "signature": "export function niceTicks(min: number, max: number, target = 5): { lo: number; hi: number; step: number }", - "summary": "pdfnative — Native vector charts (v1.6.0, extended v1.7.0) ============================================ Renders ChartBlocks as pure PDF path operators — rectangles, line segments and cubic-Bézier arcs — with zero dependencies and no rasteri" + "summary": "Compute rounded tick bounds/step covering [min, max] (1/2/5×10ⁿ)." }, { "name": "normalizeBidiEmbeddings", "kind": "value", "module": "src/shaping/bidi.ts", "signature": "export function normalizeBidiEmbeddings(text: string): string", - "summary": "pdfnative — Simplified Unicode Bidirectional Algorithm ======================================================= Pure JS implementation of a subset of UAX #9 (Unicode Bidirectional Algorithm)." + "summary": "UAX #9 explicit embedding normalization (v1.2.0, refined v1.3.0)." }, { "name": "normalizeColors", "kind": "value", "module": "src/core/pdf-color.ts", "signature": "export function normalizeColors(colors: PdfColors): PdfColors", - "summary": "pdfnative — Color Parsing, Validation & Normalization ======================================================= Accepts multiple color input formats and converts them into safe PDF operator strings (\"R G B\", values 0.0–1.0)." + "summary": "Validate and normalize all color fields in a PdfColors object." }, { "name": "openPdf", "kind": "value", "module": "src/parser/pdf-reader.ts", "signature": "export function openPdf(bytes: Uint8Array, options?: OpenPdfOptions): PdfReader", - "summary": "pdfnative — PDF Document Reader ================================== High-level reader that provides lazy object resolution and page access." + "summary": "Open a PDF file for reading." }, { "name": "parseCertificate", "kind": "value", "module": "src/crypto/x509.ts", "signature": "export function parseCertificate(der: Uint8Array): X509Certificate", - "summary": "pdfnative — X.509 Certificate Parser ===================================== Parses X.509 v3 certificates (RFC 5280) from DER-encoded bytes." + "summary": "Parse a DER-encoded X.509 certificate." }, { "name": "parseCmsSignedData", "kind": "value", "module": "src/crypto/cms-utils.ts", "signature": "export function parseCmsSignedData(der: Uint8Array): ParsedCms", - "summary": "pdfnative — CMS SignedData Parsing & DER Surgery (RFC 5652) ============================================================ Parses CMS/PKCS#7 SignedData structures (the inverse of cms.ts) and performs the one mutation LTV workflows need: appen" + "summary": "Parse a DER-encoded CMS ContentInfo containing a SignedData (RFC 5652 §5.1) into its LTV-relevant parts." }, { "name": "parseColor", "kind": "value", "module": "src/core/pdf-color.ts", "signature": "export function parseColor(input: PdfColor): string", - "summary": "pdfnative — Color Parsing, Validation & Normalization ======================================================= Accepts multiple color input formats and converts them into safe PDF operator strings (\"R G B\", values 0.0–1.0)." + "summary": "Parse a color input into a validated PDF RGB operator string." }, { "name": "parseColrCpal", "kind": "value", "module": "src/fonts/colr-parser.ts", "signature": "export function parseColrCpal(bytes: Uint8Array): Record | null", - "summary": "pdfnative — COLR / CPAL Colour-Font Parser =========================================== Pure-JS reader for the OpenType COLR (colour layers) and CPAL (colour palette) tables." + "summary": "Parse the COLR + CPAL tables of a font into a colour-glyph map keyed by base glyph id." }, { "name": "parseCpal", "kind": "value", "module": "src/fonts/colr-parser.ts", "signature": "export function parseCpal(bytes: Uint8Array): CpalColor[] | null", - "summary": "pdfnative — COLR / CPAL Colour-Font Parser =========================================== Pure-JS reader for the OpenType COLR (colour layers) and CPAL (colour palette) tables." + "summary": "Parse the CPAL table's first palette into an array of CpalColors (sRGB + alpha, each channel 0–255), indexed by palette entry." }, { "name": "parseCrl", "kind": "value", "module": "src/crypto/crl.ts", "signature": "export function parseCrl(der: Uint8Array): ParsedCrl", - "summary": "pdfnative — CRL Parser (RFC 5280 §5) ===================================== Parses X.509 CertificateList DER for LTV revocation data collection (PAdES B-LT — /DSS /CRLs streams)." + "summary": "Parse a DER-encoded CertificateList (RFC 5280 §5.1): ``` CertificateList ::= SEQUENCE { tbsCertList TBSCertList, signatureAlgorithm AlgorithmIdentifier, signatureValue BIT STRING } TBSCertList ::= SEQUENCE { version I" }, { "name": "parseGlyfFont", "kind": "value", "module": "src/fonts/glyf-outline.ts", "signature": "export function parseGlyfFont(bytes: Uint8Array): GlyfFont | null", - "summary": "pdfnative — TrueType `glyf` Outline Extractor ============================================== Pure-JS reader that turns a TrueType glyph outline into ordered contours of quadratic on/off-curve points." + "summary": "Parse the table directory, `head`, `maxp` and `loca` so that subsequent extractGlyphContours calls are O(1) lookups." }, { "name": "parseImage", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function parseImage(bytes: Uint8Array): ParsedImage", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Parse an image from raw bytes (auto-detect JPEG or PNG)." }, { "name": "parseIndirectObject", "kind": "value", "module": "src/parser/pdf-object-parser.ts", "signature": "export function parseIndirectObject(tok: PdfTokenizer): PdfIndirectObject | null", - "summary": "pdfnative — PDF Object Parser ================================ Parses PDF indirect objects from a tokenized stream (ISO 32000-1 §7.3)." + "summary": "Parse a PDF indirect object definition: `num gen obj ..." }, { "name": "parseJPEG", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function parseJPEG(bytes: Uint8Array): ParsedImage", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Parse a JPEG image to extract dimensions and color space." }, { "name": "parseOcspResponse", "kind": "value", "module": "src/crypto/ocsp.ts", "signature": "export function parseOcspResponse(der: Uint8Array): OcspResponse", - "summary": "pdfnative — OCSP Request Builder / Response Parser (RFC 6960) ============================================================== Builds unsigned OCSPRequest DER and parses OCSPResponse / BasicOCSPResponse for LTV revocation data collection (PAd" + "summary": "Parse a DER OCSPResponse (RFC 6960 §4.2.1)." }, { "name": "parsePNG", "kind": "value", "module": "src/core/pdf-image.ts", "signature": "export function parsePNG(bytes: Uint8Array): ParsedImage", - "summary": "pdfnative — PDF Image Support =============================== Parse JPEG and PNG images, build PDF XObject streams." + "summary": "Parse a PNG image to extract dimensions, color type, and compressed image data." }, { "name": "parseRsaPrivateKey", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function parseRsaPrivateKey(der: Uint8Array): RsaPrivateKey", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Parse an RSA private key from PKCS#8 DER bytes." }, { "name": "parseRsaPublicKey", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function parseRsaPublicKey(der: Uint8Array): RsaPublicKey", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Parse an RSA public key from SubjectPublicKeyInfo DER bytes." }, { "name": "parseSvgPath", "kind": "value", "module": "src/core/pdf-svg.ts", "signature": "export function parseSvgPath(d: string): SvgSegment[]", - "summary": "pdfnative — SVG Path Rendering ================================ Parses SVG path data and basic SVG elements, renders them as PDF path operators using coordinate matrix transformation." + "summary": "Parse SVG path `d` string into normalized absolute-coordinate segments." }, { "name": "parseTimestampResponse", "kind": "value", "module": "src/crypto/rfc3161.ts", "signature": "export function parseTimestampResponse(der: Uint8Array): TimestampResponse", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Parse a DER-encoded TimeStampResp (RFC 3161 §2.4.2): `SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL }`." }, { "name": "parseTimestampToken", "kind": "value", "module": "src/crypto/rfc3161.ts", "signature": "export function parseTimestampToken(tokenDer: Uint8Array): TstInfo", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Parse a TimeStampToken (a CMS SignedData whose eContent is a TSTInfo, eContentType id-ct-TSTInfo 1.2.840.113549.1.9.16.1.4) into its TSTInfo fields plus the embedded TSA certificates." }, { "name": "parseValue", "kind": "value", "module": "src/parser/pdf-object-parser.ts", "signature": "export function parseValue(tok: PdfTokenizer, depth = 0): PdfValue", - "summary": "pdfnative — PDF Object Parser ================================ Parses PDF indirect objects from a tokenized stream (ISO 32000-1 §7.3)." + "summary": "Parse a single PDF value from the token stream." }, { "name": "parseXrefTable", "kind": "value", "module": "src/parser/pdf-xref-parser.ts", "signature": "export function parseXrefTable(buf: Uint8Array): XrefTable", - "summary": "pdfnative — PDF Cross-Reference Table Parser =============================================== Parses traditional xref tables and xref streams (ISO 32000-1 §7.5)." + "summary": "Parse the complete xref table including incremental updates (via /Prev chain)." }, { "name": "pdfString", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function pdfString(str: string): string", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Create a PDF string literal: encode to WinAnsi and escape (, ), \\." }, { "name": "readFormFields", "kind": "value", "module": "src/core/pdf-form-fill.ts", "signature": "export function readFormFields(pdfBytes: Uint8Array, opts?: { password?: string }): ParsedFormField[]", - "summary": "pdfnative — AcroForm fill & flatten (ISO 32000-1 §12.7) ========================================================== Read, fill, and flatten the interactive form fields of an **existing** PDF (whether authored by pdfnative or a third party), " + "summary": "Enumerate the interactive form fields of an existing PDF." }, { "name": "registerFont", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function registerFont(lang: string, loader: FontLoader): void", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Register a font data loader for a language." }, { "name": "registerFonts", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function registerFonts(fonts: Record): void", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Register multiple font loaders at once." }, { "name": "renderBarcode", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderBarcode( format: BarcodeFormat, data: string, x: number, y: number, width: number, height: number, options?: { readonly ecLevel?: QRErrorLevel; readonly pdf417ECLevel?: number }, ): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render a barcode as PDF path operators." }, { "name": "renderChartBlock", "kind": "value", "module": "src/core/pdf-chart.ts", "signature": "export function renderChartBlock( block: ChartBlock, y: number, mgL: number, cw: number, enc: EncodingContext, tagCtx?: TagContext, documentChildren?: (StructElement | MCRef)[],", - "summary": "pdfnative — Native vector charts (v1.6.0, extended v1.7.0) ============================================ Renders ChartBlocks as pure PDF path operators — rectangles, line segments and cubic-Bézier arcs — with zero dependencies and no rasteri" + "summary": "Render a chart block." }, { "name": "renderCode128", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderCode128(data: string, x: number, y: number, width: number, height: number): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render Code 128 barcode as PDF path operators." }, { "name": "renderColorGlyph", "kind": "value", "module": "src/core/pdf-color-glyph.ts", "signature": "export function renderColorGlyph( glyph: ColorGlyph, outlines: OutlineProvider, unitsPerEm: number, ): ColorGlyphForm", - "summary": "pdfnative — Colour Glyph Renderer (COLR/CPAL → native PDF) ========================================================== Renders a resolved ColorGlyph as a native PDF Form XObject: each layer's base-glyph outline is emitted as a vector path an" + "summary": "Render a colour glyph into a ColorGlyphForm." }, { "name": "renderDataMatrix", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderDataMatrix(data: string, x: number, y: number, size: number): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render a Data Matrix barcode as PDF path operators." }, { "name": "renderEAN13", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderEAN13(data: string, x: number, y: number, width: number, height: number): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render EAN-13 barcode as PDF path operators." }, { "name": "renderPDF417", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderPDF417(data: string, x: number, y: number, width: number, height: number, ecLevel: number = 2): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render PDF417 as PDF path operators." }, { "name": "renderQR", "kind": "value", "module": "src/core/pdf-barcode.ts", "signature": "export function renderQR(data: string, x: number, y: number, size: number, ecLevel: QRErrorLevel = 'M'): string", - "summary": "pdfnative — Barcode & QR Code Generation ========================================== Pure PDF operator rendering for 1D and 2D barcodes." + "summary": "Render a QR Code as PDF path operators." }, { "name": "renderSvg", "kind": "value", "module": "src/core/pdf-svg.ts", "signature": "export function renderSvg( data: string, x: number, y: number, w: number, h: number, options?: SvgRenderOptions, enc?: EncodingContext, ): string", - "summary": "pdfnative — SVG Path Rendering ================================ Parses SVG path data and basic SVG elements, renders them as PDF path operators using coordinate matrix transformation." + "summary": "Render SVG content as PDF path operators." }, { "name": "resetFontRegistry", "kind": "value", "module": "src/fonts/font-loader.ts", "signature": "export function resetFontRegistry(): void", - "summary": "pdfnative — Font Loader ========================= Configurable font registry and lazy-loading." + "summary": "Clear all registered fonts and cache (useful for testing)." }, { "name": "resolveBidiRuns", "kind": "value", "module": "src/shaping/bidi.ts", "signature": "export function resolveBidiRuns(text: string): BidiRun[]", - "summary": "pdfnative — Simplified Unicode Bidirectional Algorithm ======================================================= Pure JS implementation of a subset of UAX #9 (Unicode Bidirectional Algorithm)." + "summary": "Resolve bidirectional text into ordered runs with embedding levels." }, { "name": "resolveLayout", "kind": "value", "module": "src/core/pdf-layout.ts", "signature": "export function resolveLayout(options?: Partial):", - "summary": "pdfnative — PDF Layout Constants ================================== A4 page dimensions and default layout settings." + "summary": "Create a resolved layout configuration from user options + defaults." }, { "name": "resolvePdfAConfig", "kind": "value", "module": "src/core/pdf-tags.ts", "signature": "export function resolvePdfAConfig(tagged: boolean | string | undefined): PdfAConfig", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Parse the `tagged` layout option into a resolved PDF/A configuration." }, { "name": "resolvePrintBoxes", "kind": "value", "module": "src/core/pdf-print.ts", "signature": "export function resolvePrintBoxes(print: PrintOptions, pgW: number, pgH: number): ResolvedPrintBoxes", - "summary": "pdfnative — Print Production (v1.7.0) ====================================== Professional-printing page geometry and decoration: - Page boxes — `/TrimBox`, `/BleedBox`, `/ArtBox`, `/CropBox` (ISO 32000-1 §14.11.2) plus large-format `/UserUn" + "summary": "Resolve the page-dictionary fragment for the configured boxes." }, { "name": "resolveTemplate", "kind": "value", "module": "src/core/pdf-layout.ts", "signature": "export function resolveTemplate( tpl: string | undefined, page: number, pages: number, title: string, date: string, ): string", - "summary": "pdfnative — PDF Layout Constants ================================== A4 page dimensions and default layout settings." + "summary": "Replace placeholder tokens in a header/footer template string." }, { "name": "rsaSign", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function rsaSign(message: Uint8Array, privateKey: RsaPrivateKey, digest: RsaDigest = 'sha256'): Uint8Array", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Sign a message with RSA PKCS#1 v1.5." }, { "name": "rsaSignHash", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function rsaSignHash(hash: Uint8Array, privateKey: RsaPrivateKey, digest: RsaDigest = 'sha256'): Uint8Array", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Sign a pre-computed hash with RSA PKCS#1 v1.5." }, { "name": "rsaVerify", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function rsaVerify(message: Uint8Array, signature: Uint8Array, publicKey: RsaPublicKey, digest: RsaDigest = 'sha256'): boolean", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Verify an RSA PKCS#1 v1.5 signature over a message." }, { "name": "rsaVerifyHash", "kind": "value", "module": "src/crypto/rsa.ts", "signature": "export function rsaVerifyHash(hash: Uint8Array, signature: Uint8Array, publicKey: RsaPublicKey, digest: RsaDigest = 'sha256'): boolean", - "summary": "pdfnative — RSA PKCS#1 v1.5 Sign / Verify ============================================ Pure BigInt implementation of RSA signature operations." + "summary": "Verify an RSA PKCS#1 v1.5 signature against a pre-computed hash." }, { "name": "setCryptoProvider", "kind": "value", "module": "src/crypto/crypto-provider.ts", "signature": "export function setCryptoProvider(provider: CryptoProvider | null): void", - "summary": "pdfnative — Pluggable Signature Crypto Provider ================================================ pdfnative ships its own pure-TypeScript RSA (PKCS#1 v1.5) and ECDSA (P-256) implementations so that signing works in any runtime with **zero de" + "summary": "Install (or clear) a global signature CryptoProvider." }, { "name": "setDeflateImpl", "kind": "value", "module": "src/core/pdf-compress.ts", "signature": "export function setDeflateImpl(fn: ((buf: Uint8Array) => Uint8Array) | null): void", - "summary": "pdfnative — PDF Stream Compression (FlateDecode) =================================================== Zero-dependency FlateDecode compression using platform-native APIs." + "summary": "Inject a custom deflate implementation (e.g." }, { "name": "setInflateImpl", "kind": "value", "module": "src/parser/pdf-inflate.ts", "signature": "export function setInflateImpl(fn: ((buf: Uint8Array) => Uint8Array) | null): void", - "summary": "pdfnative — DEFLATE Decompressor (FlateDecode reader) ====================================================== Zero-dependency DEFLATE decompression for reading FlateDecode streams." + "summary": "Inject a custom inflate implementation." }, { "name": "setMaxInflateOutputSize", "kind": "value", "module": "src/parser/pdf-inflate.ts", "signature": "export function setMaxInflateOutputSize(size: number): void", - "summary": "pdfnative — DEFLATE Decompressor (FlateDecode reader) ====================================================== Zero-dependency DEFLATE decompression for reading FlateDecode streams." + "summary": "Set the global maximum decompressed output size for inflateSync." }, { "name": "setRevocationProvider", "kind": "value", "module": "src/crypto/revocation-provider.ts", "signature": "export function setRevocationProvider(provider: RevocationProvider | null): void", - "summary": "pdfnative — Pluggable OCSP/CRL Revocation Provider =================================================== pdfnative builds OCSP requests and parses OCSP/CRL responses itself (ocsp.ts / crl.ts) but **never touches the network**: the HTTP round-" + "summary": "Install (or clear) a global RevocationProvider." }, { "name": "setTimestampProvider", "kind": "value", "module": "src/crypto/timestamp-provider.ts", "signature": "export function setTimestampProvider(provider: TimestampProvider | null): void", - "summary": "pdfnative — Pluggable RFC 3161 Timestamp Provider ================================================== pdfnative builds and parses RFC 3161 structures itself (rfc3161.ts) but **never touches the network**: the single HTTP round-trip to a Time" + "summary": "Install (or clear) a global TimestampProvider." }, { "name": "sha1", "kind": "value", "module": "src/crypto/sha.ts", "signature": "export function sha1(input: Uint8Array): Uint8Array", - "summary": "pdfnative — SHA-1, SHA-384, SHA-512, HMAC-SHA256 ================================================= SHA-2 64-bit variants using paired 32-bit words (FIPS 180-4 §6.4)." + "summary": "SHA-1 hash (FIPS 180-4)." }, { "name": "sha384", "kind": "value", "module": "src/crypto/sha.ts", "signature": "export function sha384(input: Uint8Array): Uint8Array", - "summary": "pdfnative — SHA-1, SHA-384, SHA-512, HMAC-SHA256 ================================================= SHA-2 64-bit variants using paired 32-bit words (FIPS 180-4 §6.4)." + "summary": "SHA-384 hash (FIPS 180-4)." }, { "name": "sha512", "kind": "value", "module": "src/crypto/sha.ts", "signature": "export function sha512(input: Uint8Array): Uint8Array", - "summary": "pdfnative — SHA-1, SHA-384, SHA-512, HMAC-SHA256 ================================================= SHA-2 64-bit variants using paired 32-bit words (FIPS 180-4 §6.4)." + "summary": "SHA-512 hash (FIPS 180-4)." }, { "name": "shapeArabicText", "kind": "value", "module": "src/shaping/arabic-shaper.ts", "signature": "export function shapeArabicText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Arabic Text Shaper ================================ Unicode Presentation Forms-based positional shaping for Arabic script." + "summary": "Shape Arabic text using Unicode Presentation Forms." }, { "name": "shapeBengaliText", "kind": "value", "module": "src/shaping/bengali-shaper.ts", "signature": "export function shapeBengaliText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Bengali Mini-Shaper ================================= Pure JS OpenType GSUB + GPOS shaping for Bengali script." + "summary": "Shape a string of Bengali text into an array of positioned glyphs." }, { "name": "shapeDevanagariText", "kind": "value", "module": "src/shaping/devanagari-shaper.ts", "signature": "export function shapeDevanagariText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Devanagari Mini-Shaper ==================================== Pure JS OpenType GSUB + GPOS shaping for Devanagari script." + "summary": "Shape a string of Devanagari text into an array of positioned glyphs." }, { "name": "shapeKhmerText", "kind": "value", "module": "src/shaping/khmer-shaper.ts", "signature": "export function shapeKhmerText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Khmer Mini-Shaper ============================== Pure JS OpenType GSUB + GPOS shaping for the Khmer script (Cambodia)." + "summary": "Shape a string of Khmer text into an array of positioned glyphs." }, { "name": "shapeMyanmarText", "kind": "value", "module": "src/shaping/myanmar-shaper.ts", "signature": "export function shapeMyanmarText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Myanmar Mini-Shaper ================================ Pure JS OpenType GSUB + GPOS shaping for the Myanmar (Burmese) script." + "summary": "Shape a string of Myanmar text into an array of positioned glyphs." }, { "name": "shapeSinhalaText", "kind": "value", "module": "src/shaping/sinhala-shaper.ts", "signature": "export function shapeSinhalaText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Sinhala Mini-Shaper ================================ Pure JS OpenType GSUB + GPOS shaping for the Sinhala script (Sri Lanka)." + "summary": "Shape a string of Sinhala text into an array of positioned glyphs." }, { "name": "shapeTamilText", "kind": "value", "module": "src/shaping/tamil-shaper.ts", "signature": "export function shapeTamilText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Tamil Mini-Shaper =============================== Pure JS OpenType GSUB + GPOS shaping for Tamil script." + "summary": "Shape a string of Tamil text into an array of positioned glyphs." }, { "name": "shapeTeluguText", "kind": "value", "module": "src/shaping/telugu-shaper.ts", "signature": "export function shapeTeluguText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Telugu Mini-Shaper ================================ Pure JS OpenType GSUB + GPOS shaping for Telugu script." + "summary": "Shape a string of Telugu text into an array of positioned glyphs." }, { "name": "shapeThaiText", "kind": "value", "module": "src/shaping/thai-shaper.ts", "signature": "export function shapeThaiText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Thai Mini-Shaper ============================= Pure JS OpenType GSUB + GPOS shaping for Thai script." + "summary": "Shape a string of Thai text into an array of positioned glyphs." }, { "name": "shapeTibetanText", "kind": "value", "module": "src/shaping/tibetan-shaper.ts", "signature": "export function shapeTibetanText(str: string, fontData: FontData): ShapedGlyph[]", - "summary": "pdfnative — Tibetan Mini-Shaper ================================ Pure JS OpenType GSUB + GPOS shaping for the Tibetan script." + "summary": "Shape a string of Tibetan text into an array of positioned glyphs." }, { "name": "signPdfBytes", "kind": "value", "module": "src/core/pdf-signature.ts", "signature": "export function signPdfBytes(pdfBytes: Uint8Array, options: PdfSignOptions): Uint8Array", - "summary": "pdfnative — PDF Digital Signature Support ========================================== ISO 32000-1 §12.8 compliant PDF signing." + "summary": "Sign a PDF that contains a signature placeholder." }, { "name": "signPdfBytesWithTimestamp", "kind": "value", "module": "src/core/pdf-sign-timestamp.ts", "signature": "export async function signPdfBytesWithTimestamp( pdfBytes: Uint8Array, options: PdfSignTimestampOptions, ): Promise", - "summary": "pdfnative — Timestamped PDF Signing (PAdES B-T, v1.7.0) ======================================================== `signPdfBytesWithTimestamp` signs exactly like signPdfBytes and then obtains an RFC 3161 timestamp over the CMS signature value" + "summary": "Sign a placeholder-carrying PDF and embed an RFC 3161 signature timestamp in the CMS unsigned attributes (PAdES B-T when combined with `profile: 'pades'`)." }, { "name": "slugify", "kind": "value", "module": "src/core/pdf-stream.ts", "signature": "export function slugify(str: string): string", - "summary": "pdfnative — PDF Stream Utilities ================================== Binary conversion and PDF byte-level helpers." + "summary": "Sanitize string for use in filename (filesystem-safe)." }, { "name": "splitPdf", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export function splitPdf( src: PdfSourceInput, ranges: readonly PageRange[], opts?: MergeOptions, ): Uint8Array[]", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Split a document into multiple PDFs, one per page range." }, { "name": "splitTextByFont", "kind": "value", "module": "src/shaping/multi-font.ts", "signature": "export function splitTextByFont(str: string, fontEntries: FontEntry[]): FontRun[]", - "summary": "pdfnative — Multi-Font Text Run Splitter ========================================== Split text into runs, each assigned to the font whose cmap covers it." + "summary": "Split a string into text runs, each assigned to the font whose cmap covers it." }, { "name": "streamByteLength", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function streamByteLength(stream: AsyncGenerator): Promise", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Count total byte length from streaming chunks without buffering." }, { "name": "streamExtractPages", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export async function* streamExtractPages( src: PdfSourceInput, pageIndices: readonly number[], opts?: StreamMergeOptions, ): AsyncGenerator", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Streaming extractPages: yields the extracted PDF as fixed-size chunks, byte-identical to `extractPages`." }, { "name": "streamMergedPdfs", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export async function* streamMergedPdfs( sources: readonly PdfSourceInput[], opts?: StreamMergeOptions, ): AsyncGenerator", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Streaming mergePdfs: yields the merged PDF as fixed-size chunks." }, { "name": "streamSplitPdf", "kind": "value", "module": "src/parser/pdf-pagetree.ts", "signature": "export async function* streamSplitPdf( src: PdfSourceInput, ranges: readonly PageRange[], opts?: StreamMergeOptions, ): AsyncGenerator", - "summary": "pdfnative — Page-Tree Manipulation API (ISO 32000-1 §7.7.3) ============================================================ Safe, **faithful** page-level document assembly: merge, split, and extract pages across PDF documents." + "summary": "Streaming splitPdf: yields one SplitPdfStream per range, in order." }, { "name": "streamToFile", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export async function streamToFile( stream: AsyncGenerator, filePath: string, opts?: { readonly signal?: AbortSignal }, ): Promise", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Write a streaming PDF — any of the `buildPDFStream*` / `buildDocumentPDFStream*` generators — directly to a file on disk, honouring write back-pressure so only one chunk is held at a time." }, { "name": "stripBidiControls", "kind": "value", "module": "src/shaping/bidi.ts", "signature": "export function stripBidiControls(text: string): string", - "summary": "pdfnative — Simplified Unicode Bidirectional Algorithm ======================================================= Pure JS implementation of a subset of UAX #9 (Unicode Bidirectional Algorithm)." + "summary": "Strip invisible Unicode bidirectional formatting characters." }, { "name": "toBytes", "kind": "value", "module": "src/core/pdf-stream.ts", "signature": "export function toBytes(str: string): Uint8Array", - "summary": "pdfnative — PDF Stream Utilities ================================== Binary conversion and PDF byte-level helpers." + "summary": "Convert a single-byte string to Uint8Array." }, { "name": "toWinAnsi", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function toWinAnsi(str: string): string", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Encode a JavaScript string to WinAnsiEncoding (ISO-8859-1 superset)." }, { "name": "truncate", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function truncate(str: string, max: number): string", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Truncate string to max characters, appending Unicode ellipsis (…, U+2026) if needed." }, { "name": "truncateToWidth", "kind": "value", "module": "src/fonts/encoding.ts", "signature": "export function truncateToWidth( str: string, maxWidthPt: number, sz: number, enc: EncodingContext, ): string", - "summary": "pdfnative — WinAnsi Encoding ============================== Text encoding and font reference logic for Latin (WinAnsi/Helvetica) and Unicode (CIDFont/Identity-H) modes." + "summary": "Truncate string so that its rendered width does not exceed `maxWidthPt`, appending the Unicode ellipsis (…, U+2026) when truncation occurs." }, { "name": "validateAttachments", "kind": "value", "module": "src/core/pdf-tags.ts", "signature": "export function validateAttachments(attachments: readonly PdfAttachment[] | undefined, tagged: boolean | string | undefined): void", - "summary": "pdfnative — Tagged PDF & PDF/A Support ======================================== Structure tree, marked content operators, XMP metadata, and OutputIntent for PDF/UA accessibility and PDF/A archival compliance." + "summary": "Validate attachments against PDF/A configuration." }, { "name": "validateDocumentStreamable", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export function validateDocumentStreamable(params: DocumentParams, layoutOptions?: Partial): void", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Validate that document params are compatible with streaming output." }, { "name": "validateFontData", "kind": "value", "module": "src/fonts/font-validator.ts", "signature": "export function validateFontData(data: unknown): FontValidationResult", - "summary": "pdfnative — Font Data Validator ================================ `validateFontData()` performs a read-only structural sanity check on a FontData module before it is used for rendering." + "summary": "Structurally validate a FontData module." }, { "name": "validatePdfUA", "kind": "value", "module": "src/parser/pdf-ua-validator.ts", "signature": "export function validatePdfUA(bytes: Uint8Array): PdfUAValidationResult", - "summary": "pdfnative — PDF/UA structural validator (ISO 14289-1) ====================================================== A lightweight, read-only conformance checker for tagged-PDF (PDF/UA) structural prerequisites." + "summary": "Validate the PDF/UA (ISO 14289-1) structural prerequisites of a tagged PDF." }, { "name": "validatePrintOptions", "kind": "value", "module": "src/core/pdf-print.ts", "signature": "export function validatePrintOptions( print: PrintOptions, pgW: number, pgH: number, tagged: boolean | string | undefined, ): void", - "summary": "pdfnative — Print Production (v1.7.0) ====================================== Professional-printing page geometry and decoration: - Page boxes — `/TrimBox`, `/BleedBox`, `/ArtBox`, `/CropBox` (ISO 32000-1 §14.11.2) plus large-format `/UserUn" + "summary": "Validate `layout.print` against the page geometry and conformance target." }, { "name": "validateTableStreamable", "kind": "value", "module": "src/core/pdf-stream-writer.ts", "signature": "export function validateTableStreamable(_params: PdfParams, layoutOptions?: Partial): void", - "summary": "pdfnative — Streaming PDF Output ================================== AsyncGenerator-based PDF output for progressive chunk emission." + "summary": "Validate that table params are compatible with streaming output." }, { "name": "validateURL", "kind": "value", "module": "src/core/pdf-annot.ts", "signature": "export function validateURL(url: string): boolean", - "summary": "pdfnative — PDF Link Annotations ================================== Build PDF link annotation objects for external URLs and internal page links." + "summary": "Validate a URL for use in PDF link annotations." }, { "name": "validateWatermark", "kind": "value", "module": "src/core/pdf-watermark.ts", "signature": "export function validateWatermark( watermark: WatermarkOptions, pdfaLevel: boolean | string | undefined, ): void", - "summary": "pdfnative — PDF Watermark Rendering ====================================== Builds PDF content operators for text and image watermarks." + "summary": "Validate watermark options against PDF/A constraints." }, { "name": "verifyCertSignature", "kind": "value", "module": "src/crypto/x509.ts", "signature": "export function verifyCertSignature(cert: X509Certificate, issuerCert: X509Certificate): boolean", - "summary": "pdfnative — X.509 Certificate Parser ===================================== Parses X.509 v3 certificates (RFC 5280) from DER-encoded bytes." + "summary": "Verify a certificate's signature using the issuer's public key." }, { "name": "verifyTimestampImprint", "kind": "value", "module": "src/crypto/rfc3161.ts", "signature": "export function verifyTimestampImprint(info: TstInfo, expectedHash: Uint8Array): boolean", - "summary": "pdfnative — RFC 3161 Time-Stamp Protocol Structures ==================================================== Builds TimeStampReq and parses TimeStampResp / TimeStampToken DER for PAdES B-T signature timestamps and ISO 32000-2 document timestamp" + "summary": "Check that a parsed timestamp token covers the expected data: byte-compare the TSTInfo messageImprint against `expectedHash`." }, { "name": "vriKeyForContents", "kind": "value", "module": "src/core/pdf-dss.ts", "signature": "export function vriKeyForContents(contents: Uint8Array): string", - "summary": "pdfnative — Document Security Store (PAdES B-LT, v1.7.0) ========================================================= LTV enablement per ISO 32000-2 §12.8.4: collect the validation material for every signature (certificates, OCSP responses, CR" + "summary": "Uppercase-hex SHA-1 of the full /Contents value — the /VRI key." }, { "name": "wrapText", diff --git a/docs/assets/ecosystem.json b/docs/assets/ecosystem.json index 9302458..fcbed70 100644 --- a/docs/assets/ecosystem.json +++ b/docs/assets/ecosystem.json @@ -115,7 +115,7 @@ }, "declared": { "$comment": "Hand-maintained figures that no filesystem walk can derive. Every doc must echo these exactly.", - "tests": 2665, + "tests": 2686, "scripts": 22, "bundledFontModules": 26, "blockTypes": 13, @@ -191,7 +191,7 @@ { "id": "test-count-homepage", "$comment": "The homepage quoted a stale test count for a full release train; no assertion covered it. canonical-present pins the current figure where it is displayed.", - "canonical": "2665+ tests across 122 files", + "canonical": "2686+ tests across 123 files", "requireIn": [ "docs/index.html" ] diff --git a/docs/data/surfaces.json b/docs/data/surfaces.json index 20c056d..7f01372 100644 --- a/docs/data/surfaces.json +++ b/docs/data/surfaces.json @@ -24,12 +24,12 @@ "library": { "supported": true, "call": "chart block in buildDocumentPDFBytes", "since": "1.6.0" }, "cli": { "supported": true, "command": "render", "since": "1.3.0", "notes": "chart blocks in the document JSON." }, "mcp": { "supported": true, "tool": "add_chart", "since": "1.5.0", "notes": "Also as a chart block inside generate_basic_pdf." }, - "react": { "supported": true, "call": "" } + "react": { "supported": true, "call": "", "since": "1.1.0" } }, { "id": "sign", "label": "Digital signatures (PAdES CMS)", - "library": { "supported": true, "call": "addSignaturePlaceholder + signPdfBytes" }, + "library": { "supported": true, "call": "addSignaturePlaceholder + signPdfBytes", "since": "1.2.0" }, "cli": { "supported": true, "command": "sign" }, "mcp": { "supported": true, "tool": "sign_pdf", "notes": "prepare_signature_placeholder creates the /Sig placeholder; sign_pdf auto-injects one when absent." }, "react": { "supported": false, "notes": "Sign the rendered bytes with the library or the CLI." } @@ -93,7 +93,7 @@ { "id": "validate-pdfua", "label": "Validate PDF/UA", - "library": { "supported": true, "call": "validatePdfUA" }, + "library": { "supported": true, "call": "validatePdfUA", "since": "1.3.0" }, "cli": { "supported": true, "command": "inspect", "since": "1.1.0", "notes": "inspect --pdfua, and --check pdfua as a CI assertion." }, "mcp": { "supported": true, "tool": "validate_pdf", "since": "1.1.0" }, "react": { "supported": false, "notes": "lintDocument checks the authoring model before rendering; it is not a validation of the emitted PDF." } diff --git a/docs/guides/accessibility.html b/docs/guides/accessibility.html index 7f74831..225cb55 100644 --- a/docs/guides/accessibility.html +++ b/docs/guides/accessibility.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/agentic-workflows.html b/docs/guides/agentic-workflows.html index 191718a..bc7c23f 100644 --- a/docs/guides/agentic-workflows.html +++ b/docs/guides/agentic-workflows.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/ai-governance.html b/docs/guides/ai-governance.html index 563647f..cdebf43 100644 --- a/docs/guides/ai-governance.html +++ b/docs/guides/ai-governance.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/annotations.html b/docs/guides/annotations.html index fd373fb..34f07fa 100644 --- a/docs/guides/annotations.html +++ b/docs/guides/annotations.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/architecture.html b/docs/guides/architecture.html index 030b146..076aeb1 100644 --- a/docs/guides/architecture.html +++ b/docs/guides/architecture.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/charts.html b/docs/guides/charts.html index 8fda552..af093be 100644 --- a/docs/guides/charts.html +++ b/docs/guides/charts.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/choose.html b/docs/guides/choose.html index 6204185..823eb95 100644 --- a/docs/guides/choose.html +++ b/docs/guides/choose.html @@ -28,7 +28,7 @@ - + @@ -131,11 +131,11 @@

      Capability × surfacechart block (v1.6.0)

      - + - + @@ -191,7 +191,7 @@

      Capability × surface

      - + diff --git a/docs/guides/choose.md b/docs/guides/choose.md index b8d255c..66f5c4c 100644 --- a/docs/guides/choose.md +++ b/docs/guides/choose.md @@ -49,8 +49,8 @@ means the surface does not offer it. |---|---|---|---|---| | Generate documents | `buildDocumentPDFBytes` / `buildPDFBytes` | `render` | `generate_basic_pdf` (+ the dedicated document tools) | `renderToBytes` and friends, `` tree | | Smart tables | `table` block | `render` (`table` block, or `--variant table`) | `add_table` | `
      @@ -568,7 +580,7 @@

      How It Compares

      @types
      Unicode scripts 22 Via fontkit Via pdfkit Via @pdf-lib/fontkit
      BiDi (Arabic / Hebrew)
      PDF/A compliance 4 levels
      AES encryption 128 + 256
      Digital signatures RSA + ECDSA
      Barcodes / QR codes 5 formats
      PDF parser / modifier render (chart block) (v1.3.0) add_chart (v1.5.0)<Chart><Chart> (v1.1.0)
      Digital signatures (PAdES CMS)addSignaturePlaceholder + signPdfBytesaddSignaturePlaceholder (v1.2.0) + signPdfBytes sign sign_pdf (+ prepare_signature_placeholder)
      Validate PDF/UAvalidatePdfUAvalidatePdfUA (v1.3.0) inspect --pdfua (v1.1.0) validate_pdf (v1.1.0) — (lintDocument checks the authoring model before rendering, not the emitted PDF)
      ` | -| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` | -| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | +| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` _(v1.1.0)_ | +| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` _(v1.2.0)_ + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | | LTV ladder (B-T → B-LTA) | `signPdfBytesWithTimestamp`, `addValidationInfo`, `addDocumentTimestamp` _(v1.7.0)_ | — (verification only: `verify --revocation`) | `add_ltv`, `timestamp_pdf` _(v1.6.0)_ | — | | Encrypt / decrypt | build-time `encryption` layout option; existing PDFs via the page-tree `encrypt` option and `openPdf` with a password | `encrypt` / `decrypt` _(v1.3.0)_ | `encrypt_pdf` / `decrypt_pdf` _(v1.5.0)_ | build-time only, via the `layout` render option | | Fill / flatten forms | `readFormFields`, `fillForm`, `flattenForm` _(v1.6.0)_ | `fill` _(v1.3.0)_ | `read_form_fields`, `fill_form` _(v1.5.0)_ | — | @@ -58,7 +58,7 @@ means the surface does not offer it. | Merge / split / extract pages | `mergePdfs`, `splitPdf`, `extractPages` _(v1.4.0)_ | `merge` / `split` / `extract` _(v1.2.0)_ | `merge_pdfs` / `split_pdf` / `extract_pages` _(v1.3.0)_ | — | | Markup annotations | `buildAnnotation` + `PdfModifier.addAnnotation` _(v1.5.0)_ | `annotate` _(v1.2.0)_ | `annotate_pdf` _(v1.4.0)_ | — | | Inspect layout (pagination dry run) | `inspectDocumentLayout` _(v1.5.0)_ | `render --inspect-layout` _(v1.2.0)_ | `inspect_layout` _(v1.6.0)_ | `inspectDocument` | -| Validate PDF/UA | `validatePdfUA` | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | +| Validate PDF/UA | `validatePdfUA` _(v1.3.0)_ | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | ## Honest notes diff --git a/docs/guides/cli.html b/docs/guides/cli.html index 6d2669a..f21a552 100644 --- a/docs/guides/cli.html +++ b/docs/guides/cli.html @@ -29,7 +29,7 @@ - + diff --git a/docs/guides/colour-emoji-cli.html b/docs/guides/colour-emoji-cli.html index 2b25d83..01d1adc 100644 --- a/docs/guides/colour-emoji-cli.html +++ b/docs/guides/colour-emoji-cli.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/colour-emoji.html b/docs/guides/colour-emoji.html index 4aebf74..7c1f440 100644 --- a/docs/guides/colour-emoji.html +++ b/docs/guides/colour-emoji.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/debugging.html b/docs/guides/debugging.html index 353c1dc..6011669 100644 --- a/docs/guides/debugging.html +++ b/docs/guides/debugging.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/faq.html b/docs/guides/faq.html index 3083f65..f7d7131 100644 --- a/docs/guides/faq.html +++ b/docs/guides/faq.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/font-validation.html b/docs/guides/font-validation.html index 5519a02..4776d1a 100644 --- a/docs/guides/font-validation.html +++ b/docs/guides/font-validation.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/form-filling.html b/docs/guides/form-filling.html index 6f7e8da..3e39840 100644 --- a/docs/guides/form-filling.html +++ b/docs/guides/form-filling.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/guide.css b/docs/guides/guide.css index 7a938cf..47dfe0f 100644 --- a/docs/guides/guide.css +++ b/docs/guides/guide.css @@ -254,3 +254,23 @@ .guide-source-btn:hover { border-color: var(--c-primary); } .guide-source-link { color: var(--c-text-dim); } + +/* Touch devices have no reliable :hover — the copy buttons and heading + anchors must be visible there, with WCAG 2.5.8-sized targets. */ +@media (hover: none), (pointer: coarse) { + .guide-content pre .copy-btn, + .heading-anchor { + opacity: 1; + } + .heading-anchor { + min-width: 24px; + min-height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + } +} + +@media (max-width: 600px) { + .guide-shell { padding: 48px 14px; } +} diff --git a/docs/guides/guide.js b/docs/guides/guide.js index 720649c..4e3da03 100644 --- a/docs/guides/guide.js +++ b/docs/guides/guide.js @@ -105,14 +105,20 @@ // ── Pre-rendered path ───────────────────────────────────── // build-guides.ts bakes the rendered article and its JSON-LD into the - // shell (rule guide-render-sync keeps it fresh). Nothing to fetch: just - // enhance in place. Prism loads with `defer` after this script, so wait - // for it briefly instead of highlighting a not-yet-loaded page. + // shell (rule guide-render-sync keeps it fresh). Nothing to fetch. The + // layout-affecting enhancements (source bar, copy buttons) run at once — + // deferring them behind the Prism wait used to shift the whole article + // down up to 2s after render. Only the highlighting waits for Prism. if (container.getAttribute('data-prerendered') === 'true') { + addSourceBar(container); + addCopyButtons(container); var tries = 20; - (function enhanceWhenReady() { - if (window.Prism || tries-- <= 0) { enhance(container); return; } - setTimeout(enhanceWhenReady, 100); + (function highlightWhenReady() { + if (window.Prism && typeof window.Prism.highlightAllUnder === 'function') { + window.Prism.highlightAllUnder(container); + return; + } + if (tries-- > 0) setTimeout(highlightWhenReady, 100); })(); return; } diff --git a/docs/guides/ltv.html b/docs/guides/ltv.html index 2df67ea..d307f10 100644 --- a/docs/guides/ltv.html +++ b/docs/guides/ltv.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/mcp.html b/docs/guides/mcp.html index 20b0597..a738f99 100644 --- a/docs/guides/mcp.html +++ b/docs/guides/mcp.html @@ -29,7 +29,7 @@ - + diff --git a/docs/guides/onboarding.html b/docs/guides/onboarding.html index 620e105..64427cd 100644 --- a/docs/guides/onboarding.html +++ b/docs/guides/onboarding.html @@ -29,7 +29,7 @@ - + diff --git a/docs/guides/outlines.html b/docs/guides/outlines.html index ad0ee4b..c6d32ac 100644 --- a/docs/guides/outlines.html +++ b/docs/guides/outlines.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/pdf-manipulation.html b/docs/guides/pdf-manipulation.html index aa01dfe..180317b 100644 --- a/docs/guides/pdf-manipulation.html +++ b/docs/guides/pdf-manipulation.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/pdfa.html b/docs/guides/pdfa.html index 9844fa2..f79fefc 100644 --- a/docs/guides/pdfa.html +++ b/docs/guides/pdfa.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/print.html b/docs/guides/print.html index 3e87a54..16ae92e 100644 --- a/docs/guides/print.html +++ b/docs/guides/print.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/quickstart.html b/docs/guides/quickstart.html index d4703ca..9b562c1 100644 --- a/docs/guides/quickstart.html +++ b/docs/guides/quickstart.html @@ -28,7 +28,7 @@ - + diff --git a/docs/guides/react.html b/docs/guides/react.html index aad6203..ef5a98b 100644 --- a/docs/guides/react.html +++ b/docs/guides/react.html @@ -29,7 +29,7 @@ - + diff --git a/docs/guides/self-verify.html b/docs/guides/self-verify.html index 6e09a6f..63dbaa4 100644 --- a/docs/guides/self-verify.html +++ b/docs/guides/self-verify.html @@ -28,7 +28,7 @@ - + @@ -199,9 +199,9 @@

      Verifier 3 — validat veraPDF as described in PDF/A conformance.

      Verifier 4 — listSignatures() + openPdf(): signatures, encryption, metadata#

      -

      (v1.7.0) listSignatures() inventories every signature field in the -document; openPdf() exposes page count, encryption state and the /Info -dictionary.

      +

      listSignatures() (v1.7.0) inventories every signature field in the +document; openPdf() (available since v1.0.0) exposes page count, encryption +state and the /Info dictionary.

      import { openPdf, listSignatures } from 'pdfnative';
       
       const sigs = listSignatures(signedBytes);
      diff --git a/docs/guides/self-verify.md b/docs/guides/self-verify.md
      index d5b92eb..27b1712 100644
      --- a/docs/guides/self-verify.md
      +++ b/docs/guides/self-verify.md
      @@ -152,9 +152,9 @@ tolerates them.
       
       ## Verifier 4 — `listSignatures()` + `openPdf()`: signatures, encryption, metadata
       
      -_(v1.7.0)_ `listSignatures()` inventories every signature field in the
      -document; `openPdf()` exposes page count, encryption state and the `/Info`
      -dictionary.
      +`listSignatures()` _(v1.7.0)_ inventories every signature field in the
      +document; `openPdf()` (available since v1.0.0) exposes page count, encryption
      +state and the `/Info` dictionary.
       
       ```ts
       import { openPdf, listSignatures } from 'pdfnative';
      diff --git a/docs/guides/signatures.html b/docs/guides/signatures.html
      index 5c91c8f..f16892c 100644
      --- a/docs/guides/signatures.html
      +++ b/docs/guides/signatures.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/guides/streaming.html b/docs/guides/streaming.html
      index 0cc02c6..6399b96 100644
      --- a/docs/guides/streaming.html
      +++ b/docs/guides/streaming.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/guides/tables.html b/docs/guides/tables.html
      index 24e2bfd..23a2c06 100644
      --- a/docs/guides/tables.html
      +++ b/docs/guides/tables.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/guides/text-extraction.html b/docs/guides/text-extraction.html
      index 2d5c240..db63c33 100644
      --- a/docs/guides/text-extraction.html
      +++ b/docs/guides/text-extraction.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/guides/troubleshooting.html b/docs/guides/troubleshooting.html
      index c4f2aad..b419eb2 100644
      --- a/docs/guides/troubleshooting.html
      +++ b/docs/guides/troubleshooting.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/guides/viewer-preferences.html b/docs/guides/viewer-preferences.html
      index 830bc32..7ea0aa6 100644
      --- a/docs/guides/viewer-preferences.html
      +++ b/docs/guides/viewer-preferences.html
      @@ -28,7 +28,7 @@
         
         
         
      -  
      +  
         
       
       
      diff --git a/docs/index.html b/docs/index.html
      index d018c2b..9699767 100644
      --- a/docs/index.html
      +++ b/docs/index.html
      @@ -212,7 +212,7 @@ 

      Pure Native PDF Generation

      -
      2665+
      +
      2686+
      Tests
      @@ -277,7 +277,7 @@

      Rich Content

      Production Ready

      -

      AsyncGenerator streaming (including object-boundary page-by-page), Web Worker off-thread generation, PDF parser & modifier. 2665+ tests across 122 files, 95%+ statement coverage, SLSA provenance.

      +

      AsyncGenerator streaming (including object-boundary page-by-page), Web Worker off-thread generation, PDF parser & modifier. 2686+ tests across 123 files, 95%+ statement coverage, SLSA provenance.

      @@ -429,9 +429,9 @@

      Try It Live

      Pick an example, edit the code, and watch the PDF render beside it — everything runs in your browser via the npm CDN. Nothing is uploaded anywhere.

      -
      - - +
      + +
      diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 9d34f6b..0b1e112 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -56,7 +56,7 @@ - [src/](https://github.com/Nizoka/pdfnative/tree/main/src): Library source (core, crypto, fonts, parser, shaping, types, worker). - [scripts/generators/](https://github.com/Nizoka/pdfnative/tree/main/scripts/generators): 48 sample generators producing 242 reference PDFs. -- [tests/](https://github.com/Nizoka/pdfnative/tree/main/tests): 2665+ tests across 122 files. 95%+ statements measured at v1.6.0; CI enforces ≥88%. +- [tests/](https://github.com/Nizoka/pdfnative/tree/main/tests): 2686+ tests across 123 files. 95%+ statements measured at v1.6.0; CI enforces ≥88%. ## Machine-readable artefacts @@ -155,7 +155,7 @@ Detailed docs: [CLI guide](docs/guides/cli.md) · [MCP guide](docs/guides/mcp.md - **FlateDecode compression** — zlib stream compression (50–90% size reduction), zero-dependency, platform-native - **Web Worker support** — off-main-thread generation for large datasets - **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations -- **Heavily tested** — 2665+ tests across 122 files, fuzz suite, dual-mode visual-regression suite, performance benchmarks; 95.41% statement coverage measured at the v1.6.0 release, with CI enforcing ≥88% statements / 80% branches / 85% functions / 90% lines (vitest.config.ts) +- **Heavily tested** — 2686+ tests across 123 files, fuzz suite, dual-mode visual-regression suite, performance benchmarks; 95.41% statement coverage measured at the v1.6.0 release, with CI enforcing ≥88% statements / 80% branches / 85% functions / 90% lines (vitest.config.ts) - **NPM provenance** — signed builds via GitHub Actions OIDC - **On-device generation** — runs in Node, browsers, Workers, Deno, Bun. No SaaS round-trip; documents never leave the calling process unless your application explicitly sends them - **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home @@ -180,7 +180,7 @@ npm install pdfnative - ❓ **FAQ:** [docs/guides/faq.md](docs/guides/faq.md) — fonts, encryption, signatures, comparisons. - 🤖 **Agentic workflows:** [docs/guides/agentic-workflows.md](docs/guides/agentic-workflows.md) — extend the engine at runtime (register fonts without a release) and embed agent-generated images. - 🛠️ **Troubleshooting:** [docs/guides/troubleshooting.md](docs/guides/troubleshooting.md) — common pitfalls. -- 🎮 **Playgrounds:** nine interactive demos at [docs/playgrounds/](docs/playgrounds/) — [extreme-scripts](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests), [all-scripts](docs/playgrounds/all-scripts.html) (every Unicode script), [scale](docs/playgrounds/scale.html) (1k-100k pages, Web Worker + true streaming), [authoring-plus](docs/playgrounds/authoring-plus.html) (SVG text, layout overlay, annotations), [toolkit](docs/playgrounds/toolkit.html) (bookmarks, page labels, viewer prefs, merge/split/extract, and v1.6.0 form fill/flatten), [charts](docs/playgrounds/charts.html) (native vector charts — all 9 v1.7.0 types), plus [cli](docs/playgrounds/cli.html), [mcp](docs/playgrounds/mcp.html) and [react](docs/playgrounds/react.html) ecosystem explorers. +- 🎮 **Playgrounds:** ten interactive demos at [docs/playgrounds/](docs/playgrounds/) — [extreme-scripts](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests), [all-scripts](docs/playgrounds/all-scripts.html) (every Unicode script), [scale](docs/playgrounds/scale.html) (1k-100k pages, Web Worker + true streaming), [authoring-plus](docs/playgrounds/authoring-plus.html) (SVG text, layout overlay, annotations), [toolkit](docs/playgrounds/toolkit.html) (bookmarks, page labels, viewer prefs, merge/split/extract, and v1.6.0 form fill/flatten), [charts](docs/playgrounds/charts.html) (native vector charts — all 9 v1.7.0 types), [inspect](docs/playgrounds/inspect.html) (drop any PDF and read a full report, entirely client-side), plus [cli](docs/playgrounds/cli.html), [mcp](docs/playgrounds/mcp.html) and [react](docs/playgrounds/react.html) ecosystem explorers. - 🧪 **Sample PDFs:** [scripts/generators/](scripts/generators/) — 242 sample PDFs across 37 categories (48 generators) (see [Sample PDFs](#sample-pdfs) below). - 🎓 **Learn:** [docs/learn/](docs/learn/) — an eight-step guided path from installing the package to generating 100,000 pages, live at [pdfnative.dev/learn/](https://pdfnative.dev/learn/). - 🤖 **Machine-readable:** [llms.txt](llms.txt) (doc index for LLMs), [AGENTS.md](AGENTS.md) (agent guidance), and [docs/assets/ecosystem.json](docs/assets/ecosystem.json) — the canonical manifest of ecosystem versions and counts, enforced by `npm run verify:docs`. @@ -1290,7 +1290,7 @@ src/ fonts/ # Pre-built font data modules (22 scripts) tools/ # CLI: build-font-data.cjs (TTF → JS module) scripts/ # Modular sample PDF generation (48 generators, 242 PDFs) -tests/ # 2665+ tests (122 files: unit + integration + fuzz + parser) +tests/ # 2686+ tests (123 files: unit + integration + fuzz + parser + docs) bench/ # Performance benchmarks (vitest bench) ``` @@ -1302,7 +1302,7 @@ cd pdfnative npm install npm run build # tsup → dist/ (ESM + CJS + .d.ts) -npm run test # vitest run (2665+ tests) +npm run test # vitest run (2686+ tests) npm run test:coverage # vitest with v8 coverage (95.41% statements at the v1.6.0 release; CI gates: 88/80/85/90) npm run test:generate # Generate 242 sample PDFs → test-output/ npm run lint # ESLint 9 + typescript-eslint strict @@ -1317,7 +1317,7 @@ npm run bench # Performance benchmarks (vitest bench) | Metric | Value | |--------|-------| -| Tests | 2665+ (122 files) | +| Tests | 2686+ (123 files) | | Statement coverage | 95.41% (measured at the v1.6.0 release; CI enforces ≥88%, vitest.config.ts) | | Branch coverage | 87.79% (measured at the v1.6.0 release; CI enforces ≥80%) | | Function coverage | 98.5% (measured at the v1.6.0 release; CI enforces ≥85%; lines gate: ≥90%) | @@ -2677,8 +2677,8 @@ means the surface does not offer it. |---|---|---|---|---| | Generate documents | `buildDocumentPDFBytes` / `buildPDFBytes` | `render` | `generate_basic_pdf` (+ the dedicated document tools) | `renderToBytes` and friends, `` tree | | Smart tables | `table` block | `render` (`table` block, or `--variant table`) | `add_table` | `

      ` | -| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` | -| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | +| Native vector charts | `chart` block _(v1.6.0)_ | `render` (`chart` block) _(v1.3.0)_ | `add_chart` _(v1.5.0)_ | `` _(v1.1.0)_ | +| Digital signatures (PAdES CMS) | `addSignaturePlaceholder` _(v1.2.0)_ + `signPdfBytes` | `sign` | `sign_pdf` (+ `prepare_signature_placeholder`) | — | | LTV ladder (B-T → B-LTA) | `signPdfBytesWithTimestamp`, `addValidationInfo`, `addDocumentTimestamp` _(v1.7.0)_ | — (verification only: `verify --revocation`) | `add_ltv`, `timestamp_pdf` _(v1.6.0)_ | — | | Encrypt / decrypt | build-time `encryption` layout option; existing PDFs via the page-tree `encrypt` option and `openPdf` with a password | `encrypt` / `decrypt` _(v1.3.0)_ | `encrypt_pdf` / `decrypt_pdf` _(v1.5.0)_ | build-time only, via the `layout` render option | | Fill / flatten forms | `readFormFields`, `fillForm`, `flattenForm` _(v1.6.0)_ | `fill` _(v1.3.0)_ | `read_form_fields`, `fill_form` _(v1.5.0)_ | — | @@ -2686,7 +2686,7 @@ means the surface does not offer it. | Merge / split / extract pages | `mergePdfs`, `splitPdf`, `extractPages` _(v1.4.0)_ | `merge` / `split` / `extract` _(v1.2.0)_ | `merge_pdfs` / `split_pdf` / `extract_pages` _(v1.3.0)_ | — | | Markup annotations | `buildAnnotation` + `PdfModifier.addAnnotation` _(v1.5.0)_ | `annotate` _(v1.2.0)_ | `annotate_pdf` _(v1.4.0)_ | — | | Inspect layout (pagination dry run) | `inspectDocumentLayout` _(v1.5.0)_ | `render --inspect-layout` _(v1.2.0)_ | `inspect_layout` _(v1.6.0)_ | `inspectDocument` | -| Validate PDF/UA | `validatePdfUA` | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | +| Validate PDF/UA | `validatePdfUA` _(v1.3.0)_ | `inspect --pdfua` _(v1.1.0)_ | `validate_pdf` _(v1.1.0)_ | — (`lintDocument` checks the authoring model before rendering, not the emitted PDF) | ## Honest notes @@ -7714,9 +7714,9 @@ tolerates them. ## Verifier 4 — `listSignatures()` + `openPdf()`: signatures, encryption, metadata -_(v1.7.0)_ `listSignatures()` inventories every signature field in the -document; `openPdf()` exposes page count, encryption state and the `/Info` -dictionary. +`listSignatures()` _(v1.7.0)_ inventories every signature field in the +document; `openPdf()` (available since v1.0.0) exposes page count, encryption +state and the `/Info` dictionary. ```ts import { openPdf, listSignatures } from 'pdfnative'; diff --git a/docs/llms-index.json b/docs/llms-index.json index d090e49..0311a89 100644 --- a/docs/llms-index.json +++ b/docs/llms-index.json @@ -11,26 +11,26 @@ { "url": "https://pdfnative.dev/agent-brief.md", "description": "Compact paste-into-context briefing for coding agents: core API, verified pitfalls, surface decision tree, self-verification loop.", - "bytes": 6959, - "approxTokens": 1740 + "bytes": 7114, + "approxTokens": 1779 }, { "url": "https://pdfnative.dev/llms-full.txt", "description": "Full corpus: index + README + every guide, one request.", - "bytes": 485843, - "approxTokens": 121461 + "bytes": 486009, + "approxTokens": 121502 }, { "url": "https://pdfnative.dev/llms-recipes.txt", "description": "Executable recipes: CI-verified, copy-ready code for the most common tasks.", - "bytes": 24351, - "approxTokens": 6088 + "bytes": 24446, + "approxTokens": 6112 }, { "url": "https://github.com/Nizoka/pdfnative/blob/main/README.md", "description": "Complete feature and API reference (also embedded in llms-full.txt).", - "bytes": 97538, - "approxTokens": 24385 + "bytes": 97646, + "approxTokens": 24412 } ], "guides": [ @@ -144,8 +144,8 @@ "you-can-switch-later", "further-reading" ], - "bytes": 7177, - "approxTokens": 1794 + "bytes": 7210, + "approxTokens": 1803 }, { "title": "pdfnative-cli — Command-Line Interface Guide", @@ -467,8 +467,8 @@ "wire-it-into-ci-and-agent-loops", "further-reading" ], - "bytes": 13802, - "approxTokens": 3451 + "bytes": 13827, + "approxTokens": 3457 }, { "title": "Digital signatures in pdfnative", diff --git a/docs/llms-recipes.txt b/docs/llms-recipes.txt index cb6cfbd..136346d 100644 --- a/docs/llms-recipes.txt +++ b/docs/llms-recipes.txt @@ -177,8 +177,10 @@ import * as notoSans from 'pdfnative/fonts/noto-sans-data.js'; // PDF/A requires every rendered glyph to come from an embedded font // (ISO 19005 §6.2.11.4.1); the bundled Noto Sans data module covers Latin. +// /F1 and /F2 are reserved by the engine — custom fontRefs start at /F3. const latinFont: FontEntry = { - fontRef: '/F1', + fontRef: '/F3', + lang: 'latin', fontData: notoSans as unknown as FontData, }; diff --git a/docs/llms.txt b/docs/llms.txt index 577e5f5..fa255d5 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -56,7 +56,7 @@ - [src/](https://github.com/Nizoka/pdfnative/tree/main/src): Library source (core, crypto, fonts, parser, shaping, types, worker). - [scripts/generators/](https://github.com/Nizoka/pdfnative/tree/main/scripts/generators): 48 sample generators producing 242 reference PDFs. -- [tests/](https://github.com/Nizoka/pdfnative/tree/main/tests): 2665+ tests across 122 files. 95%+ statements measured at v1.6.0; CI enforces ≥88%. +- [tests/](https://github.com/Nizoka/pdfnative/tree/main/tests): 2686+ tests across 123 files. 95%+ statements measured at v1.6.0; CI enforces ≥88%. ## Machine-readable artefacts diff --git a/docs/playgrounds/inspect.html b/docs/playgrounds/inspect.html index a921576..4e616e2 100644 --- a/docs/playgrounds/inspect.html +++ b/docs/playgrounds/inspect.html @@ -120,7 +120,7 @@

      Inspect & verify — drop a PDF, nothing leaves your machine

      Drop any PDF below and read a full report — document metadata, extracted text, PDF/UA structure, a signature inventory, and form fields — produced by the same pdfnative reader APIs that run in Node, Deno and Bun. Open your browser's Network panel before dropping a file: the only requests this page ever makes happen while it loads — its stylesheets, the Prism highlighter, and the pdfnative engine from the CDN — before you drop anything. The file is read with the standard FileReader API and never leaves this tab. Nothing is stored either: reload the page and the report is gone.

      In the spirit of the responsibility page, every card below links the source file in the repository that produced it — the report is only as trustworthy as the code you can read.

      -

      Loading pdfnative from esm.sh… this happens once, at page load.

      +

      Loading pdfnative from esm.sh… this happens once, at page load.

      Drop a PDF anywhere in this box — or pick one:

      @@ -140,7 +140,7 @@

      Inspect & verify — drop a PDF, nothing leaves your machine

      This document is encrypted. The password is handed to openPdf() in this tab only — it is never stored and never sent anywhere.

      - +