Frequently asked questions about pdfnative. Can't find your answer? Open a discussion or read the Troubleshooting guide.
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.
| Builder | Best for | Key shape |
|---|---|---|
buildPDFBytes |
Tabular reports, bank statements, invoices with a single table | { title, headers, rows, infoItems, balanceText, ... } |
buildDocumentPDFBytes |
Mixed-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.
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.
Yes. The library is standard ESM with no Node.js-specific imports in the core. Both Deno and Bun import it directly.
Yes. Build a font data module from any TTF file:
npx pdfnative-build-font path/to/MyFont.ttf my-font-data.jsThen 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.
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.
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.
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.
| Mode | PDF version | Standard |
|---|---|---|
| Default | 1.4 | ISO 32000-1 |
tagged: true |
1.7 | PDF/A-2b (default tagged mode, ISO 19005-2) |
tagged: 'pdfa1b' |
1.4 | PDF/A-1b (ISO 19005-1) |
tagged: 'pdfa2u' |
1.7 | PDF/A-2u (ISO 19005-2 with unicode) |
tagged: 'pdfa3b' |
1.7 | PDF/A-3b (ISO 19005-3, supports embedded files) |
- 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.
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.
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.
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.
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), both with SHA-256 digests. 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).
- Zero runtime dependencies —
npm install pdfnativebrings 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.
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 chainbuildPDFBytes 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.
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);
}- Enable compression:
compress: true(FlateDecode, 50–90 % size reduction). - Initialize native zlib in Node.js:
await initNodeCompression()once at startup. - Use JPEG for photos — JPEG (DCTDecode) is already compressed; PNG is FlateDecode-compressed.
- Font subsetting is automatic — but each script you embed adds one subset.
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.
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.
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.
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.
| Error | Likely cause | Fix |
|---|---|---|
| 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 throwing |
Call registerFonts({ xx: … }) (or registerFont) first, then check the loadFontData result for null before building fontEntries |
PDF/A and encryption are mutually exclusive |
tagged: 'pdfa…' combined with encryption: … |
Pick one |
Invalid color format: … / Invalid color tuple … |
Unrecognized color value passed to a layout option | Use #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 glyphs | Font for that script is not loaded | See Troubleshooting → Missing glyphs |
| Parser throws on external PDF | Encrypted PDF opened without a password, or non-standard structure | Pass 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.