Node.js / TypeScript

The rustpdf package wraps the rust-pdf C core with idiomatic, chainable classes over Koffi (pure FFI, no node-gyp, no native build). It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. Ships with TypeScript types.

Two classes do almost everything. Document authors a new PDF; EditableDoc loads and manipulates an existing one. Each holds a native handle, so call close() when done to free it promptly.

Installation

Install from npm. The native library (libpdf_ffi) ships as per-platform optional dependencies (@rustpdf/darwin-arm64, @rustpdf/linux-x64-gnu, @rustpdf/linux-arm64-gnu, @rustpdf/win32-x64-msvc): npm downloads only the one matching your OS/architecture, so there's nothing to compile and no node-gyp.

shell
npm install rustpdf

Requires Node.js 18+. Verify it loaded:

javascript
const rustpdf = require("rustpdf");
console.log(rustpdf.version());   // native library version
Bundling for a serverless target (AWS Lambda, etc.)? Make sure the matching @rustpdf/<platform> package is included for the deploy architecture: e.g. install on a Linux x64 host, or add it explicitly as an optionalDependency.

Quick start

A one-page document with a filled rectangle, saved to disk:

javascript
const { Document } = require("rustpdf");

const doc = new Document();              // A4 by default
doc.addPage();
doc.setFillRgb(0.86, 0.20, 0.18);
doc.rect(72, 640, 200, 120);             // x, y, width, height (points)
doc.fill();
doc.save("out.pdf");
doc.close();

Most methods return the document, so calls chain:

javascript
const doc = new Document();
const font = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage()
   .setFillRgb(0.1, 0.1, 0.12)
   .rect(0, 800, 595, 42).fill()
   .showText(font, 24, 72, 740, "Olá, açúcar — café");
const data = doc.toBytes();              // a Node Buffer instead of a file
doc.close();

TypeScript

The package ships type declarations (index.d.ts), so no @types are needed. Both require and ES-module import work.

typescript
import { Document, PdfaLevel, Align } from "rustpdf";

const doc = new Document();
doc.pdfa(PdfaLevel.A2a).setInfo({ title: "Report" });
const f = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage().showText(f, 20, 72, 760, "Title", 1);   // headingLevel 1 = H1
const bytes: Buffer = doc.toBytes();
doc.close();

Licensing & activation

Basic generation (everything above) is always free. The corporate features (PDF/A, digital signatures/PAdES, encryption, accessibility) require an active license token. Without one, those calls throw PdfError and produce no output.

Activation needs no rebuild. Easiest is an environment variable, auto-activated the first time a corporate feature is used:

shell
export RUSTPDF_LICENSE="010f0000…"           # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txt

Or activate explicitly in code:

javascript
const rustpdf = require("rustpdf");
rustpdf.activateLicense(token);   // throws PdfError if forged / expired / malformed
Verification is fully offline: signature + expiry checked against a public key embedded in the library. No network callback, no telemetry.

Coordinate system

Threading & concurrency

Every native call is synchronous (it blocks the event loop for its duration) and the core is Send but not Sync: a single handle must never be touched by two threads at once.

javascript
// worker.js
const { parentPort, workerData } = require("worker_threads");
const { Document } = require("rustpdf");

const doc = new Document();              // one document per worker
doc.addPage().setFillRgb(0.1, 0.1, 0.12).rect(72, 700, 200, 80).fill();
parentPort.postMessage(doc.toBytes());
doc.close();

Authoring: create & save

new Document() free

Creates an empty document (A4 default page size). Call close() to free the native handle.

MethodDescription
addPage(size?)Append a page. size is an optional { width, height } object in points.
setDefaultSize(w, h)Default size for subsequently added pages.
setVersion(v)Set the PDF header version (e.g. 14 → PDF 1.4, 17 → 1.7).
pageCountProperty: number of pages so far.
toBytes()Render the document to a Buffer.
save(path)Render and write to a file.
close()Free the native handle.

Pages & vector graphics

Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.

MethodDescription
setFillRgb(r, g, b)Fill color.
setStrokeRgb(r, g, b)Stroke color.
setLineWidth(w)Stroke width in points.
rect(x, y, w, h)Add a rectangle subpath.
fill()Fill the current path with the fill color.
stroke()Stroke the current path with the stroke color.
javascript
const doc = new Document();
doc.addPage();
doc.setStrokeRgb(0.10, 0.45, 0.90).setLineWidth(3);
doc.rect(72, 600, 300, 160).stroke();
doc.setFillRgb(0.95, 0.77, 0.06);
doc.rect(120, 640, 120, 80).fill();
doc.save("shapes.pdf");
doc.close();

