FAQ
Frequently asked questions about pdfnative — getting started, custom fonts, PDF/A variants, encryption, BiDi text, browser / Deno / Bun support, signatures, workers, and how it compares with other libraries. 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?#
| 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.
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, Thai, Lao, Tai Tham, and Cham. Since v1.8.0 the five Indic scripts share one OpenType engine that applies the font's own features in specification order (reph, half forms, subjoined consonants, pre-base vowel signs), and Latin text with free-standing combining marks — Yoruba ẹ́ ọ̀, Igbo ị́, NFD accents — is composed through the font's ccmp and anchors when set in a registered font such as the bundled latin module.
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?#
31 Noto font-data modules ship with the package: the 27 scripts — Amharic/Ethiopic, Arabic, Armenian, Bengali, Cham, Cyrillic, Devanagari, Georgian, Greek, Hebrew, Japanese, Khmer, Korean, Lao, Myanmar, New Tai Lue, Polish, Simplified Chinese, Sinhala, Tai Le, Tai Tham, Tamil, Telugu, Thai, Tibetan, Turkish, Vietnamese — plus Latin (Noto Sans), math (Noto Sans Math), and monochrome + COLRv1 colour emoji. Latin-script languages with extended letters or tone marks — Hausa, Yoruba, Igbo, Swahili — need only the latin module.
Standards and compliance#
What PDF versions can pdfnative produce?#
| 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) |
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 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.
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?#
- 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.
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).
- 27 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#
| 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.