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) — 21 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.

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, <Document> tree
Smart tables table block render (table block, or --variant table) add_table <Table>
Native vector charts chart block (v1.6.0) render (chart block) (v1.3.0) add_chart (v1.5.0) <Chart> (v1.1.0; charts v2 — nine kinds, secondary axis, log/time scales, data labels — since v1.2.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) sign --timestamp, ltv, doc-timestamp (v1.4.0) 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)
Update metadata (signature-safe) PdfModifier.updateMetadata (v1.7.0) metadata (v1.4.0) update_metadata (v1.6.0) —
Compare two PDFs (text + structure) — (compose openPdf / extractText / the readers) compare (v1.4.0) — —
Runtime font registration registerFont / registerFonts (v1.5.0, any loader — incl. fonts you compile with compileFontData) render --font (bundled fonts only) (v1.1.0) — (fixed lang enum; fonts resolved from the local pdfnative install) registerFont / registerFonts (re-exported)
Typography (paragraph breaking, justification, hyphenation, kerning, OpenType features) layout.typography (TypographyOptions) + setHyphenationProvider / bindUnits / bindShortWords / applyPunctuationSpacing (v1.8.0; every option opt-in and byte-identical when absent) render --split-paragraphs / --keep-headings-with-next / --kerning / --font-features, or the full layout.typography object in the document JSON / a --layout file (v1.5.0) typography object on the nine document tools and inspect_layout; paragraph align: 'justify' / keepWithNext / splittable, heading keepWithNext (v1.7.0) <Document typography> · <Paragraph align="justify" keepWithNext splittable> · <Heading keepWithNext> (v1.3.0)
CMYK content colours [c, m, y, k] tuples (percent) or 'c m y k' strings on any colour field; parseColor / resolveColor / fillOp / strokeOp (v1.8.0) render ("c m y k" or [c, m, y, k] colour fields in the document JSON) (v1.5.0) '0 0.6 1 0' or [0, 60, 100, 0] on every colour input (v1.7.0) Color accepts a CMYK tuple or string on every colour prop; L_CMYK_INTENT_MISMATCH (v1.3.0)
CMYK / Gray OutputIntent with RGB content kept conforming layout.outputIntent with a CMYK or Gray ICC profile (v1.8.0; RGB content routed through a calibrated /DefaultRGB; no press profile shipped) render --output-intent-icc <file.icc> --output-intent-id <s> (v1.5.0) outputIntent accepts CMYK and Gray profiles on the document tools (v1.7.0) <Document outputIntent> (also a DocSpec field) (v1.3.0)
PDF/X-4 conformance claim layout: { pdfx: 'pdfx4', outputIntent, metadata.trapped } (v1.8.0; exclusive with tagged and encryption) render --pdfx pdfx4 --output-intent-icc <file.icc> --trapped true|false|unknown (coherence errors → E_INPUT, PDFX_* diagnostics fail under --strict) (v1.5.0) pdfx: 'pdfx4' on six document tools (needs a prtr outputIntent and embedFonts: true; exclusive with pdfA / encrypt) (v1.7.0) <Document pdfx="pdfx4" outputIntent metadata={{ trapped }}> with eight L_PDFX_* lint rules (v1.3.0)
Validate PDF/X-4 validatePdfX (v1.8.0; structural ISO 15930-7 prerequisites, not a certified preflight) inspect --pdfx / --check pdfx (v1.5.0) validate_pdf with standard: 'pdf-x-4' (result carries caveats[]); inspect_pdf reports pdfX (v1.7.0) — (import validatePdfX from the pdfnative peer and run it on the rendered bytes)
Printer's marks: registration colour and colour control bars layout.print.marks (true, or { colourBars: true | { tints, size } }) (v1.8.0; bars off by default and byte-neutral when off) render (print.marks incl. colourBars in the document JSON or a --layout file) (v1.5.0) print.marks: true (v1.6.0) or { colourBars: true | { tints, size } } (v1.7.0) <Document print={{ marks: { colourBars: true } }}> (ColourBarOptions, L_PRINT_COLOUR_BARS) (v1.3.0)
Reproducible bytes layout.creationDate, or setDefaultCreationDate(date) once per process; getDefaultCreationDate (v1.8.0; dates are written in UTC, so no TZ pin is needed) global --creation-date <iso8601> (SOURCE_DATE_EPOCH fallback), echoed in the --json envelope (v1.5.0) creationDate on all nine document tools (v1.6.0); server-wide PDFNATIVE_MCP_CREATION_DATE → SOURCE_DATE_EPOCH → clock (v1.7.0) <Document creationDate> (also a DocSpec field); setDefaultCreationDate / getDefaultCreationDate re-exported; no environment variable is read (v1.3.0)

Honest notes#

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#