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.
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.
npm install rustpdfRequires Node.js 18+. Verify it loaded:
const rustpdf = require("rustpdf");
console.log(rustpdf.version()); // native library version@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:
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:
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.
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:
export RUSTPDF_LICENSE="010f0000…" # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txtOr activate explicitly in code:
const rustpdf = require("rustpdf");
rustpdf.activateLicense(token); // throws PdfError if forged / expired / malformedCoordinate system
- Units are points (1 pt = 1/72 inch). A4 is
595 × 842, US Letter612 × 792. - The origin
(0, 0)is the bottom-left corner;ygrows upward. - For text,
(x, y)is the baseline of the first glyph. - Drawing/text always targets the most recently added page.
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.
- Documents are independent. Each
Document/EditableDocshares no state, so they're safe to build acrossworker_threads, one handle per worker. - Don't share a live handle between workers; give each its own.
- Errors are per-thread. The native last-error is thread-local, so a failure in one worker never clobbers another's, and
PdfErrorsurfaces on the calling side. - License is process-global.
activateLicense(or the env var) applies to every worker; activate once at startup. - Heavy jobs: generating a large PDF on the main thread will stall other requests, so offload to a worker.
// 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() freeCreates an empty document (A4 default page size). Call close() to free the native handle.
| Method | Description |
|---|---|
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). |
pageCount | Property: 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.
| Method | Description |
|---|---|
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. |
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) → numbershowText(font, size, x, y, text, headingLevel?)headingLevel (1–6) tags the run as H1–H6 in an accessible document (see Accessibility); leave it out (or 0) for ordinary text.
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?)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.
| Method | Description |
|---|---|
addImageFile(path) → number | Load JPEG/PNG from a file; returns the image id. |
addImagePng(data: Buffer) → number | Register a PNG from memory. |
addImageJpeg(data: Buffer) → number | Register 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). |
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?)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();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 H1–H6, and figure(..., alt) for described images.
tagged()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?)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.
| Method | Description |
|---|---|
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. |
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.
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?)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
| Method | Description |
|---|---|
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) → EditableDoc | New document containing just those pages. |
pageCount | Property: current page count. |
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
| Method | Description |
|---|---|
setInfo(key, value) | Set one info entry (e.g. "Title"). |
getInfo(key) → string | Read 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) → boolean | Fill an AcroForm text field; returns whether it was found. |
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
| Method | Description |
|---|---|
optimize() | Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects. |
compact(on?) | Pack objects into object streams + emit a cross-reference stream. |
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? })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
| Method | Description |
|---|---|
toBytes() → Buffer | Serialize the manipulated document. |
save(path) | Serialize to a file. |
toBytesIncremental(original: Buffer) → Buffer | Append only changes to the original bytes (signature-safe, non-destructive). |
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? }?) → Bufferconst 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?) → Bufferrustpdf.timestamp(pdf, tsaKeyDer, tsaCertDer, date?) → Bufferconst 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 freeconst data = fs.readFileSync("report.pdf");
console.log(rustpdf.extractText(data));Enums
PdfaLevel
| Value | Level |
|---|---|
PdfaLevel.A1b | PDF/A-1b (basic, PDF 1.4) |
PdfaLevel.A2b | PDF/A-2b (basic): default of pdfa() |
PdfaLevel.A2a | PDF/A-2a (accessible: pair with tagged()) |
PdfaLevel.A3b | PDF/A-3b (basic, allows attachments) |
PdfaLevel.A3a | PDF/A-3a (accessible + attachments) |
Align
| Value | Meaning |
|---|---|
Align.Left | Left-aligned (default) |
Align.Right | Right-aligned |
Align.Center | Centered |
Align.Justify | Justified (space distributed between words) |
AFRelationship
| Value | Meaning |
|---|---|
AFRelationship.Source | Source data for the document (e.g. the invoice XML) |
AFRelationship.Data | Data used to derive the visual content |
AFRelationship.Alternative | Alternative representation |
AFRelationship.Supplement | Supplementary material |
AFRelationship.Unspecified | Unspecified relationship |
Encryption
| Value | Cipher |
|---|---|
Encryption.Rc4 | RC4 (legacy) |
Encryption.Aes128 | AES-128 |
Encryption.Aes256 | AES-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.
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
| Function | Description |
|---|---|
rustpdf.version() → string | Native library version string. |
rustpdf.activateLicense(token) | Activate a license token (throws on invalid/expired). |