diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 94d0b7b..66bc4c8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -77,10 +77,10 @@ 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/ # 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. @@ -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) @@ -113,14 +113,14 @@ 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 # 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) - 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 @@ -186,7 +186,7 @@ npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms - Watermarks: ExtGState for transparency (`/ca opacity`), text rotation via `cos(θ) sin(θ) -sin(θ) cos(θ) cx cy Tm`, image centering with aspect ratio - Watermark validation: PDF/A-1b blocks transparency (ISO 19005-1 §6.4) — `validateWatermark()` throws - Watermark position: `'background'` = ops before content stream; `'foreground'` = ops after content stream -- Table of contents: `TocBlock` with multi-pass pagination (max 3 passes), `_renderToc()` with dot leaders, right-aligned page numbers +- Table of contents: `TocBlock` with multi-pass pagination (max 3 passes), `renderToc()` with dot leaders, right-aligned page numbers - TOC internal links: named destinations `/Dests << /toc_h_N [pageObj /XYZ x y null] >>` in catalog; annotations use `/Dest /toc_h_N` (not `/URI`) - TOC tagged mode: `/TOC` structure element with `/TOCI` children for PDF/UA compliance - Smart tables (v1.2.0): `TableBlock` gains six optional fields — `wrap` (`'auto'`|`'always'`|`'never'`, default `'auto'`), `repeatHeader` (default `true`), `zebra` (`boolean|PdfColor`, default `false`, true uses `'0.969 0.973 0.984'`), `caption`, `minRowHeight` (default `12`), `cellPadding` (default `3`). Architecture: `planTable()` in `pdf-renderers.ts` measures once; `_paginateBlocks()` in `pdf-document.ts` slices at row boundaries into `TableSlice` items; `renderTable()` is page-lifecycle-free and accepts an optional `slice` arg. Tagged-mode `/Table` continues across slices via shared `tableStructAccum` array (ISO 14289-1 §7.10.6); `/Caption` emitted once. Single-page tables that fit without wrapping are byte-identical to v1.1.0 in their **body** rendering (header baseline `+4`, data baseline `+3`, `ROW_H=12`, `TH_H=15` preserved); right- and centre-aligned **header** glyph positioning shifts 2–5pt because v1.2.0 corrects a pre-1.2.0 width-measurement bug (see next bullet). `planTable()` and `TableSlice` are internal — NOT re-exported from `src/index.ts`. @@ -256,7 +256,7 @@ npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms - Latin VF (PDF/A): Noto Sans VF (OFL-1.1) bundled as `fonts/noto-sans-data.{js,d.ts}` under lang `'latin'`. Activates automatically for PDF/A documents containing non-WinAnsi Latin (curly quotes, em-dash, ellipsis…). Opt-in via `registerFont('latin', () => import('pdfnative/fonts/noto-sans-data.js'))`. - Document outline/bookmarks (v1.4.0): `buildOutlineObjects(items, startObjNum, pageObjNumFor, defaultY, fmtNum, pageCount)` in `src/core/pdf-outline.ts` builds the `/Outlines` tree (`/First /Last /Next /Prev /Parent /Count`, nested children, `/F` flags bold=2 italic=1, `/C` color, `/Dest [pageObj /XYZ 0 y null]`). Titles via `encodePdfTextString`. `OutlineItem.open?: boolean` (default true) — `false` emits a **negative** `/Count` (collapsed, ISO 32000-1 §12.3.3); a collapsed node contributes only itself (not its hidden descendants) to ancestors' visible counts (`openDescendantCount` magnitude + sign by `open`). Wired into `pdf-document.ts` as **trailing indirect objects** appended after embedded files (`totalObjs = outlineStart + built.totalObjects - 1`) so the catalog-rewrite offset-adjustment loop covers them — same pattern as colour-emoji/embedded-files. `DocumentParams.outline?: readonly OutlineItem[] | 'auto'`; `'auto'` derives a nested tree from heading levels via `autoOutlineFromHeadings()`. Catalog gains `/Outlines N 0 R`. - Page labels (v1.4.0): `buildPageLabelsDict(ranges, pageCount)` in `src/core/pdf-page-labels.ts` emits an inline `/PageLabels << /Nums [...] >>` number tree. `PageLabelStyle` decimal/roman/Roman/alpha/Alpha/none → `/S /D|r|R|a|A` (none omits `/S`). `PageLabelRange { startPage, style?, prefix?, start? }` — validated for ordering/bounds; prefix parens escaped. `DocumentParams.pageLabels?: readonly PageLabelRange[]`. Inline (not indirect) so no object-number impact. -- Page-tree manipulation (v1.4.0): `mergePdfs(sources, opts?)`, `splitPdf(src, ranges)`, `extractPages(src, indices)` in `src/parser/pdf-pagetree.ts` deep-copy kept pages + their transitive object graph into a fresh object-number space (obj 1=Catalog, 2=Pages, 3+=graph). `copyObject()` is memoized per-reader and cycle-safe; `rewrite()`/`copyObject()`/`serializeStreamBody()` thread a `depth` capped at `MAX_COPY_DEPTH=2000` (throws on deeper nesting/ref-chains — stack-overflow hardening). `resolveInherited()` folds MediaBox/CropBox/Rotate/Resources from ancestors onto each page; `filterAnnotations()` keeps only URI `/Link` annots; `serializeValue/Dict` are binary-safe (Latin-1). `serializeDocument()` emits a deterministic content-addressed trailer `/ID [ ]` via `md5(...)` imported from `core/pdf-encrypt.js` (parser→core is an allowed edge). `assertNotEncrypted()` throws on `/Encrypt`. `MAX_MERGE_SOURCES=50`. `MergeOptions { dropSignatures?, dropAnnotations?, maxOutputSize?, password?, encrypt? }` (`password` decrypts encrypted sources and `encrypt` re-encrypts the rebuilt output — both added in v1.6.0) — `maxOutputSize` (default `DEFAULT_MAX_OUTPUT_SIZE = 256*1024*1024`; `Infinity` disables) is a secure-by-default output ceiling enforced via `accountBytes()`/`setBody()` and a pre-flight `accountBytes(ctx, stream.data.length)` in `serializeStreamBody()` *before* the Latin-1 conversion (rejects multi-GB streams before they materialise — OOM hardening); validated at entry via `resolveMaxOutputSize()`. `splitPdf(src, ranges, opts?)` and `extractPages(src, indices, opts?)` also accept `MergeOptions` (additive trailing param). Full rebuild (not in-place surgery) — unblocks `pdfnative-mcp` merge_pdfs/split_pdf. +- Page-tree manipulation (v1.4.0): `mergePdfs(sources, opts?)`, `splitPdf(src, ranges)`, `extractPages(src, indices)` in `src/parser/pdf-pagetree.ts` deep-copy kept pages + their transitive object graph into a fresh object-number space (obj 1=Catalog, 2=Pages, 3+=graph). `copyObject()` is memoized per-reader and cycle-safe; `rewrite()`/`copyObject()`/`serializeStreamBody()` thread a `depth` capped at `MAX_COPY_DEPTH=2000` (throws on deeper nesting/ref-chains — stack-overflow hardening). `resolveInherited()` folds MediaBox/CropBox/Rotate/Resources from ancestors onto each page; `filterAnnotations()` keeps only URI `/Link` annots; `serializeValue/Dict` are binary-safe (Latin-1). `serializeDocument()` emits a deterministic content-addressed trailer `/ID [ ]` via `md5(...)` imported from `core/pdf-encrypt.js` (parser→core is an allowed edge). `MAX_MERGE_SOURCES=50`. `MergeOptions { dropSignatures?, dropAnnotations?, maxOutputSize?, password?, encrypt? }` (`password` decrypts encrypted sources and `encrypt` re-encrypts the rebuilt output — both added in v1.6.0) — `maxOutputSize` (default `DEFAULT_MAX_OUTPUT_SIZE = 256*1024*1024`; `Infinity` disables) is a secure-by-default output ceiling enforced via `accountBytes()`/`setBody()` and a pre-flight `accountBytes(ctx, stream.data.length)` in `serializeStreamBody()` *before* the Latin-1 conversion (rejects multi-GB streams before they materialise — OOM hardening); validated at entry via `resolveMaxOutputSize()`. `splitPdf(src, ranges, opts?)` and `extractPages(src, indices, opts?)` also accept `MergeOptions` (additive trailing param). Full rebuild (not in-place surgery) — unblocks `pdfnative-mcp` merge_pdfs/split_pdf. - streamToFile (v1.4.0): `streamToFile(stream, filePath, { signal? })` in `src/core/pdf-stream-writer.ts` drains any `AsyncGenerator` to disk in Node, honouring write back-pressure (awaits `'drain'`) and `AbortSignal`; returns `{ bytesWritten, path }`. On abort (pre-start, mid-iter, or post-loop) or any write error it releases the fd (awaits `'close'`) and best-effort `fs.rmSync(filePath, { force: true })` so no orphaned partial file is left behind. Uses top-level `import type * as NodeFs from 'node:fs'` + dynamic `await import('node:fs')` (no static node dep — keeps browser/Deno builds clean). ESLint forbids inline `import()` type annotations, so the type-only import MUST be top-level. - COLRv1 advanced compositing (v1.4.0): `colr-parser.ts` resolves `PaintSweepGradient` (format 8) → `SweepGradientPaint { kind:'sweep', center, startAngle, endAngle, stops, extend }` (matrix rotation folded via `Math.atan2`), and `PaintComposite` (format 32) → backdrop+source layers with the source tagged `ColorLayer.blendMode` via `compositeModeToBlendMode(mode)` (separable modes 3/13–27 → Normal/Multiply/Screen/Overlay/Darken/Lighten/ColorDodge/ColorBurn/HardLight/SoftLight/Difference/Exclusion/Hue/Saturation/Color/Luminosity; structural Porter-Duff modes → `null` → `UnsupportedPaint` → mono fallback). `pdf-color-glyph.ts`: `emitSweep()` renders flat triangular wedges (no `/Shading` resource); unified `gsFor(alpha, bm)` ExtGState helper combines `/ca`+`/BM`. New types `SweepGradientPaint` + `ColorLayer.blendMode?` in `src/types/pdf-types.ts`. - Pluggable signature crypto provider (v1.4.0): `src/crypto/crypto-provider.ts` mirrors the `setDeflateImpl` global-registry pattern — module-level `_cryptoProvider`, `setCryptoProvider(provider | null)`, `getCryptoProvider()`. `CryptoProvider { sign(tbs, algorithm): Uint8Array }` receives the **DER-encoded CMS signed attributes** (`signedAttrsForSig`, a SET tag 0x31) and hashes them internally (native `crypto.sign('sha256', …)`); RSA → PKCS#1 v1.5 over SHA-256(tbs), ECDSA → DER-encoded sig. Dispatch lives in `buildSignerInfo()` in `src/crypto/cms.ts`: `const provider = options.provider ?? getCryptoProvider()` — when present, `signatureValue = provider.sign(...)` and `rsaKey`/`ecKey` are not required; else the pure-JS `rsaSignHash`/`ecdsaSignHash` fallback runs. `CmsSignOptions.provider` and `PdfSignOptions.provider` thread the per-call provider (per-call wins over global). Exported from `src/index.ts`. This is the in-library escape hatch for the SECURITY.md BigInt timing caveat. @@ -305,7 +305,7 @@ npm run verify:docs # 17 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/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/pdf-core.instructions.md b/.github/instructions/pdf-core.instructions.md index 8f1c706..baf6b22 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) @@ -201,7 +201,7 @@ applyTo: "src/core/**" - `headerTemplate` / `footerTemplate` on `PdfLayoutOptions` — both builders support them - `HEADER_H = 15` constant in `pdf-layout.ts` — header zone reduces available content height - Backward compat: `footerText` maps to `{ left: footerText, right: '{page}/{pages}' }` -- `_renderPageTemplate()` (pdf-document.ts) / `_buildPageTemplate()` (pdf-builder.ts) — renders left/center/right at given Y +- `renderPageTemplate()` (pdf-document.ts) / `_buildPageTemplate()` (pdf-builder.ts) — renders left/center/right at given Y - Default color from `colors.footer` (`PdfColor`), parsed via `parseColor()` - Tagged mode: template text wrapped in `/P` structure elements with marked content @@ -225,9 +225,9 @@ applyTo: "src/core/**" - **Document builder only** — table builder has no headings concept - **Multi-pass pagination** (max 3 iterations): 1. Pass 1: paginate without TOC → collect `HeadingDestination[]` (destName, text, level, pageIndex, y) - 2. Pass 2: estimate TOC height via `_estimateTocHeight()`, re-paginate with TOC height included + 2. Pass 2: estimate TOC height via `estimateTocHeight()`, re-paginate with TOC height included 3. Pass 3 (if needed): if heading page assignments shifted, re-paginate one more time -- `_renderToc()`: renders TOC title (bold, larger font), indented entries with dot leaders, right-aligned page numbers +- `renderToc()`: renders TOC title (bold, larger font), indented entries with dot leaders, right-aligned page numbers - TOC entries are `/GoTo` annotations: `<< /Type /Annot /Subtype /Link /Rect [...] /Dest /toc_h_N >>` - Annotations starting with `#` prefix → `/Dest` (internal); others → `/URI` (external) - **Named destinations** in catalog: `/Dests << /toc_h_0 [pageObj /XYZ x y null] ... >>` @@ -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/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/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 97bf442..a2fa9fb 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 +- **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/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..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 (2396+ 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/.github/workflows/docs.yml b/.github/workflows/docs.yml index 75b7ed5..21c8dd5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,8 +24,12 @@ on: - 'docs/**' - '**.md' - 'llms.txt' + - 'recipes/**' + - 'bench/**' - 'scripts/verify-docs.ts' - 'scripts/build-llms-full.ts' + - 'scripts/build-guides.ts' + - 'scripts/build-api-json.ts' - '.github/workflows/docs.yml' # The api-exists rule cross-checks documented identifiers against src/, # so a rename there can break the docs without touching them. @@ -37,8 +41,12 @@ on: - 'docs/**' - '**.md' - 'llms.txt' + - 'recipes/**' + - 'bench/**' - 'scripts/verify-docs.ts' - 'scripts/build-llms-full.ts' + - 'scripts/build-guides.ts' + - 'scripts/build-api-json.ts' - '.github/workflows/docs.yml' - 'src/**' - 'package.json' diff --git a/AGENTS.md b/AGENTS.md index 58031f1..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. 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. 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,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 (2686+ 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 ``` @@ -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/CONTRIBUTING.md b/CONTRIBUTING.md index e7e57b5..1dd91a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,12 +54,12 @@ 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/ 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 # 22 offline rules over docs/, playgrounds, README, llms files npm run bench # Performance benchmarks (vitest bench) ``` @@ -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 4903d5b..9307dd5 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 @@ -77,11 +77,11 @@ 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 -- **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 @@ -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`. @@ -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 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) @@ -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.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 @@ -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, 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) | -| `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 @@ -1206,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) ``` @@ -1218,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 @@ -1233,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/ROADMAP.md b/ROADMAP.md index 74aae2b..cdd2826 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,11 +33,11 @@ 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) -- [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/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/agent-brief.md b/docs/agent-brief.md new file mode 100644 index 0000000..2908cdf --- /dev/null +++ b/docs/agent-brief.md @@ -0,0 +1,125 @@ +# 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 +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 + +- **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 / trapped (v1.7.0) + 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 + `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`. +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..aade23e 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,12 +87,78 @@ 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); }); }); }); + // ── 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 copyText(text); }) + .then(function () { + var prev = btn.textContent; + btn.textContent = '✓ Copied'; + setTimeout(function () { btn.textContent = prev; }, 1500); + }) + .catch(function () { btn.textContent = 'Copy failed'; }); + }); + }); + + // ── 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'); + // 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) { @@ -159,6 +246,16 @@ 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 demoJson = document.getElementById('demo-json'); + var demoJsonNote = document.getElementById('demo-json-note'); + var demoModeCode = document.getElementById('demo-mode-code'); + var demoModeJson = document.getElementById('demo-mode-json'); + var demoCopyCli = document.getElementById('demo-copy-cli'); + var demoCopyMcp = document.getElementById('demo-copy-mcp'); var pdfnativeModule = null; // ── Examples gallery ────────────────────────────────────── @@ -529,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';", '', @@ -591,6 +690,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 +711,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 +726,404 @@ 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. + // 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; + } + + 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; + } + + // 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' }; + 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; + } + + if (demoDownload) { + demoDownload.addEventListener('click', function () { + if (lastPdf && pdfnativeModule) pdfnativeModule.downloadBlob(lastPdf.bytes, lastPdf.name); + }); + } + + // ── JSON mode: the DocumentParams the whole ecosystem consumes ── + var demoMode = 'code'; + var BLOCK_TYPES = ['heading', 'paragraph', 'list', 'table', 'image', 'link', 'toc', 'barcode', 'svg', 'formField', 'chart', 'pageBreak', 'spacer']; + var STARTER_DOC = { + title: 'Shared document', + blocks: [ + { type: 'heading', text: 'Shared document', level: 1 }, + { type: 'paragraph', text: 'Edit this JSON and generate — the same object drives the library, pdfnative-cli render, and the generate_basic_pdf MCP tool.' }, + { type: 'table', headers: ['Surface', 'Entry point'], rows: [ + { cells: ['Library', 'buildDocumentPDFBytes(params)'] }, + { cells: ['CLI', 'pdfnative render --input doc.json'] }, + { cells: ['MCP', 'generate_basic_pdf'] } + ] } + ] + }; + + 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; + if (json && !demoJson.value) demoJson.value = JSON.stringify(STARTER_DOC, null, 2); + } + if (demoPicker) demoPicker.disabled = json; + if (demoSourceLink) demoSourceLink.hidden = json; + if (demoJsonNote) demoJsonNote.hidden = !json; + if (demoCopyCli) demoCopyCli.hidden = !json; + if (demoCopyMcp) demoCopyMcp.hidden = !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'); }); + + // Defensive parse: a #doc= payload is untrusted input. Data only — the + // hash can never carry code — with a size cap and a block-type check + // against the engine's 13-kind union. Anything off-shape is rejected. + function parseDocJson(text) { + if (typeof text !== 'string' || text.length > 100000) throw new Error('Document JSON too large.'); + var doc = JSON.parse(text); + if (!doc || typeof doc !== 'object' || Array.isArray(doc)) throw new Error('Document must be a JSON object.'); + if (!Array.isArray(doc.blocks)) throw new Error('Document needs a "blocks" array.'); + for (var i = 0; i < doc.blocks.length; i++) { + var b = doc.blocks[i]; + if (!b || typeof b !== 'object' || BLOCK_TYPES.indexOf(b.type) === -1) { + throw new Error('blocks[' + i + '].type must be one of: ' + BLOCK_TYPES.join(', ')); + } + // Shared #doc= payloads are untrusted: only the schemes the engine + // itself allows may reach a link annotation. + if (b.type === 'link' && typeof b.url === 'string' && !/^(https?:|mailto:|#)/i.test(b.url)) { + throw new Error('blocks[' + i + '].url must use http:, https:, mailto: or a #fragment.'); + } + } + return doc; + } + + // #doc= payload: base64url, deflate-raw-compressed when the native + // CompressionStream API exists ("d." prefix), raw otherwise ("r."). + function toBase64Url(bytes) { + var bin = ''; + for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + function fromBase64Url(s) { + s = s.replace(/-/g, '+').replace(/_/g, '/'); + var bin = atob(s); + var bytes = new Uint8Array(bin.length); + for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return bytes; + } + async function encodeDocHash(jsonText) { + var raw = new TextEncoder().encode(jsonText); + if (typeof CompressionStream === 'function') { + var stream = new Blob([raw]).stream().pipeThrough(new CompressionStream('deflate-raw')); + var packed = new Uint8Array(await new Response(stream).arrayBuffer()); + return 'd.' + toBase64Url(packed); + } + return 'r.' + toBase64Url(raw); + } + async function decodeDocHash(payload) { + if (payload.length > 20000) throw new Error('Link payload too large.'); + var kind = payload.slice(0, 2); + var bytes = fromBase64Url(payload.slice(2)); + if (kind === 'd.') { + if (typeof DecompressionStream !== 'function') throw new Error('This browser cannot decompress the link.'); + // Read the inflated stream chunk by chunk and abort past the JSON + // size cap — deflate can expand ~1000:1, so never materialise an + // unbounded payload before checking its size. + var reader = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw')).getReader(); + var chunks = []; + var total = 0; + for (;;) { + var step = await reader.read(); + if (step.done) break; + total += step.value.length; + if (total > 150000) { + reader.cancel(); + throw new Error('Link payload too large after decompression.'); + } + chunks.push(step.value); + } + bytes = new Uint8Array(total); + var off = 0; + for (var ci = 0; ci < chunks.length; ci++) { bytes.set(chunks[ci], off); off += chunks[ci].length; } + } else if (kind !== 'r.') { + throw new Error('Unrecognised link format.'); + } + return new TextDecoder().decode(bytes); + } + + if (demoShare) { + demoShare.addEventListener('click', async function () { + var base = location.origin + location.pathname; + var url; + try { + if (demoMode === 'json') { + var doc = parseDocJson(demoJson.value); + url = base + '#doc=' + (await encodeDocHash(JSON.stringify(doc))); + } else { + url = base + '#preset=' + (currentId || DEFAULT_ID); + } + await copyText(url); + var prev = demoShare.textContent; + demoShare.textContent = '✓ Copied'; + setTimeout(function () { demoShare.textContent = prev; }, 1500); + } catch (e) { + demoError.textContent = 'Share failed: ' + (e.message || e); + demoError.style.display = 'block'; + } + }); + } + + function copyWithFeedback(btn, text) { + copyText(text).then(function () { + var prev = btn.textContent; + btn.textContent = '✓ Copied'; + setTimeout(function () { btn.textContent = prev; }, 1500); + }); + } + if (demoCopyCli) { + demoCopyCli.addEventListener('click', function () { + try { + var doc = parseDocJson(demoJson.value); + copyWithFeedback(demoCopyCli, + '# Save the JSON below as doc.json, then:\n' + + 'npx pdfnative-cli render --input doc.json --output out.pdf\n\n' + + JSON.stringify(doc, null, 2) + '\n'); + } catch (e) { demoError.textContent = e.message; demoError.style.display = 'block'; } + }); + } + if (demoCopyMcp) { + demoCopyMcp.addEventListener('click', function () { + try { + var doc = parseDocJson(demoJson.value); + copyWithFeedback(demoCopyMcp, JSON.stringify({ + tool: 'generate_basic_pdf', + input: { title: doc.title || 'Document', blocks: doc.blocks, outputMode: 'base64' } + }, null, 2) + '\n'); + } catch (e) { demoError.textContent = e.message; demoError.style.display = 'block'; } + }); + } + + // 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) { + 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 */ }); + } + + // ── 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 + // Named imports — single- or multi-line. + .replace( + /^(\s*)import\s*\{([\s\S]*?)\}\s*from\s*['"]pdfnative['"]\s*;?[ \t]*$/gm, + '$1const {$2} = globalThis.__pdfnativeDemo.mod;' + ) + // Namespace imports: import * as pdf from 'pdfnative'. + .replace( + /^(\s*)import\s*\*\s*as\s+([A-Za-z_$][\w$]*)\s+from\s*['"]pdfnative['"]\s*;?[ \t]*$/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 - ); + if (demoMode === 'json') { + var doc = parseDocJson(demoJson.value); + captureSink(pdfnativeModule.buildDocumentPDFBytes(doc), (doc.title || 'document') + '.pdf'); + } else { + globalThis.__pdfnativeDemo = { + mod: Object.assign({}, pdfnativeModule, { downloadBlob: captureSink }) + }; - 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 - ); + var source = rewriteImports(demoCode.value); + if (/from\s*['"]pdfnative['"]/.test(source)) { + throw new Error("This demo can only resolve `import { name } from 'pdfnative'` or `import * as pdf from 'pdfnative'` — rewrite the import in one of those forms."); + } + moduleUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); + await import(moduleUrl); + } - demoStatus.textContent = 'PDF generated!'; + 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); + + // ── Live benchmark (never reuses .bench-label/.bench-value: those + // classes are tied to bench/RESULTS.md by the bench-parity CI rule) ── + var benchBtn = document.getElementById('bench-live-run'); + if (benchBtn) { + benchBtn.addEventListener('click', async function () { + var rowsEl = document.getElementById('bench-live-rows'); + var noteEl = document.getElementById('bench-live-note'); + benchBtn.disabled = true; + benchBtn.textContent = 'Loading pdfnative…'; + try { + if (!pdfnativeModule) pdfnativeModule = await loadPdfnative(); + benchBtn.textContent = 'Measuring…'; + var makeParams = function (n) { + var rows = []; + for (var i = 0; i < n; i++) { + rows.push({ cells: ['2026-08-' + ((i % 28) + 1), 'Line item ' + i, '$' + (i * 3.5).toFixed(2)] }); + } + // 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]; + var results = []; + for (var s = 0; s < sizes.length; s++) { + var t0 = performance.now(); + pdfnativeModule.buildPDFBytes(makeParams(sizes[s])); + results.push({ n: sizes[s], ms: performance.now() - t0 }); + await new Promise(function (r) { setTimeout(r, 0); }); // keep the tab responsive + } + var max = Math.max.apply(null, results.map(function (r) { return r.ms; })); + rowsEl.innerHTML = results.map(function (r) { + var pct = Math.max(4, Math.round((r.ms / max) * 100)); + return '
' + + '' + r.n.toLocaleString('en-GB') + ' rows — this device' + + '
' + + '' + r.ms.toFixed(1) + ' ms' + + '
'; + }).join(''); + rowsEl.hidden = false; + noteEl.hidden = false; + } catch (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, 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; + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting && !ran) { + ran = true; + io.disconnect(); + // 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' }); + io.observe(demoSection); + } + } } })(); diff --git a/docs/assets/api.json b/docs/assets/api.json new file mode 100644 index 0000000..9a236a1 --- /dev/null +++ b/docs/assets/api.json @@ -0,0 +1,3123 @@ +{ + "$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": 445, + "exports": [ + { + "name": "AddDocumentTimestampOptions", + "kind": "type", + "module": "src/core/pdf-doc-timestamp.ts", + "signature": "export interface AddDocumentTimestampOptions", + "summary": "Options for addDocumentTimestamp." + }, + { + "name": "AddSignaturePlaceholderOptions", + "kind": "type", + "module": "src/core/pdf-sig-placeholder.ts", + "signature": "export interface AddSignaturePlaceholderOptions", + "summary": "Options for addSignaturePlaceholder." + }, + { + "name": "Annotation", + "kind": "type", + "module": "src/core/pdf-annot.ts", + "signature": "export type Annotation = LinkAnnotation | InternalLink;", + "summary": "Union of all annotation types." + }, + { + "name": "AnnotationBase", + "kind": "type", + "module": "src/core/pdf-annot-markup.ts", + "signature": "export interface AnnotationBase", + "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": "Rectangle `[x1, y1, x2, y2]` in PDF user space (points)." + }, + { + "name": "Asn1Node", + "kind": "type", + "module": "src/crypto/asn1.ts", + "signature": "export interface Asn1Node", + "summary": null + }, + { + "name": "BAL_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const BAL_H = 32;", + "summary": null + }, + { + "name": "BarcodeBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface BarcodeBlock", + "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": "Supported barcode formats." + }, + { + "name": "BidiRun", + "kind": "type", + "module": "src/shaping/bidi.ts", + "signature": "export interface BidiRun", + "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": "Per-cell border configuration for a TableBlock." + }, + { + "name": "ChartBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface ChartBlock", + "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": "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": "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": "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": "CMS profile: - `'pkcs7'` (default) — classic adbe.pkcs7.detached signed attributes: content-type, message-digest, signing-time." + }, + { + "name": "CmsSignOptions", + "kind": "type", + "module": "src/crypto/cms.ts", + "signature": "export interface CmsSignOptions", + "summary": null + }, + { + "name": "CollectLtvOptions", + "kind": "type", + "module": "src/core/pdf-dss.ts", + "signature": "export interface CollectLtvOptions", + "summary": "Options for collectValidationInfo / addValidationInfo." + }, + { + "name": "ColorGlyph", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface ColorGlyph", + "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": "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": "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": "A paint used to fill a colour-glyph layer." + }, + { + "name": "ColorStop", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface ColorStop", + "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": "Column definition for the table layout." + }, + { + "name": "Contour", + "kind": "type", + "module": "src/fonts/glyf-outline.ts", + "signature": "export type Contour = OutlinePoint[];", + "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": "An sRGB colour with alpha, each channel 0–255." + }, + { + "name": "CrlRevokedEntry", + "kind": "type", + "module": "src/crypto/crl.ts", + "signature": "export interface CrlRevokedEntry", + "summary": "One revokedCertificates entry." + }, + { + "name": "CryptFilterMethod", + "kind": "type", + "module": "src/parser/pdf-decrypt.ts", + "signature": "export type CryptFilterMethod = 'V2' | 'AESV2' | 'AESV3' | 'Identity';", + "summary": "Crypt-filter method resolved for streams or strings." + }, + { + "name": "CryptoProvider", + "kind": "type", + "module": "src/crypto/crypto-provider.ts", + "signature": "export interface CryptoProvider", + "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": "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", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_COLORS: PdfColors =", + "summary": null + }, + { + "name": "DEFAULT_COLUMNS", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_COLUMNS: ColumnDef[] = [", + "summary": null + }, + { + "name": "DEFAULT_CW", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_CW = PG_W - DEFAULT_MARGINS.l - DEFAULT_MARGINS.r;", + "summary": null + }, + { + "name": "DEFAULT_FONT_SIZES", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_FONT_SIZES = { title: 16, info: 9, th: 8, td: 7.5, ft: 7 };", + "summary": null + }, + { + "name": "DEFAULT_MARGINS", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_MARGINS = { t: 45, r: 36, b: 35, l: 36 };", + "summary": null + }, + { + "name": "DEFAULT_MAX_BLOCKS", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const DEFAULT_MAX_BLOCKS = 100_000;", + "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": "Default maximum decompressed size (bytes) for inflate operations." + }, + { + "name": "DecryptionContext", + "kind": "type", + "module": "src/parser/pdf-decrypt.ts", + "signature": "export interface DecryptionContext", + "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": "Union of all supported document blocks." + }, + { + "name": "DocumentMetadata", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": null, + "summary": null + }, + { + "name": "DocumentParams", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface DocumentParams", + "summary": "Parameters for the free-form document PDF builder." + }, + { + "name": "EcPrivateKey", + "kind": "type", + "module": "src/crypto/ecdsa.ts", + "signature": "export interface EcPrivateKey", + "summary": "ECDSA P-256 private key." + }, + { + "name": "EcPublicKey", + "kind": "type", + "module": "src/crypto/ecdsa.ts", + "signature": "export interface EcPublicKey", + "summary": "ECDSA P-256 public key." + }, + { + "name": "EmbeddedFilesResult", + "kind": "type", + "module": "src/core/pdf-tags.ts", + "signature": "export interface EmbeddedFilesResult", + "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": "Encoding context encapsulating text encoding and font reference logic." + }, + { + "name": "EncryptionOptions", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface EncryptionOptions", + "summary": "Options for PDF encryption (password protection)." + }, + { + "name": "ExtractTextOptions", + "kind": "type", + "module": "src/parser/pdf-text-extract.ts", + "signature": "export interface ExtractTextOptions", + "summary": null + }, + { + "name": "ExtractedPageText", + "kind": "type", + "module": "src/parser/pdf-text-extract.ts", + "signature": "export interface ExtractedPageText", + "summary": null + }, + { + "name": "ExtractedTextRun", + "kind": "type", + "module": "src/parser/pdf-text-extract.ts", + "signature": "export interface ExtractedTextRun", + "summary": "One text-showing operation (`Tj`, one `TJ` string, `'` or `\"`)." + }, + { + "name": "FT_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const FT_H = 15;", + "summary": null + }, + { + "name": "FillFormOptions", + "kind": "type", + "module": "src/core/pdf-form-fill.ts", + "signature": "export interface FillFormOptions", + "summary": "Options for fillForm." + }, + { + "name": "FlattenFormOptions", + "kind": "type", + "module": "src/core/pdf-form-fill.ts", + "signature": "export interface FlattenFormOptions", + "summary": "Options for flattenForm." + }, + { + "name": "FontData", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface FontData", + "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": "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": "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": "Font metrics embedded in font data modules." + }, + { + "name": "FontRun", + "kind": "type", + "module": "src/shaping/multi-font.ts", + "signature": "export interface FontRun", + "summary": "A text run with its assigned font entry" + }, + { + "name": "FontValidationResult", + "kind": "type", + "module": "src/fonts/font-validator.ts", + "signature": "export interface FontValidationResult", + "summary": "Result of validateFontData." + }, + { + "name": "FormField", + "kind": "type", + "module": "src/core/pdf-form.ts", + "signature": "export interface FormField", + "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": "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": "A named field was not found in the document." + }, + { + "name": "FormFieldType", + "kind": "type", + "module": "src/core/pdf-form.ts", + "signature": "export type FormFieldType =", + "summary": "Supported AcroForm field types (ISO 32000-1 §12.7.3–12.7.4)." + }, + { + "name": "FormFillValue", + "kind": "type", + "module": "src/core/pdf-form-fill.ts", + "signature": "export type FormFillValue = string | boolean | readonly string[];", + "summary": null + }, + { + "name": "FormUnsupportedError", + "kind": "value", + "module": "src/core/pdf-form-fill.ts", + "signature": "export class FormUnsupportedError extends Error", + "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": "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": "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": "Free-text (typewriter) annotation (`/Subtype /FreeText`)." + }, + { + "name": "GlyfFont", + "kind": "type", + "module": "src/fonts/glyf-outline.ts", + "signature": "export interface GlyfFont", + "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": "How a gradient extends beyond its [0,1] range (COLR Extend / PDF Extend)." + }, + { + "name": "HEADER_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const HEADER_H = 15;", + "summary": null + }, + { + "name": "HeadingBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface HeadingBlock", + "summary": "Heading block — rendered at a predefined size based on level." + }, + { + "name": "INFO_LN", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const INFO_LN = 13;", + "summary": null + }, + { + "name": "ImageBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface ImageBlock", + "summary": "Image block — embeds a JPEG or PNG image." + }, + { + "name": "InspectedBlock", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface InspectedBlock", + "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": "One page's worth of InspectedBlocks." + }, + { + "name": "InternalLink", + "kind": "type", + "module": "src/core/pdf-annot.ts", + "signature": "export interface InternalLink", + "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": "Known non-Flate decode filter names (for membership checks)." + }, + { + "name": "LayoutDebugOptions", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface LayoutDebugOptions", + "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": "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": "Straight-line annotation (`/Subtype /Line`)." + }, + { + "name": "LinearGradientPaint", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface LinearGradientPaint", + "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": "External link annotation (URI action)." + }, + { + "name": "LinkBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface LinkBlock", + "summary": "Link block — clickable hyperlink text." + }, + { + "name": "ListBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface ListBlock", + "summary": "List block — bullet or numbered items." + }, + { + "name": "ListItem", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface ListItem", + "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": "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": "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": "Maximum depth of the xref `/Prev` chain." + }, + { + "name": "MarkupAnnotation", + "kind": "type", + "module": "src/core/pdf-annot-markup.ts", + "signature": "export type MarkupAnnotation =", + "summary": "Any builder-supported annotation." + }, + { + "name": "MergeOptions", + "kind": "type", + "module": "src/parser/pdf-pagetree.ts", + "signature": "export interface MergeOptions", + "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": "SingleResponse certStatus." + }, + { + "name": "OcspRequestOptions", + "kind": "type", + "module": "src/crypto/ocsp.ts", + "signature": "export interface OcspRequestOptions", + "summary": "Options for buildOcspRequest." + }, + { + "name": "OcspResponse", + "kind": "type", + "module": "src/crypto/ocsp.ts", + "signature": "export interface OcspResponse", + "summary": "Parsed OCSPResponse (RFC 6960 §4.2.1)." + }, + { + "name": "OpenPdfOptions", + "kind": "type", + "module": "src/parser/pdf-reader.ts", + "signature": "export interface OpenPdfOptions", + "summary": "Options for openPdf." + }, + { + "name": "OutlineItem", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface OutlineItem", + "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": "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": "Provider of glyph outlines (decoupled from the font parser for testing)." + }, + { + "name": "PAGE_SIZES", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const PAGE_SIZES =", + "summary": null + }, + { + "name": "PDF_A_CONFORMANCE_TARGETS", + "kind": "value", + "module": "src/core/pdf-tags.ts", + "signature": "export const PDF_A_CONFORMANCE_TARGETS = ['pdfa1b', 'pdfa2b', 'pdfa2u', 'pdfa3b'] as const;", + "summary": "Canonical list of PDF/A conformance targets accepted by the `tagged` layout option." + }, + { + "name": "PG_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const PG_H = 841.89; // A4 height (297mm)", + "summary": null + }, + { + "name": "PG_W", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const PG_W = 595.28; // A4 width (210mm)", + "summary": null + }, + { + "name": "PageBox", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export type PageBox = readonly [number, number, number, number];", + "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": "Page break block — forces a new page." + }, + { + "name": "PageLabelRange", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": null, + "summary": null + }, + { + "name": "PageLabelStyle", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": null, + "summary": null + }, + { + "name": "PageRange", + "kind": "type", + "module": "src/parser/pdf-pagetree.ts", + "signature": "export interface PageRange", + "summary": "A contiguous, inclusive page range (0-based)." + }, + { + "name": "PageTemplate", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface PageTemplate", + "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": "Paragraph block — text wrapping with configurable alignment." + }, + { + "name": "ParsedAnnotation", + "kind": "type", + "module": "src/parser/pdf-reader.ts", + "signature": "export interface ParsedAnnotation", + "summary": "A page annotation parsed by PdfReader.getAnnotations." + }, + { + "name": "ParsedArray", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": null, + "summary": null + }, + { + "name": "ParsedCms", + "kind": "type", + "module": "src/crypto/cms-utils.ts", + "signature": "export interface ParsedCms", + "summary": "Structural view over a DER-encoded CMS ContentInfo/SignedData." + }, + { + "name": "ParsedCrl", + "kind": "type", + "module": "src/crypto/crl.ts", + "signature": "export interface ParsedCrl", + "summary": "Parsed CertificateList (RFC 5280 §5.1)." + }, + { + "name": "ParsedDict", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": null, + "summary": null + }, + { + "name": "ParsedFieldType", + "kind": "type", + "module": "src/core/pdf-form-fill.ts", + "signature": "export type ParsedFieldType =", + "summary": null + }, + { + "name": "ParsedFormField", + "kind": "type", + "module": "src/core/pdf-form-fill.ts", + "signature": "export interface ParsedFormField", + "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": "Parsed image data ready for PDF embedding." + }, + { + "name": "PdfAConfig", + "kind": "type", + "module": "src/core/pdf-tags.ts", + "signature": "export interface PdfAConfig", + "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": "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": "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": "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": "Color input accepted by pdfnative." + }, + { + "name": "PdfColors", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface PdfColors", + "summary": "Color palette for the PDF." + }, + { + "name": "PdfDiagnostic", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface PdfDiagnostic", + "summary": "A single conformance diagnostic surfaced by the builders." + }, + { + "name": "PdfDiagnosticCode", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export type PdfDiagnosticCode =", + "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": "Sink for conformance diagnostics." + }, + { + "name": "PdfEncryptionInfo", + "kind": "type", + "module": "src/parser/pdf-reader.ts", + "signature": "export interface PdfEncryptionInfo", + "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": "Thrown when the document uses an encryption scheme pdfnative cannot read." + }, + { + "name": "PdfIndirectObject", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export interface PdfIndirectObject", + "summary": null + }, + { + "name": "PdfInfoItem", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface PdfInfoItem", + "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": "Layout options (all optional, A4 defaults applied)." + }, + { + "name": "PdfMetadataUpdate", + "kind": "type", + "module": "src/parser/pdf-modifier.ts", + "signature": "export interface PdfMetadataUpdate", + "summary": "Metadata fields applied by PdfModifier.updateMetadata." + }, + { + "name": "PdfModifier", + "kind": "type", + "module": "src/parser/pdf-modifier.ts", + "signature": "export interface PdfModifier", + "summary": null + }, + { + "name": "PdfName", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export interface PdfName", + "summary": null + }, + { + "name": "PdfParams", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface PdfParams", + "summary": "Parameters for PDF generation." + }, + { + "name": "PdfPasswordError", + "kind": "value", + "module": "src/parser/pdf-decrypt.ts", + "signature": "export class PdfPasswordError extends Error", + "summary": "Thrown when a password is missing or rejected." + }, + { + "name": "PdfReader", + "kind": "type", + "module": "src/parser/pdf-reader.ts", + "signature": "export interface PdfReader", + "summary": null + }, + { + "name": "PdfRef", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export interface PdfRef", + "summary": null + }, + { + "name": "PdfRgbString", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export type PdfRgbString = `${number} ${number} ${number}`;", + "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": "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": "A single row in the PDF table." + }, + { + "name": "PdfSignOptions", + "kind": "type", + "module": "src/core/pdf-signature.ts", + "signature": "export interface PdfSignOptions extends SigDictMetadata", + "summary": null + }, + { + "name": "PdfSignTimestampOptions", + "kind": "type", + "module": "src/core/pdf-sign-timestamp.ts", + "signature": "export interface PdfSignTimestampOptions extends PdfSignOptions", + "summary": "Options for signPdfBytesWithTimestamp." + }, + { + "name": "PdfSignatureInfo", + "kind": "type", + "module": "src/core/pdf-sig-utils.ts", + "signature": "export interface PdfSignatureInfo", + "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": "A merge source: raw PDF bytes, or bytes paired with the password needed to decrypt an encrypted source." + }, + { + "name": "PdfStream", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export interface PdfStream", + "summary": null + }, + { + "name": "PdfToken", + "kind": "type", + "module": "src/parser/pdf-tokenizer.ts", + "signature": "export interface PdfToken", + "summary": null + }, + { + "name": "PdfTokenizer", + "kind": "type", + "module": "src/parser/pdf-tokenizer.ts", + "signature": "export interface PdfTokenizer", + "summary": null + }, + { + "name": "PdfUAValidationResult", + "kind": "type", + "module": "src/parser/pdf-ua-validator.ts", + "signature": "export interface PdfUAValidationResult", + "summary": "Result of a PDF/UA structural validation." + }, + { + "name": "PdfValue", + "kind": "type", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export type PdfValue =", + "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": "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": "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": "QR Code error correction levels." + }, + { + "name": "ROW_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const ROW_H = 12;", + "summary": null + }, + { + "name": "RadialGradientPaint", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface RadialGradientPaint", + "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": "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": "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": "A pluggable revocation-data transport." + }, + { + "name": "RsaDigest", + "kind": "type", + "module": "src/crypto/rsa.ts", + "signature": "export type RsaDigest = 'sha256' | 'sha384' | 'sha512';", + "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": "RSA private key (CRT form for efficiency)." + }, + { + "name": "RsaPublicKey", + "kind": "type", + "module": "src/crypto/rsa.ts", + "signature": "export interface RsaPublicKey", + "summary": "RSA public key." + }, + { + "name": "ShapeAnnotation", + "kind": "type", + "module": "src/core/pdf-annot-markup.ts", + "signature": "export interface ShapeAnnotation extends AnnotationBase", + "summary": "Rectangle / ellipse drawing annotation." + }, + { + "name": "ShapedGlyph", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface ShapedGlyph", + "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": "Metadata-only subset of PdfSignOptions used by buildSigDict and addSignaturePlaceholder." + }, + { + "name": "SignatureAlgorithm", + "kind": "type", + "module": "src/crypto/cms.ts", + "signature": "export type SignatureAlgorithm = 'rsa-sha256' | 'ecdsa-sha256' | 'rsa-sha384' | 'rsa-sha512';", + "summary": null + }, + { + "name": "SolidPaint", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface SolidPaint", + "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": "Spacer block — vertical whitespace." + }, + { + "name": "SplitPdfStream", + "kind": "type", + "module": "src/parser/pdf-pagetree.ts", + "signature": "export interface SplitPdfStream", + "summary": "One emitted document from streamSplitPdf." + }, + { + "name": "StreamMergeOptions", + "kind": "type", + "module": "src/parser/pdf-pagetree.ts", + "signature": "export interface StreamMergeOptions extends MergeOptions", + "summary": "Options for the streaming page-tree variants." + }, + { + "name": "StreamOptions", + "kind": "type", + "module": "src/core/pdf-stream-writer.ts", + "signature": "export interface StreamOptions", + "summary": "Options for streaming PDF output." + }, + { + "name": "StreamToFileResult", + "kind": "type", + "module": "src/core/pdf-stream-writer.ts", + "signature": "export interface StreamToFileResult", + "summary": "Result of streamToFile." + }, + { + "name": "SvgBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface SvgBlock", + "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": "Options for SVG rendering to PDF operators." + }, + { + "name": "SvgSegment", + "kind": "type", + "module": "src/core/pdf-svg.ts", + "signature": "export interface SvgSegment", + "summary": "Parsed SVG path segment in absolute coordinates (M, L, C, Z only)." + }, + { + "name": "TH_H", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const TH_H = 15;", + "summary": null + }, + { + "name": "TITLE_LN", + "kind": "value", + "module": "src/core/pdf-layout.ts", + "signature": "export const TITLE_LN = 22;", + "summary": null + }, + { + "name": "TableBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface TableBlock", + "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": "Sticky-note text annotation (`/Subtype /Text`)." + }, + { + "name": "TextMarkupAnnotation", + "kind": "type", + "module": "src/core/pdf-annot-markup.ts", + "signature": "export interface TextMarkupAnnotation extends AnnotationBase", + "summary": "Text-markup annotation (highlight / underline / strike-out / squiggly)." + }, + { + "name": "TextRun", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface TextRun", + "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": "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": "Options for buildTimestampRequest." + }, + { + "name": "TimestampResponse", + "kind": "type", + "module": "src/crypto/rfc3161.ts", + "signature": "export interface TimestampResponse", + "summary": "Parsed TimeStampResp (RFC 3161 §2.4.2)." + }, + { + "name": "TocBlock", + "kind": "type", + "module": "src/types/pdf-document-types.ts", + "signature": "export interface TocBlock", + "summary": "Table of Contents block — auto-generated from heading blocks." + }, + { + "name": "TokenType", + "kind": "type", + "module": "src/parser/pdf-tokenizer.ts", + "signature": "export type TokenType =", + "summary": null + }, + { + "name": "TstInfo", + "kind": "type", + "module": "src/crypto/rfc3161.ts", + "signature": "export interface TstInfo", + "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": "USE-lite cluster categories." + }, + { + "name": "UseClassifiedCp", + "kind": "type", + "module": "src/shaping/use-lite.ts", + "signature": "export interface UseClassifiedCp", + "summary": "Classified code point with its USE-lite category." + }, + { + "name": "UseCluster", + "kind": "type", + "module": "src/shaping/use-lite.ts", + "signature": "export interface UseCluster", + "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": "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": "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": "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": "Image watermark configuration." + }, + { + "name": "WatermarkOptions", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface WatermarkOptions", + "summary": "Watermark options for PDF pages." + }, + { + "name": "WatermarkState", + "kind": "type", + "module": "src/core/pdf-watermark.ts", + "signature": "export interface WatermarkState", + "summary": "Resolved watermark state for use during PDF assembly." + }, + { + "name": "WatermarkText", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface WatermarkText", + "summary": "Text watermark configuration." + }, + { + "name": "WorkerGenerationOptions", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export interface WorkerGenerationOptions", + "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": "Message sent to the PDF Worker." + }, + { + "name": "WorkerOutputMessage", + "kind": "type", + "module": "src/types/pdf-types.ts", + "signature": "export type WorkerOutputMessage =", + "summary": "Messages received from the PDF Worker." + }, + { + "name": "X509Certificate", + "kind": "type", + "module": "src/crypto/x509.ts", + "signature": "export interface X509Certificate", + "summary": null + }, + { + "name": "X509Name", + "kind": "type", + "module": "src/crypto/x509.ts", + "signature": "export interface X509Name", + "summary": null + }, + { + "name": "XrefEntry", + "kind": "type", + "module": "src/parser/pdf-xref-parser.ts", + "signature": "export interface XrefEntry", + "summary": null + }, + { + "name": "XrefTable", + "kind": "type", + "module": "src/parser/pdf-xref-parser.ts", + "signature": "export interface XrefTable", + "summary": null + }, + { + "name": "addDocumentTimestamp", + "kind": "value", + "module": "src/core/pdf-doc-timestamp.ts", + "signature": "export async function addDocumentTimestamp( pdfBytes: Uint8Array, options: AddDocumentTimestampOptions = {}, ): Promise", + "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": "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": "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": "Convenience: collectValidationInfo + embedValidationInfo." + }, + { + "name": "applyDecodeFilter", + "kind": "value", + "module": "src/parser/pdf-decode-filters.ts", + "signature": "export function applyDecodeFilter(name: string, data: Uint8Array): Uint8Array", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Check if text contains Hebrew characters." + }, + { + "name": "containsKhmer", + "kind": "value", + "module": "src/shaping/script-registry.ts", + "signature": "export function containsKhmer(str: string): boolean", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Decode an ASCIIHexDecode stream." + }, + { + "name": "decodeEcPublicKey", + "kind": "value", + "module": "src/crypto/ecdsa.ts", + "signature": "export function decodeEcPublicKey(bytes: Uint8Array): EcPublicKey", + "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": "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": "Decode a RunLengthDecode stream." + }, + { + "name": "defaultFieldHeight", + "kind": "value", + "module": "src/core/pdf-form.ts", + "signature": "export function defaultFieldHeight(fieldType: FormFieldType): number", + "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": "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": "Parse a DER-encoded ASN.1 structure." + }, + { + "name": "derGeneralizedTime", + "kind": "value", + "module": "src/crypto/asn1.ts", + "signature": "export function derGeneralizedTime(date: Date): Uint8Array", + "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": "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": "Encode an OCTET STRING." + }, + { + "name": "derOid", + "kind": "value", + "module": "src/crypto/asn1.ts", + "signature": "export function derOid(oidBytes: Uint8Array): Uint8Array", + "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": "Encode a SEQUENCE (0x30) from child TLVs." + }, + { + "name": "derSetOf", + "kind": "value", + "module": "src/crypto/asn1.ts", + "signature": "export function derSetOf(...children: Uint8Array[]): Uint8Array", + "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": "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": "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": "Detect the image format from magic bytes." + }, + { + "name": "dictGet", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGet(dict: PdfDict, key: string): PdfValue | undefined", + "summary": null + }, + { + "name": "dictGetArray", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGetArray(dict: PdfDict, key: string): PdfArray | undefined", + "summary": null + }, + { + "name": "dictGetDict", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGetDict(dict: PdfDict, key: string): PdfDict | undefined", + "summary": null + }, + { + "name": "dictGetName", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGetName(dict: PdfDict, key: string): string | undefined", + "summary": null + }, + { + "name": "dictGetNum", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGetNum(dict: PdfDict, key: string): number | undefined", + "summary": null + }, + { + "name": "dictGetRef", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function dictGetRef(dict: PdfDict, key: string): PdfRef | undefined", + "summary": null + }, + { + "name": "downloadBlob", + "kind": "value", + "module": "src/core/pdf-stream.ts", + "signature": "export function downloadBlob(bytes: Uint8Array, filename: string): void", + "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": "Calculate EAN-13 check digit." + }, + { + "name": "ecPublicKeyFromPrivate", + "kind": "value", + "module": "src/crypto/ecdsa.ts", + "signature": "export function ecPublicKeyFromPrivate(privateKey: EcPrivateKey): EcPublicKey", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Generate a QR Code module matrix." + }, + { + "name": "getCryptoProvider", + "kind": "value", + "module": "src/crypto/crypto-provider.ts", + "signature": "export function getCryptoProvider(): CryptoProvider | null", + "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": "Get the current maximum decompressed output size." + }, + { + "name": "getRegisteredLangs", + "kind": "value", + "module": "src/fonts/font-loader.ts", + "signature": "export function getRegisteredLangs(): string[]", + "summary": "Get all registered language codes." + }, + { + "name": "getRevocationProvider", + "kind": "value", + "module": "src/crypto/revocation-provider.ts", + "signature": "export function getRevocationProvider(): RevocationProvider | null", + "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": "The currently-installed global TimestampProvider, or `null` if none." + }, + { + "name": "getTrailerRef", + "kind": "value", + "module": "src/parser/pdf-xref-parser.ts", + "signature": "export function getTrailerRef(trailer: PdfDict, key: string): PdfRef | undefined", + "summary": null + }, + { + "name": "getTrailerValue", + "kind": "value", + "module": "src/parser/pdf-xref-parser.ts", + "signature": "export function getTrailerValue(trailer: PdfDict, key: string): PdfValue | undefined", + "summary": null + }, + { + "name": "hasFontLoader", + "kind": "value", + "module": "src/fonts/font-loader.ts", + "signature": "export function hasFontLoader(lang: string): boolean", + "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": "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": "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": "HMAC-SHA256." + }, + { + "name": "inflateSync", + "kind": "value", + "module": "src/parser/pdf-inflate.ts", + "signature": "export function inflateSync(data: Uint8Array): Uint8Array", + "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": "Initialize all crypto module cross-dependencies." + }, + { + "name": "initNodeCompression", + "kind": "value", + "module": "src/core/pdf-compress.ts", + "signature": "export async function initNodeCompression(): Promise", + "summary": "Initialize Node.js compression for ESM contexts." + }, + { + "name": "initNodeDecompression_parser", + "kind": "value", + "module": "src/parser/pdf-inflate.ts", + "signature": null, + "summary": null + }, + { + "name": "inspectDocumentLayout", + "kind": "value", + "module": "src/core/pdf-layout-inspect.ts", + "signature": "export function inspectDocumentLayout( params: DocumentParams, layoutOptions?: Partial, ): LayoutInspection", + "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": "Check if a codepoint falls in any Armenian Unicode block." + }, + { + "name": "isArray", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function isArray(v: PdfValue | undefined): v is PdfArray", + "summary": null + }, + { + "name": "isBengaliCodepoint", + "kind": "value", + "module": "src/shaping/script-registry.ts", + "signature": "export function isBengaliCodepoint(cp: number): boolean", + "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": "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": "Check if a codepoint falls in any Devanagari Unicode block." + }, + { + "name": "isDict", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function isDict(v: PdfValue): v is PdfDict", + "summary": null + }, + { + "name": "isEthiopicCodepoint", + "kind": "value", + "module": "src/shaping/script-registry.ts", + "signature": "export function isEthiopicCodepoint(cp: number): boolean", + "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": "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": "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": "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": "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": "Check if a codepoint falls in any Myanmar Unicode block." + }, + { + "name": "isName", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function isName(v: PdfValue | undefined): v is PdfName", + "summary": null + }, + { + "name": "isRef", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function isRef(v: PdfValue | undefined): v is PdfRef", + "summary": null + }, + { + "name": "isSelfSigned", + "kind": "value", + "module": "src/crypto/x509.ts", + "signature": "export function isSelfSigned(cert: X509Certificate): boolean", + "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": "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": "Check if a codepoint falls in the Sinhala Unicode block." + }, + { + "name": "isStream", + "kind": "value", + "module": "src/parser/pdf-object-parser.ts", + "signature": "export function isStream(v: PdfValue): v is PdfStream", + "summary": null + }, + { + "name": "isTamilCodepoint", + "kind": "value", + "module": "src/shaping/script-registry.ts", + "signature": "export function isTamilCodepoint(cp: number): boolean", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Open a PDF file for reading." + }, + { + "name": "parseCertificate", + "kind": "value", + "module": "src/crypto/x509.ts", + "signature": "export function parseCertificate(der: Uint8Array): X509Certificate", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Render SVG content as PDF path operators." + }, + { + "name": "resetFontRegistry", + "kind": "value", + "module": "src/fonts/font-loader.ts", + "signature": "export function resetFontRegistry(): void", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Inject a custom inflate implementation." + }, + { + "name": "setMaxInflateOutputSize", + "kind": "value", + "module": "src/parser/pdf-inflate.ts", + "signature": "export function setMaxInflateOutputSize(size: number): void", + "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": "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": "Install (or clear) a global TimestampProvider." + }, + { + "name": "sha1", + "kind": "value", + "module": "src/crypto/sha.ts", + "signature": "export function sha1(input: Uint8Array): Uint8Array", + "summary": "SHA-1 hash (FIPS 180-4)." + }, + { + "name": "sha384", + "kind": "value", + "module": "src/crypto/sha.ts", + "signature": "export function sha384(input: Uint8Array): Uint8Array", + "summary": "SHA-384 hash (FIPS 180-4)." + }, + { + "name": "sha512", + "kind": "value", + "module": "src/crypto/sha.ts", + "signature": "export function sha512(input: Uint8Array): Uint8Array", + "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Strip invisible Unicode bidirectional formatting characters." + }, + { + "name": "toBytes", + "kind": "value", + "module": "src/core/pdf-stream.ts", + "signature": "export function toBytes(str: string): Uint8Array", + "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": "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": "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": "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": "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": "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": "Structurally validate a FontData module." + }, + { + "name": "validatePdfUA", + "kind": "value", + "module": "src/parser/pdf-ua-validator.ts", + "signature": "export function validatePdfUA(bytes: Uint8Array): PdfUAValidationResult", + "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": "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": "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": "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": "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": "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": "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": "Uppercase-hex SHA-1 of the full /Contents value — the /VRI key." + }, + { + "name": "wrapText", + "kind": "value", + "module": "src/core/pdf-document.ts", + "signature": null, + "summary": null + } + ] +} 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/assets/ecosystem.json b/docs/assets/ecosystem.json index 360989b..fcbed70 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": { @@ -97,16 +105,17 @@ }, "derived": { "$comment": "Asserted against the filesystem by verify-docs. Never hand-edit to make a check pass — fix the docs or the tree instead.", - "testFiles": 122, + "testFiles": 123, "sampleGenerators": 48, "samplePdfs": 242, - "guides": 28, - "playgrounds": 9, - "learnSteps": 8 + "guides": 30, + "playgrounds": 10, + "learnSteps": 8, + "recipes": 14 }, "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, @@ -148,9 +157,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", @@ -182,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": "665+ tests across 122 files", + "canonical": "2686+ tests across 123 files", "requireIn": [ "docs/index.html" ] diff --git a/docs/assets/og-image.png b/docs/assets/og-image.png index 41b6653..1c2c650 100644 Binary files a/docs/assets/og-image.png and b/docs/assets/og-image.png differ 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/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/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/data/surfaces.json b/docs/data/surfaces.json new file mode 100644 index 0000000..7f01372 --- /dev/null +++ b/docs/data/surfaces.json @@ -0,0 +1,102 @@ +{ + "$comment": "Machine-readable form of the capability × surface table in docs/guides/choose.md. Hand-maintained, verified against the sources on the date below: library `call` names against src/index.ts exports, CLI `command` names against packages.pdfnative-cli.commandGroups and MCP `tool` names against packages.pdfnative-mcp.tools in docs/assets/ecosystem.json (the single source of truth for names and counts — regenerate nothing from this file), React entries against docs/guides/react.md. Each `since` is a version of that surface's OWN package (pdfnative, pdfnative-cli, pdfnative-mcp or pdfnative-react); it is omitted where the capability predates 1.0.0 or the introducing release is not documented. `supported: false` cells carry a note naming the honest alternative where one exists.", + "verifiedOn": "2026-08-25", + "capabilities": [ + { + "id": "generate", + "label": "Generate documents", + "library": { "supported": true, "call": "buildDocumentPDFBytes / buildPDFBytes" }, + "cli": { "supported": true, "command": "render" }, + "mcp": { "supported": true, "tool": "generate_basic_pdf", "notes": "Plus the dedicated document tools (add_table, add_barcode, add_international_text, add_form, embed_image, add_chart, prepare_signature_placeholder, add_attachment)." }, + "react": { "supported": true, "call": "renderToBytes / renderToBlob / renderToStream / renderToFile / renderToResponse over a tree" } + }, + { + "id": "tables", + "label": "Smart tables", + "library": { "supported": true, "call": "table block in buildDocumentPDFBytes (or the table-centric buildPDFBytes)" }, + "cli": { "supported": true, "command": "render", "notes": "table block in the document JSON, or --variant table; smart-table flags since v1.3.0." }, + "mcp": { "supported": true, "tool": "add_table" }, + "react": { "supported": true, "call": "
/ / " } + }, + { + "id": "charts", + "label": "Native vector charts", + "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": "", "since": "1.1.0" } + }, + { + "id": "sign", + "label": "Digital signatures (PAdES CMS)", + "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." } + }, + { + "id": "ltv", + "label": "LTV ladder (PAdES B-T → B-LTA)", + "library": { "supported": true, "call": "signPdfBytesWithTimestamp + addValidationInfo + addDocumentTimestamp", "since": "1.7.0", "notes": "Network transport is injected by the caller via setTimestampProvider / setRevocationProvider — the engine opens no socket." }, + "cli": { "supported": false, "notes": "Verification only: verify --revocation validates RFC 3161 timestamps and OCSP/CRL material; sign-side LTV is documented as out of scope." }, + "mcp": { "supported": true, "tool": "add_ltv + timestamp_pdf", "since": "1.6.0", "notes": "Transport is operator-configured via PDFNATIVE_MCP_TSA_URL / PDFNATIVE_MCP_REVOCATION and a host allow-list — never from tool arguments. verify_pdf with ltv: true reports the achieved level." }, + "react": { "supported": false } + }, + { + "id": "encrypt-decrypt", + "label": "Encrypt / decrypt", + "library": { "supported": true, "call": "encryption layout option at build time; page-tree encrypt option and openPdf with a password for existing PDFs" }, + "cli": { "supported": true, "command": "encrypt / decrypt", "since": "1.3.0" }, + "mcp": { "supported": true, "tool": "encrypt_pdf / decrypt_pdf", "since": "1.5.0", "notes": "Build-time encrypt on the document tools (since 1.6.0) keeps the AcroForm; encrypt_pdf rebuilds the page tree and drops signatures and /AcroForm." }, + "react": { "supported": true, "call": "layout render option (build-time encryption only)", "notes": "No decryption of existing PDFs." } + }, + { + "id": "fill-forms", + "label": "Fill / flatten forms", + "library": { "supported": true, "call": "readFormFields + fillForm + flattenForm", "since": "1.6.0" }, + "cli": { "supported": true, "command": "fill", "since": "1.3.0" }, + "mcp": { "supported": true, "tool": "read_form_fields + fill_form", "since": "1.5.0" }, + "react": { "supported": false, "notes": " creates form widgets in new documents; filling existing AcroForms is a library / CLI / MCP capability." } + }, + { + "id": "extract-text", + "label": "Extract text", + "library": { "supported": true, "call": "extractText", "since": "1.6.0" }, + "cli": { "supported": true, "command": "extract-text", "since": "1.3.0" }, + "mcp": { "supported": true, "tool": "extract_text" }, + "react": { "supported": false } + }, + { + "id": "merge-split", + "label": "Merge / split / extract pages", + "library": { "supported": true, "call": "mergePdfs + splitPdf + extractPages", "since": "1.4.0" }, + "cli": { "supported": true, "command": "merge / split / extract", "since": "1.2.0" }, + "mcp": { "supported": true, "tool": "merge_pdfs / split_pdf / extract_pages", "since": "1.3.0" }, + "react": { "supported": false } + }, + { + "id": "annotate", + "label": "Markup annotations", + "library": { "supported": true, "call": "buildAnnotation + PdfModifier.addAnnotation", "since": "1.5.0" }, + "cli": { "supported": true, "command": "annotate", "since": "1.2.0" }, + "mcp": { "supported": true, "tool": "annotate_pdf", "since": "1.4.0" }, + "react": { "supported": false } + }, + { + "id": "inspect-layout", + "label": "Inspect layout (pagination dry run)", + "library": { "supported": true, "call": "inspectDocumentLayout", "since": "1.5.0" }, + "cli": { "supported": true, "command": "render", "since": "1.2.0", "notes": "render --inspect-layout emits the LayoutInspection report instead of a PDF." }, + "mcp": { "supported": true, "tool": "inspect_layout", "since": "1.6.0" }, + "react": { "supported": true, "call": "inspectDocument" } + }, + { + "id": "validate-pdfua", + "label": "Validate PDF/UA", + "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 ee7b799..225cb55 100644 --- a/docs/guides/accessibility.html +++ b/docs/guides/accessibility.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,174 @@

Home  ›  Guides  ›  Accessibility

-
-

Loading…

- +
+ +

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.

+

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/accessibility.md b/docs/guides/accessibility.md index 6de79fd..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 @@ -110,6 +112,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/agentic-workflows.html b/docs/guides/agentic-workflows.html index 0417724..bc7c23f 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/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/ai-governance.html b/docs/guides/ai-governance.html index b3ddb0e..cdebf43 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/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/annotations.html b/docs/guides/annotations.html index 7418378..34f07fa 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..076aeb1 100644 --- a/docs/guides/architecture.html +++ b/docs/guides/architecture.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,176 @@

Home  ›  Guides  ›  Architecture

-
-

Loading…

- +
+ +

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#

+

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/architecture.md b/docs/guides/architecture.md index 5452f73..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 @@ -111,7 +113,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 +144,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/charts.html b/docs/guides/charts.html index edd6dd6..af093be 100644 --- a/docs/guides/charts.html +++ b/docs/guides/charts.html @@ -4,17 +4,18 @@ Charts (native vector) — pdfnative - + + - + @@ -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/choose.html b/docs/guides/choose.html new file mode 100644 index 0000000..823eb95 --- /dev/null +++ b/docs/guides/choose.html @@ -0,0 +1,267 @@ + + + + + + Choosing your surface — pdfnative + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

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> (v1.1.0)
Digital signatures (PAdES CMS)addSignaturePlaceholder (v1.2.0) + 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/UAvalidatePdfUA (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#

+
    +
  • 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..66f5c4c --- /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)_ | `` _(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)_ | — | +| 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` _(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 + +- **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/cli.html b/docs/guides/cli.html index 5898770..f21a552 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/cli.md b/docs/guides/cli.md index e9708b2..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.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.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` | diff --git a/docs/guides/colour-emoji-cli.html b/docs/guides/colour-emoji-cli.html index cdc943c..01d1adc 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 69be57e..7c1f440 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..6011669 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..f7d7131 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..4776d1a 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..3e39840 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/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/guide.css b/docs/guides/guide.css index f9f18a0..4923df0 100644 --- a/docs/guides/guide.css +++ b/docs/guides/guide.css @@ -189,3 +189,89 @@ 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; +} + +.pre-wrap { position: relative; } + +.pre-wrap .copy-btn { + user-select: none; + 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; +} + +.pre-wrap:hover .copy-btn, +.pre-wrap .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); } + +/* 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) { + .pre-wrap .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 d244cc8..6e9e130 100644 --- a/docs/guides/guide.js +++ b/docs/guides/guide.js @@ -44,8 +44,100 @@ var container = document.getElementById('guide-content'); if (!container) return; - var src = container.getAttribute('data-md'); - if (!src) return; + // The Markdown source name is DERIVED from the page's own URL (every guide + // pairs name.html with name.md) — never from DOM text. CodeQL's + // js/xss-through-dom tracks getAttribute() values into href/fetch sinks + // regardless of regex guards, so the attribute is demoted to an opt-in + // marker: it must agree with the derived name, but the value that reaches + // fetch(), the source-bar href and the GitHub fallback URLs comes from + // location, filtered to a plain same-directory Markdown filename. + var page = (location.pathname.split('/').pop() || '').replace(/\.html$/, ''); + var src = page + '.md'; + var declared = container.getAttribute('data-md'); + if (!declared || declared !== src || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.md$/.test(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) { + // The button lives in a positioned wrapper OUTSIDE the scrollable + //
: as a child it would scroll away with wide code and its label
+      // would pollute manual text selection.
+      if (pre.parentNode.classList && pre.parentNode.classList.contains('pre-wrap')) return;
+      var wrap = document.createElement('div');
+      wrap.className = 'pre-wrap';
+      pre.parentNode.insertBefore(wrap, pre);
+      wrap.appendChild(pre);
+      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'; });
+      });
+      wrap.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. 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 highlightWhenReady() {
+      if (window.Prism && typeof window.Prism.highlightAllUnder === 'function') {
+        window.Prism.highlightAllUnder(container);
+        return;
+      }
+      if (tries-- > 0) setTimeout(highlightWhenReady, 100);
+    })();
+    return;
+  }
 
   function showError(msg) {
     container.innerHTML = '
' + @@ -123,10 +215,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 506ad17..f9dcbf1 100644 --- a/docs/guides/index.html +++ b/docs/guides/index.html @@ -8,6 +8,7 @@ + @@ -33,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", @@ -42,7 +43,7 @@ }, "mainEntity": { "@type": "ItemList", - "numberOfItems": 28, + "numberOfItems": 30, "itemListOrder": "https://schema.org/ItemListUnordered", "itemListElement": [ { @@ -84,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" } @@ -285,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. @@ -315,7 +332,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 +340,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 +352,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 → @@ -359,7 +376,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 → @@ -377,13 +394,17 @@

    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.
  • 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 +451,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/ltv.html b/docs/guides/ltv.html index 2824f78..d307f10 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/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.html b/docs/guides/mcp.html index 394ec72..a738f99 100644 --- a/docs/guides/mcp.html +++ b/docs/guides/mcp.html @@ -4,29 +4,33 @@ pdfnative-mcp — MCP Integration Guide - + + - + - + + + + @@ -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/mcp.md b/docs/guides/mcp.md index e068c39..a1d2fb8 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.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. @@ -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.html b/docs/guides/onboarding.html index d09060d..64427cd 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/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/guides/outlines.html b/docs/guides/outlines.html index 911858e..c6d32ac 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..180317b 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/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..f79fefc 100644 --- a/docs/guides/pdfa.html +++ b/docs/guides/pdfa.html @@ -4,28 +4,32 @@ PDF/A conformance — pdfnative - + + - + - + + + + @@ -57,9 +61,384 @@

    Home  ›  Guides  ›  PDF/A

    -
    -

    Loading…

    - +
    + +

    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 +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/pdfa.md b/docs/guides/pdfa.md index 87e8a40..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 @@ -101,6 +103,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/print.html b/docs/guides/print.html index d326a3d..16ae92e 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..9b562c1 100644 --- a/docs/guides/quickstart.html +++ b/docs/guides/quickstart.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,191 @@

    Home  ›  Guides  ›  Quick Start

    -
    -

    Loading…

    - +
    + +

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

    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/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/react.html b/docs/guides/react.html index 6e0f116..ef5a98b 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/react.md b/docs/guides/react.md index 51708fb..339847c 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 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. @@ -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..63dbaa4 --- /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#

    +

    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);
    +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..27b1712 --- /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 + +`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'; + +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/guides/signatures.html b/docs/guides/signatures.html index ac3818e..f16892c 100644 --- a/docs/guides/signatures.html +++ b/docs/guides/signatures.html @@ -1,85 +1,491 @@ - - - - - - Digital signatures — pdfnative - - - + + + + + + Digital signatures — pdfnative + + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - -
    -

    Home  ›  Guides  ›  Digital signatures

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

    Home  ›  Guides  ›  Digital signatures

    +
    + +

    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 (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 {
    +    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/signatures.md b/docs/guides/signatures.md index 148dd03..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 @@ -108,7 +110,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 +362,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/guides/streaming.html b/docs/guides/streaming.html index 9f6eff7..6399b96 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..23a2c06 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/tables.md b/docs/guides/tables.md index 3115ef3..b907a14 100644 --- a/docs/guides/tables.md +++ b/docs/guides/tables.md @@ -114,7 +114,7 @@ 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`](#columndef-fields) overrides the table default. +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`](#tableblock-fields-all-optional) overrides the table default. ```ts { type: 'table', headers: [/* … */], rows: [/* … */], minRowHeight: 30, cellVAlign: 'middle' } diff --git a/docs/guides/text-extraction.html b/docs/guides/text-extraction.html index d621279..db63c33 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/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/guides/troubleshooting.html b/docs/guides/troubleshooting.html index 7787f7b..b419eb2 100644 --- a/docs/guides/troubleshooting.html +++ b/docs/guides/troubleshooting.html @@ -9,6 +9,7 @@ + @@ -26,6 +27,9 @@ + + + @@ -57,9 +61,117 @@

    Home  ›  Guides  ›  Troubleshooting

    -
    -

    Loading…

    - +
    + +

    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.

    +

    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/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/guides/viewer-preferences.html b/docs/guides/viewer-preferences.html index c02203a..7ea0aa6 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 f9255bc..4ca85d2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ pdfnative — Zero-dependency PDF generation for TypeScript - + @@ -30,6 +30,7 @@ + @@ -72,7 +73,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", @@ -103,9 +104,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, @@ -211,7 +212,7 @@

    Pure Native PDF Generation

    -
    2665+
    +
    2686+
    Tests
    @@ -252,7 +253,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,19 +271,19 @@

    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 →

    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.

    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.

    @@ -425,21 +426,39 @@

    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.

    +
    + + +
    View source ↗

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

    Need more? Browse all 48 generators (242 sample PDFs) @@ -456,7 +475,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';
    @@ -479,7 +498,7 @@ 

    CLI v1.3.0

    CLI guide →
    -

    MCP v1.5.0

    +

    MCP v1.6.0

    For Claude Desktop, Cursor, Continue, Zed, …

    {
       "mcpServers": {
    @@ -514,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.

    +
    + + + + + + +
    + +
    - + @@ -557,7 +588,7 @@

    How It Compares

    - + @@ -565,7 +596,7 @@

    How It Compares

    - + @@ -573,7 +604,7 @@

    How It Compares

    - + @@ -581,7 +612,7 @@

    How It Compares

    - + @@ -589,7 +620,7 @@

    How It Compares

    - + @@ -621,7 +652,7 @@

    How It Compares

    - + @@ -700,6 +731,13 @@

    Performance

    Faster hardware (Apple M-series, desktop i7/i9) typically runs 2–4× quicker. Results may vary. Run npx vitest bench to measure on your hardware.

    +
    + + + +
    @@ -739,7 +777,7 @@

    Architecture

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

    @@ -763,25 +801,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

    @@ -549,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
    - + - - - - - - + + + + + + + + + - + @@ -790,11 +831,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, 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).
    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).
    @@ -811,6 +853,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.

    @@ -967,6 +1011,8 @@

    Guides

  • Annotations
  • Outlines & page labels
  • Signatures
  • +
  • Long-term validation
  • +
  • Print production
  • Colour emoji
  • FAQ
  • Troubleshooting
  • @@ -980,6 +1026,7 @@

    Playgrounds

  • Extreme scripts
  • All 22 scripts
  • PDF Toolkit
  • +
  • Inspect & verify
  • Charts
  • Authoring+
  • Scale
  • 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  ›  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 cb068ef..4b1f633 100644 --- a/docs/playgrounds/mcp.html +++ b/docs/playgrounds/mcp.html @@ -4,8 +4,8 @@ MCP Tool Explorer — pdfnative - - + + @@ -14,24 +14,24 @@ - - + + - - + +