Fonts & text

Fonts are embedded and subsetted, with HarfBuzz-quality shaping, kerning and full Unicode (Type0/CIDFontType2 with ToUnicode, so text extracts and copies correctly). Register a font once, then reference it by its integer id.

addFontFile(path) → number   addFont(data: Buffer) → number
showText(font, size, x, y, text, headingLevel?)

headingLevel (1–6) tags the run as H1H6 in an accessible document (see Accessibility); leave it out (or 0) for ordinary text.

javascript
const doc = new Document();
const regular = doc.addFontFile("Roboto-Regular.ttf");
// …or from bytes you already have in memory:
// const regular = doc.addFont(fs.readFileSync("Roboto-Regular.ttf"));

doc.addPage();
doc.showText(regular, 28, 72, 760, "Invoice #1024");
doc.showText(regular, 12, 72, 720, "日本語 · Ελληνικά · العربية");
doc.save("text.pdf");
doc.close();

Paragraphs

The paragraph layer wraps, aligns and justifies text inside a fixed width (greedy line breaking using shaped glyph widths).

paragraph(font, size, x, y, width, text, align?)
javascript
const { Document, Align } = require("rustpdf");

const intro =
  "A long paragraph that wraps to the given width and is justified " +
  "automatically; extra space is distributed between words.";

const doc = new Document();
const f = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage();
doc.paragraph(f, 12, 72, 700, 451, intro, Align.Justify);
doc.save("paragraph.pdf");
doc.close();

See the Align enum for the alignment options.

Images

JPEGs are embedded verbatim (DCTDecode, no re-encode). PNGs are decoded and re-encoded (FlateDecode); alpha becomes an /SMask, palette becomes an Indexed color space. Register an image once, draw it many times.

MethodDescription
addImageFile(path) → numberLoad JPEG/PNG from a file; returns the image id.
addImagePng(data: Buffer) → numberRegister a PNG from memory.
addImageJpeg(data: Buffer) → numberRegister a JPEG from memory.
drawImage(image, x, y, w, h)Draw at (x, y) scaled to w × h points.
figure(image, x, y, w, h, alt)Draw as a tagged /Figure with alt text (accessibility).
javascript
const doc = new Document();
const logo = doc.addImageFile("logo.png");
doc.addPage();
doc.drawImage(logo, 72, 680, 160, 90);
doc.save("with_image.pdf");
doc.close();

PDF/A licensed

Produce archival-grade output. pdfa() defaults to A-2b; pass a PdfaLevel for a specific level. An embedded sRGB ICC profile, output intent, XMP metadata and document /ID are added automatically; A-1b also forces PDF 1.4 and emits a /CIDSet.

pdfa(level?)
javascript
const { Document, PdfaLevel } = require("rustpdf");

const doc = new Document();
doc.pdfa(PdfaLevel.A2b).setInfo({ title: "Q3 Report", author: "Acme Inc." });
const f = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage();
doc.showText(f, 20, 72, 760, "Archival report");
doc.save("report_pdfa.pdf");     // throws PdfError without a license granting PDF/A
doc.close();
PDF/A requires the title to be set for valid metadata: call setInfo({ title: … }).

Accessibility (Tagged PDF / PDF/UA) licensed

tagged() builds a logical structure tree (PDF/UA-1). Combine with pdfa(PdfaLevel.A2a) for archival and accessible output. Use headingLevel on showText for H1H6, and figure(..., alt) for described images.

tagged()
javascript
const { Document, PdfaLevel } = require("rustpdf");

const doc = new Document();
doc.pdfa(PdfaLevel.A2a).tagged().setInfo({ title: "Accessible report" });
const f = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage();
doc.showText(f, 26, 72, 760, "Annual report", 1);   // H1
doc.showText(f, 14, 72, 720, "Overview", 2);         // H2
doc.showText(f, 11, 72, 690, "Body paragraph of the section…");
const chart = doc.addImageFile("chart.png");
doc.figure(chart, 72, 520, 300, 150, "Revenue grew 18% year over year");
doc.save("accessible.pdf");
doc.close();

Attachments (PDF/A-3) licensed

PDF/A-3 allows embedding arbitrary source files (e.g. the XML behind an e-invoice). Each attachment carries a MIME type and an AFRelationship.

attachFile(name, mime, data, relationship?, description?)
javascript
const fs = require("fs");
const { Document, PdfaLevel, AFRelationship } = require("rustpdf");

const xml = fs.readFileSync("invoice.xml");
const doc = new Document();
doc.pdfa(PdfaLevel.A3b).setInfo({ title: "E-invoice 1024" });
const f = doc.addFontFile("Roboto-Regular.ttf");
doc.addPage();
doc.showText(f, 18, 72, 760, "Invoice 1024");
doc.attachFile("invoice.xml", "text/xml", xml,
               AFRelationship.Source, "Structured invoice data");
