Home  ›  Guides  ›  Quick Start

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:

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 21 commands — including render, fill, sign, verify, ltv, encrypt, decrypt, merge, split, extract, extract-text and compare:

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