doc.save("einvoice.pdf");
doc.close();

AcroForm fields

Build interactive forms with generated appearance streams (no NeedAppearances). Rectangles are [x0, y0, x1, y1] arrays; page is a 0-based page index. Dotted names ("a.b.c") create hierarchical fields.

MethodDescription
textField(name, page, rect, value?, size?)Text input (size=0 → auto font size).
checkbox(name, page, rect, checked)Checkbox.
dropdown(name, page, rect, options, selected?, size?)Combo box from an array of strings.
radioGroup(name, page, buttons, selected?)buttons = array of { rect, export } objects.
javascript
const doc = new Document();
doc.addPage();
doc.textField("applicant.name", 0, [72, 700, 320, 720], "");
doc.checkbox("agree", 0, [72, 660, 88, 676], false);
doc.dropdown("plan", 0, [72, 620, 240, 640],
             ["Starter", "Pro", "Enterprise"], 1);
doc.radioGroup("billing", 0, [
  { rect: [72, 580, 88, 596], export: "monthly" },
  { rect: [140, 580, 156, 596], export: "annual" },
], 1);
doc.save("form.pdf");
doc.close();

Fill fields later with EditableDoc.fillTextField.

Metadata

setInfo({ title?, author?, subject?, keywords?, creator? })

Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the fields you need.

javascript
doc.setInfo({ title: "Q3 Report", author: "Acme Inc.",
              subject: "Quarterly results", keywords: "finance, q3" });

Manipulation: load an existing PDF

EditableDoc parses an existing document (classic & xref streams, object streams, all standard filters, RC4/AES decryption) into an editable model. Pages are a flat list; the page tree is rebuilt on output.

EditableDoc.load(data: Buffer, password?)
EditableDoc.loadFile(path, password?)
javascript
const { EditableDoc } = require("rustpdf");

const ed = EditableDoc.loadFile("in.pdf");
console.log(ed.pageCount);
ed.close();

// encrypted input:
const sec = EditableDoc.loadFile("secured.pdf", "user-or-owner-pw");
sec.save("plain.pdf");
sec.close();

Pages: merge, split, reorder, rotate

MethodDescription
merge(other)Append all pages of another EditableDoc (objects renumbered & remapped).
rotatePage(index, degrees)Rotate one page (90 / 180 / 270).
deletePage(index)Remove a page.
reorderPages(order)Reorder with a full permutation array of indices.
extractPages(indices) → EditableDocNew document containing just those pages.
pageCountProperty: current page count.
javascript
const a = EditableDoc.loadFile("a.pdf");
const b = EditableDoc.loadFile("b.pdf");
a.merge(b);                        // a now has a's pages followed by b's
a.rotatePage(0, 90);
a.reorderPages([...Array(a.pageCount).keys()].reverse());
a.save("merged.pdf");
b.close();

const subset = a.extractPages([0, 2]);   // pages 1 and 3
subset.save("subset.pdf");
subset.close();
a.close();

Metadata, overlay & form fill

MethodDescription
setInfo(key, value)Set one info entry (e.g. "Title").
getInfo(key) → stringRead an info entry.
setXmp(xml: Buffer)Replace the XMP metadata stream.
overlayPage(index, content: Buffer)Overlay a content-stream fragment onto a page (stamps/watermarks).
fillTextField(name, value) → booleanFill an AcroForm text field; returns whether it was found.
javascript
const ed = EditableDoc.loadFile("form.pdf");
ed.setInfo("Title", "Filled form");
const found = ed.fillTextField("applicant.name", "Jane Doe");
console.log("filled:", found, "| title:", ed.getInfo("Title"));
ed.save("filled.pdf");
ed.close();

Optimize & compact

MethodDescription
optimize()Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects.
compact(on?)Pack objects into object streams + emit a cross-reference stream.
javascript
const ed = EditableDoc.loadFile("big.pdf");
ed.optimize().compact(true);
ed.save("small.pdf");
ed.close();

Encryption licensed

Apply standard-handler encryption at output. AES-256 (V5/R6) uses OS-CSPRNG keys/IVs.

encrypt({ method?, user?, owner?, readOnly? })
javascript
const { EditableDoc, Encryption } = require("rustpdf");

const ed = EditableDoc.loadFile("in.pdf");
ed.encrypt({ user: "", owner: "owner-secret",
             method: Encryption.Aes256, readOnly: true });
ed.save("secured.pdf");          // throws PdfError without an Encryption license
ed.close();

See the Encryption enum for RC4 / AES-128 / AES-256.

Output & incremental update

MethodDescription
toBytes() → BufferSerialize the manipulated document.
save(path)Serialize to a file.
toBytesIncremental(original: Buffer) → BufferAppend only changes to the original bytes (signature-safe, non-destructive).
javascript
const fs = require("fs");
const original = fs.readFileSync("in.pdf");
const ed = EditableDoc.load(original);
ed.setInfo("Subject", "reviewed");
const incremental = ed.toBytesIncremental(original);   // original bytes preserved verbatim
fs.writeFileSync("reviewed.pdf", incremental);
ed.close();

Digital signatures licensed

Sign a PDF with a PKCS#7 detached signature via an incremental update (the original bytes are preserved). Keys and certificates are passed as DER Buffers. pades: true switches to PAdES-B-B.

rustpdf.sign(pdf, keyDer, certDer, { reason?, location?, name?, pades? }?) → Buffer
javascript
const fs = require("fs");
const rustpdf = require("rustpdf");

const pdf     = fs.readFileSync("contract.pdf");
const keyDer  = fs.readFileSync("signing-key.pkcs8.der");   // PKCS#8 private key (DER)
const certDer = fs.readFileSync("signing-cert.der");        // X.509 certificate (DER)

const signed = rustpdf.sign(pdf, keyDer, certDer, {
  reason: "Approved", location: "New York",
  name: "Jane Doe", pades: true,
});
fs.writeFileSync("contract.signed.pdf", signed);
// Verify in a shell: pdfsig contract.signed.pdf  →  "Signature is Valid."

Timestamp & DSS (PAdES LTV) licensed

Build long-term-validation signatures offline. addDss appends a Document Security Store (/DSS with certs/CRLs, PAdES-B-LT); timestamp appends an RFC 3161 document timestamp (/DocTimeStamp, PAdES-B-LTA).

rustpdf.addDss(pdf, certs?, crls?) → Buffer
rustpdf.timestamp(pdf, tsaKeyDer, tsaCertDer, date?) → Buffer
javascript
const signed = fs.readFileSync("contract.signed.pdf");

// B-LT: embed validation material (caller supplies DER certs/CRLs)
const lt = rustpdf.addDss(signed, [certDer], [crlDer]);

// B-LTA: add a document timestamp signed by a TSA key/cert
const lta = rustpdf.timestamp(lt, tsaKeyDer, tsaCertDer);
fs.writeFileSync("contract.lta.pdf", lta);

Text extraction

Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference.

rustpdf.extractText(data: Buffer) → string free
javascript
const data = fs.readFileSync("report.pdf");
console.log(rustpdf.extractText(data));

Enums

PdfaLevel

ValueLevel
PdfaLevel.A1bPDF/A-1b (basic, PDF 1.4)
PdfaLevel.A2bPDF/A-2b (basic): default of pdfa()
PdfaLevel.A2aPDF/A-2a (accessible: pair with tagged())
PdfaLevel.A3bPDF/A-3b (basic, allows attachments)
PdfaLevel.A3aPDF/A-3a (accessible + attachments)

Align

ValueMeaning
Align.LeftLeft-aligned (default)
Align.RightRight-aligned
Align.CenterCentered
Align.JustifyJustified (space distributed between words)

AFRelationship

ValueMeaning
AFRelationship.SourceSource data for the document (e.g. the invoice XML)
AFRelationship.DataData used to derive the visual content
AFRelationship.AlternativeAlternative representation
AFRelationship.SupplementSupplementary material
AFRelationship.UnspecifiedUnspecified relationship

Encryption

ValueCipher
Encryption.Rc4RC4 (legacy)
Encryption.Aes128AES-128
Encryption.Aes256AES-256 (V5/R6): recommended

Error handling

Every failing native call throws PdfError (an Error) carrying the status (PdfStatus) code and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too.

javascript
const { Document, PdfError } = require("rustpdf");

try {
  const doc = new Document();
  doc.pdfa().setInfo({ title: "x" });
  doc.addPage();
  doc.save("out.pdf");
  doc.close();
} catch (e) {
  if (e instanceof PdfError) {
    console.error("failed:", e.status, e.message);
    // e.g. PdfStatus=7: feature 'pdfa' requires a valid license
  }
}

Utilities

FunctionDescription
rustpdf.version() → stringNative library version string.
rustpdf.activateLicense(token)Activate a license token (throws on invalid/expired).
Looking for another language? The same API exists in Python, Delphi / Free Pascal, Swift, C#, Go, PHP, Ruby and Java: browse all docs. They share one core, so behavior is identical.