pdfium-native
v0.10.0
Published
Native Node.js bindings for PDFium
Maintainers
Readme
pdfium-native
Fast, native PDF rendering and text extraction for Node.js — powered by PDFium, the same engine used in Chromium. Built as a C++ addon with N-API for ABI stability across Node.js versions.
Designed for server-side workloads. Non-blocking, fast, and production-ready.
🚀 Quick Start
import { loadDocument } from 'pdfium-native';
const doc = await loadDocument('invoice.pdf');
const page = await doc.getPage(0);
const text = await page.getText();
const image = await page.render({ scale: 3, format: 'png' }); // high-resolution render
page.close();
doc.destroy();💡 Why pdfium-native?
⚡ Performance
- Native C++ — no WASM overhead, no JS parsing
- Non-blocking — all operations run off the main thread via libuv workers
🛠️ Developer experience
- Built-in JPEG/PNG rendering — no extra dependencies like sharp
- Prebuilt binaries for 10 platform/arch combinations — no compile step
- Full TypeScript support with types included
🔒 Reliability
- Built on PDFium — the PDF engine used in Chromium
- ABI-stable via N-API — works across Node.js 22–24 without recompilation
- Password-protected PDFs supported out of the box
🎯 Use cases
- 🖼️ Generate thumbnails and previews for uploaded PDFs
- 📄 Extract searchable text from documents at scale
- ⚙️ Build server-side PDF processing pipelines
- ✂️ Split, merge, reorder, or impose PDFs — including n-up handout layouts
- 🔗 Read annotations, bookmarks, links, and form fields from existing PDFs
- 📐 Map extracted text back to page coordinates for redaction or layout analysis
- ✍️ Inspect digital signatures and document-level JavaScript before trusting a file
- ♿ Audit tagged-PDF structure and alternate text for accessibility
- 🗄️ Flatten filled forms for archiving
📊 How it compares
| | pdfium-native | @hyzyla/pdfium | pdfjs-dist | | --------------- | -------------------- | ------------------- | ----------------- | | Engine | PDFium (C++ addon) | PDFium (WASM) | pdf.js (JS) | | Rendering | ✅ JPEG/PNG built-in | ⚠️ Raw bitmap (BYO) | ⚠️ Canvas/browser | | Text extraction | ✅ Plain + per-char | ❌ | ✅ | | Search | ✅ With rects | ❌ | ⚠️ Manual | | Split / Merge | ✅ + reorder / n-up | ❌ | ❌ | | Signatures | ✅ Metadata + blob | ❌ | ⚠️ Partial | | Struct tree | ✅ | ❌ | ✅ | | Flatten | ✅ | ❌ | ❌ | | Annotations | ✅ | ❌ | ⚠️ Partial | | Bookmarks | ✅ | ❌ | ✅ | | Links | ✅ | ❌ | ✅ | | Form fields | ✅ | ❌ | ✅ | | Attachments | ✅ Files + annots | ❌ | ✅ | | Async I/O | ✅ libuv workers | ❌ Sync | ❌ Main thread | | Platforms | macOS/Linux/Windows | Any (WASM) | Any |
¹ Prebuilt binaries downloaded at install — no runtime dependencies. Falls back to source compilation if unavailable.
Table of Contents
📦 Install
npm install pdfium-nativePrebuilt binaries are available for all supported platforms — most installs require no compiler. If no prebuilt is available, the package falls back to compiling from source (requires a C++ toolchain: Xcode CLI tools on macOS, build-essential on Linux, Visual Studio on Windows).
🌍 Supported Platforms
| OS | Architectures | | --------------------- | ---------------------- | | macOS | arm64, x64 | | Linux (glibc) | x64, arm64, arm, ppc64 | | Linux (musl / Alpine) | x64, arm64 | | Windows | x64, arm64 |
📚 API
loadDocument(input, password?)
Opens a PDF from a Buffer or file path string. Returns Promise<PDFiumDocument>.
const doc = await loadDocument(buffer);
const doc = await loadDocument('/path/to/file.pdf');
const doc = await loadDocument(buffer, 'secret');splitDocument(input, splitAt, options?)
Splits a PDF into multiple documents at the given page indices. Each index in splitAt marks the first page of a new chunk. Returns Promise<Buffer[]>, or Promise<void> if outputs is set.
A 10-page PDF with splitAt: [3, 7] produces three documents: pages 0–2, 3–6, and 7–9.
import { splitDocument } from 'pdfium-native';
// split a two-page PDF into two single-page documents
const [part1, part2] = await splitDocument('report.pdf', [1]);
// split into three parts (no split points = single document containing all pages)
const [a, b, c] = await splitDocument(buffer, [3, 7]);
// write parts to files
await splitDocument('report.pdf', [5], {
outputs: ['first-half.pdf', 'second-half.pdf'],
});
// password-protected source
const parts = await splitDocument('encrypted.pdf', [3], { password: 'secret' });| Option | Type | Default | Description |
| ---------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------- |
| outputs | string[] | — | Write each part to these file paths instead of returning Buffers. Must have splitAt.length + 1 entries. |
| password | string | — | Password for the source PDF. |
mergeDocuments(inputs, options?)
Combines multiple PDFs into a single document. Returns Promise<Buffer>, or Promise<void> if output is set.
Each element of inputs can be a Buffer, a file path string, or an object { input, password? } for password-protected PDFs.
import { mergeDocuments } from 'pdfium-native';
// merge two files
const buf = await mergeDocuments(['part1.pdf', 'part2.pdf']);
// mix buffers, paths, and password-protected PDFs
const buf = await mergeDocuments([
buffer1,
'part2.pdf',
{ input: 'encrypted.pdf', password: 'secret' },
]);
// write directly to a file
await mergeDocuments(['a.pdf', 'b.pdf'], { output: 'merged.pdf' });| Option | Type | Default | Description |
| -------- | -------- | ------- | ------------------------------------------------------ |
| output | string | — | Write to this file path instead of returning a Buffer. |
assemblePages(input, pages, options?)
Builds a new PDF from selected pages of input, in the order given. Returns Promise<Buffer>, or Promise<void> when options.output is set.
Where splitDocument only cuts a document into consecutive runs, the index list here is taken literally: pages may be reordered, left out, or repeated.
import { assemblePages } from 'pdfium-native';
// reverse a four-page document
const reversed = await assemblePages('doc.pdf', [3, 2, 1, 0]);
// pull out one page
await assemblePages('doc.pdf', [2], { output: 'page3.pdf' });
// repeat a page — e.g. a cover sheet before each section
const buf = await assemblePages('doc.pdf', [0, 1, 0, 2]);| Option | Type | Default | Description |
| ---------- | -------- | ------- | ------------------------------------------------------ |
| output | string | — | Write to this file path instead of returning a Buffer. |
| password | string | — | Password for the source PDF, if encrypted. |
Rejects if any index is outside 0 … pageCount - 1, naming the offending index.
nUpPages(input, options)
Imposes columns × rows source pages onto each page of a new document — the classic "n-up" layout for handouts and proof sheets. Returns Promise<Buffer>, or Promise<void> when options.output is set.
import { nUpPages } from 'pdfium-native';
// four source pages per sheet, sheet size taken from the first source page
const handout = await nUpPages('slides.pdf', { columns: 2, rows: 2 });
// two-up on landscape A4
await nUpPages('doc.pdf', {
columns: 2,
rows: 1,
width: 842,
height: 595,
output: 'twoup.pdf',
});| Option | Type | Default | Description |
| ---------- | -------- | ----------------- | ------------------------------------------------------ |
| columns | number | — (required) | Source pages placed side by side across each sheet. |
| rows | number | — (required) | Source pages stacked down each sheet. |
| width | number | first source page | Output sheet width in points. |
| height | number | first source page | Output sheet height in points. |
| output | string | — | Write to this file path instead of returning a Buffer. |
| password | string | — | Password for the source PDF, if encrypted. |
The sheet defaults to the size of the first source page, so a 2×2 n-up of A4 pages lands on A4 with each source page scaled to a quarter. Partial sheets are allowed: 4 pages at 3 per sheet produce 2 sheets.
addAttachments(input, attachments, options?)
Embeds files into a copy of input. Returns Promise<Buffer>, or Promise<void> when options.output is set.
This does not produce a conformant PDF/A-3 e-invoice. PDFium's write API reaches only the file name, the bytes and the
/Paramsdates. It cannot set the MIME type (/Subtype), the description (/Desc), the/AFRelationshipthat marks an attachment as the invoice source, the catalog/AFarray, an OutputIntent, or the XMP metadata — all of which ZUGFeRD, Factur-X and XRechnung require. Use a PDF/A toolchain to produce e-invoices. Reading them is fully supported bygetAttachments().
import { addAttachments } from 'pdfium-native';
const pdf = await addAttachments('report.pdf', [
{ name: 'data.csv', data: csvBuffer },
{ name: 'notes.txt', data: Buffer.from('...'), creationDate: 'D:20250101120000Z' },
]);interface AttachmentInput {
name: string; // must be non-empty and not already in the document
data: Buffer;
creationDate?: string; // PDF date string; defaults to now
modDate?: string;
}| Option | Type | Default | Description |
| ---------- | -------- | ------- | ------------------------------------------------------ |
| output | string | — | Write to this file path instead of returning a Buffer. |
| password | string | — | Password for the source PDF, if encrypted. |
Rejects on an empty or duplicate file name.
flattenDocument(input, options?)
Merges annotations and form widgets into the page content and removes them. Returns Promise<Buffer>, or Promise<void> when options.output is set.
The result renders identically in every viewer but is no longer interactive — which is the point when archiving, or handing a filled form to a system that ignores annotations.
import { flattenDocument } from 'pdfium-native';
const archived = await flattenDocument('filled-form.pdf');
// print appearances, first page only
await flattenDocument('doc.pdf', { usage: 'print', pages: [0], output: 'out.pdf' });| Option | Type | Default | Description |
| ---------- | ---------- | ----------- | ------------------------------------------------------- |
| usage | string | 'display' | 'display' or 'print' — which appearance to bake in. |
| pages | number[] | all pages | Only flatten these page indices. |
| output | string | — | Write to this file path instead of returning a Buffer. |
| password | string | — | Password for the source PDF, if encrypted. |
This bakes in the appearance streams the document already carries rather than regenerating them. That is correct for any PDF whose widgets have valid /AP entries — what every mainstream producer writes. A form flagged /NeedAppearances, which asks the viewer to regenerate appearances itself, is the case this cannot help with. Interactive form filling is not supported; see Not supported.
PDFiumDocument
| Property | Type | Description |
| ----------- | ------------------ | --------------------------------------- |
| pageCount | number | Total number of pages. |
| metadata | DocumentMetadata | Title, author, dates, PDF version, etc. |
getPage(index)
Loads a page by 0-based index. Returns Promise<PDFiumPage>.
pages()
Async generator that yields every page. Caller must close each page.
for await (const page of doc.pages()) {
console.log(await page.getText());
page.close();
}getBookmarks()
Returns the bookmark/outline tree. Returns Promise<Bookmark[]>.
interface Bookmark {
title: string;
pageIndex?: number;
open: boolean; // whether the node is initially expanded
actionType?: 'goto' | 'remoteGoto' | 'uri' | 'launch' | 'embeddedGoto';
url?: string; // external URL for URI bookmarks
destX?: number; // destination X coordinate
destY?: number; // destination Y coordinate
destZoom?: number; // destination zoom level
children?: Bookmark[];
}getAttachments()
Lists every embedded file (attachment) in the document's /EmbeddedFiles name tree. Returns Promise<Attachment[]>. This reads only dictionary metadata — it does not decode the file streams; use getAttachment(index) to read the bytes.
interface Attachment {
index: number; // 0-based index in the embedded-files name tree
name: string; // file name, e.g. 'factur-x.xml'
mimeType: string; // /Subtype, e.g. 'text/xml' ('' if the PDF omits it)
afRelationship?: string; // /AFRelationship, e.g. 'Alternative' (absent if the PDF omits it)
creationDate?: string; // PDF date string, e.g. 'D:20250101120000Z'
modDate?: string; // PDF date string
}afRelationship is the associated-file relationship defined by PDF 2.0 (ISO 32000-2:2020, Table 43) and used by PDF/A-3: it states an embedded file's role rather than its type. A ZUGFeRD / Factur-X e-invoice marks the machine-readable invoice XML Alternative (it is an alternative representation of the visible page), while a human-readable companion is Supplement; Source, Data and Unspecified are the other standard values. Without it, an e-invoice reader has to guess which attachment is the invoice from its file name. The key is absent when the PDF omits the entry, and addAttachments() cannot set it (see Writing attachments), so attachments written by this library report no value.
getAttachment(index, options?)
Reads the raw bytes of the attachment at index. Returns Promise<Buffer>, or Promise<void> when options.output is a file path (the bytes are written there instead).
// Extract the embedded XML from a ZUGFeRD / Factur-X / XRechnung PDF/A-3 e-invoice.
// The PDF stays the display artifact; the XML is the structured source of truth.
const E_INVOICE_NAMES = ['factur-x.xml', 'zugferd-invoice.xml', 'xrechnung.xml'];
const doc = await loadDocument('invoice.pdf');
const attachments = await doc.getAttachments();
const entry = attachments.find((a) => E_INVOICE_NAMES.includes(a.name.toLowerCase()));
if (entry) {
const xml = await doc.getAttachment(entry.index); // Buffer of the exact embedded bytes
console.log(xml.toString('utf8'));
}
// or write straight to disk:
await doc.getAttachment(0, { output: 'attachment.xml' });
doc.destroy();Rejects if index is out of range (0 … attachmentCount - 1). metadata.attachmentCount gives the count without loading anything.
getSignatures()
Lists every digital signature in the document. Returns Promise<Signature[]>.
Nothing here is cryptographically verified — PDFium does not do that. These values are what the signature dictionary declares, so a well-formed entry proves only that the document claims to be signed.
interface Signature {
index: number; // 0-based index in the AcroForm field list
subFilter: string; // encoding, e.g. 'adbe.pkcs7.detached', 'ETSI.CAdES.detached'
reason?: string; // /Reason, if given
time?: string; // /M as a PDF date string, e.g. "D:20250101120000+01'00'"
docMdpPermission?: 1 | 2 | 3; // certification level; absent for ordinary signatures
byteRange: number[]; // flat (offset, length) pairs covered by the digest
contentsLength: number; // size of /Contents in bytes
}const doc = await loadDocument('contract.pdf');
for (const sig of await doc.getSignatures()) {
// A signature covers the whole file only if its last range ends at the file
// size. Anything less means content was appended after signing.
const end = sig.byteRange.at(-2)! + sig.byteRange.at(-1)!;
console.log(sig.subFilter, sig.reason, 'covers', end, 'bytes');
}metadata.signatureCount gives the count without listing anything.
getSignatureContents(index, options?)
Reads the raw /Contents bytes of the signature at index — a DER-encoded PKCS#1 or PKCS#7 binary. Returns Promise<Buffer>, or Promise<void> when options.output is a file path.
Pass this, together with the byteRange from getSignatures(), to a crypto library to actually validate the signature.
getJavaScriptActions()
Lists the document-level scripts a viewer runs when the document opens. Returns Promise<JavaScriptAction[]>.
Nothing is executed — the bundled PDFium is built with V8 disabled, so scripts come back as inert text. This is for inspection and triage.
interface JavaScriptAction {
index: number;
name: string; // entry name in the /Names /JavaScript tree
script: string; // the script source, as text
}const doc = await loadDocument('untrusted.pdf');
const scripts = await doc.getJavaScriptActions();
if (scripts.length > 0) {
console.warn(`${scripts.length} document-open script(s) — review before rendering`);
}getNamedDestinations()
Lists the document's named destinations — the anchors that GoTo actions and external links target by name rather than by page number. Returns Promise<NamedDestination[]>.
Both storage forms are read: the modern /Names /Dests name tree and the legacy /Dests catalog dictionary.
interface NamedDestination {
name: string; // e.g. 'Chapter2'
pageIndex?: number; // resolved target page
view: 'xyz' | 'fit' | 'fitH' | 'fitV' | 'fitR' | 'fitB' | 'fitBH' | 'fitBV' | 'unknown';
viewParams: number[]; // up to 4 numbers; meaning depends on `view`
destX?: number; // 'xyz' destinations only
destY?: number;
destZoom?: number;
}// Resolve "document.pdf#Chapter2" to a page index without walking every link.
const doc = await loadDocument('document.pdf');
const target = (await doc.getNamedDestinations()).find((d) => d.name === 'Chapter2');
console.log(target?.pageIndex);destroy()
Closes the document and frees all native resources. Must be called when done.
PDFiumPage
| Property | Type | Description |
| ----------------- | ------------------- | --------------------------------------------------- |
| width | number | Page width in points (1 pt = 1/72 inch). |
| height | number | Page height in points. |
| number | number | 0-based page index. |
| objectCount | number | Number of page objects (text, images, paths, etc.). |
| rotation | number | Page rotation: 0, 1 (90° CW), 2 (180°), 3 (270°). |
| hasTransparency | boolean | Whether the page has transparency. |
| label | string? | Page label (e.g. 'i', 'ii', '1'). |
| cropBox | PageObjectBounds? | Crop box (visible region), if set. |
| trimBox | PageObjectBounds? | Trim box (intended finished size), if set. |
getText()
Extracts all text from the page. Returns Promise<string>.
getCharacters(options?)
Extracts every character with the geometry getText() throws away — bounding box, baseline origin, font and rotation. Returns Promise<TextCharacter[]>.
Character indices line up with getText() and with search() results, so a match maps straight back to page coordinates.
interface TextCharacter {
index: number; // 0-based, matches the offset in getText()
char: string; // '' when the glyph has no Unicode mapping
unicode: number;
bounds?: PageObjectBounds; // tight box around the glyph's ink
x?: number; // baseline origin
y?: number;
fontSize: number;
fontName: string; // e.g. 'Helvetica-Bold'
fontFlags: number;
fontWeight?: number;
angle: number; // radians, [0, 2π) — see the note below
isGenerated: boolean;
isHyphen: boolean;
hasUnicodeMapError: boolean;
}const page = await doc.getPage(0);
// Where on the page is the total?
const chars = await page.getCharacters();
const [match] = await page.search('Total');
const box = chars[match.charIndex].bounds;
// Dense pages can hold tens of thousands of characters — page through instead
// of materialising all of them at once.
const firstThousand = await page.getCharacters({ start: 0, count: 1000 });Two PDFium behaviours worth knowing:
isGeneratedmarks characters PDFium synthesized rather than read from the content stream — line breaks between text runs, and spaces inferred from glyph spacing. They carry no real font, sofontNameis'',fontSizeis1andfontWeightis absent.angleruns clockwise. PDFium derives it asatan2(c, a)from the text matrix, so text rotated 45° counterclockwise on the page reports ≈5.4978(2π − π/4), not ≈0.7854.
getStructTree()
Returns the tagged-PDF structure tree for this page — the logical outline (headings, paragraphs, tables, figures) that screen readers follow, and where alternate text lives. Returns Promise<StructElement[]>.
The tree is per-page: these are the elements whose content is on this page, not the whole document's tree. Returns an empty array for an untagged page; metadata.isTagged says up front whether there is anything to find.
interface StructElement {
type: string; // /S, e.g. 'H1', 'P', 'Table', 'Figure'
objType?: string; // /Type, normally 'StructElem'
title?: string; // /T
altText?: string; // /Alt — the accessibility description
actualText?: string; // /ActualText
id?: string; // /ID
lang?: string; // /Lang override for this subtree
markedContentId?: number;
children?: StructElement[];
}// Accessibility check: which figures are missing alternate text?
const walk = (nodes: StructElement[]): StructElement[] =>
nodes.flatMap((n) => [n, ...walk(n.children ?? [])]);
const missing = walk(await page.getStructTree()).filter((n) => n.type === 'Figure' && !n.altText);render(options?)
Renders the page to an encoded image. Returns Promise<Buffer>, or Promise<void> when output is specified.
interface PageRenderOptions {
scale?: number; // default: 1 (72 DPI). Use 3–4 for print quality.
width?: number; // override render width in pixels
height?: number; // override render height in pixels
format?: 'jpeg' | 'png'; // default: 'png'
quality?: number; // JPEG quality 1–100 (default: 100)
output?: string; // write to file instead of returning a Buffer
rotation?: 0 | 1 | 2 | 3; // 0=none, 1=90° CW, 2=180°, 3=270° CW
transparent?: boolean; // transparent background (PNG only, default: false)
renderAnnotations?: boolean; // render annotations (default: true)
grayscale?: boolean; // render in grayscale
lcdText?: boolean; // LCD-optimized sub-pixel text rendering
}getObject(index)
Returns the page object at the given index. Returns Promise<PageObject>. Objects are discriminated by type:
type PageObject = TextPageObject | ImagePageObject | OtherPageObject;
// all objects have:
// bounds: { left, bottom, right, top }
// fillColor: { r, g, b, a } | null
// strokeColor: { r, g, b, a } | null
// type: 'text' adds: text, fontSize, fontName, fontWeight?, italicAngle?,
// renderMode?, fontFamily?, isEmbedded?, fontFlags?
// type: 'image' adds: imageWidth, imageHeight, horizontalDpi?, verticalDpi?,
// bitsPerPixel?, colorspace?, filters?, render()
// type: 'path' | 'shading' | 'form' | 'unknown'Image objects have a render() method for extracting the embedded image:
const obj = await page.getObject(0);
if (obj.type === 'image') {
const png = await obj.render(); // PNG buffer (default)
const jpeg = await obj.render({ format: 'jpeg', quality: 80 });
const raw = await obj.render({ format: 'raw' }); // original encoded stream
await obj.render({ output: '/tmp/image.png' }); // write to file
await obj.render({ rendered: true }); // apply image mask and transformation matrix
}interface ImageRenderOptions {
format?: 'jpeg' | 'png' | 'raw'; // default: 'png'. 'raw' returns original stream bytes
quality?: number; // JPEG quality 1–100 (default: 100)
output?: string; // write to file instead of returning a Buffer
rendered?: boolean; // apply image mask and transformation matrix (default: false)
}objects()
Async generator that yields every page object. Convenience wrapper around getObject().
for await (const obj of page.objects()) {
if (obj.type === 'image') {
await obj.render({ output: `image-${obj.imageWidth}x${obj.imageHeight}.png` });
}
}getLinks()
Returns all links on the page. Returns Promise<Link[]>.
interface Link {
bounds?: { left; bottom; right; top };
url?: string; // external URL
pageIndex?: number; // internal link target
actionType?: 'goto' | 'remoteGoto' | 'uri' | 'launch' | 'embeddedGoto' | 'unknown';
destX?: number; // destination X coordinate
destY?: number; // destination Y coordinate
destZoom?: number; // destination zoom level
filePath?: string; // file path for remote goto / launch actions
}search(text, options?)
Searches for text on the page. Returns Promise<SearchMatch[]> with character positions and bounding rectangles.
const matches = await page.search('invoice', {
caseSensitive: true,
wholeWord: false,
consecutive: false,
});
// [{ charIndex: 42, length: 7, matchedText: 'invoice', rects: [{ left, top, right, bottom }] }]getAnnotations()
Returns all annotations on the page. Returns Promise<Annotation[]>.
interface Annotation {
index: number; // 0-based annotation index on the page
type: 'text' | 'link' | 'highlight' | 'underline' | 'strikeout' | /* ... */ 'unknown';
fileName?: string; // embedded file name (fileattachment annotations only)
bounds?: { left; bottom; right; top };
contents: string;
color: { r; g; b; a } | null;
interiorColor?: { r; g; b; a }; // fill color for markup annotations
author: string; // annotation author
subject: string; // annotation subject
creationDate: string; // PDF date string (e.g. "D:20250101120000Z")
modDate: string; // modification date
flags: number; // annotation flags bitmask (PDF spec Table 165)
border?: { horizontalRadius; verticalRadius; width };
quadPoints?: Array<{ x1; y1; x2; y2; x3; y3; x4; y4 }>;
}getAnnotationAttachment(index, options?)
Reads the embedded file of the 'fileattachment' (paperclip) annotation at index — the annotation's index from getAnnotations(). Returns a Promise<Buffer>, or writes to a file and returns Promise<void> when output is given.
This is the page-level counterpart to the document-level getAttachment(index): file-attachment annotations live on a page, not in the /EmbeddedFiles name tree, so ZUGFeRD / Factur-X e-invoice XML (a document-level embedded file) is read with document.getAttachment, not this method.
Rejects if the annotation at index is not a file attachment or carries no embedded file.
const annotations = await page.getAnnotations();
for (const annot of annotations) {
if (annot.type === 'fileattachment') {
const bytes = await page.getAnnotationAttachment(annot.index); // Buffer of exact embedded bytes
// ...or write straight to disk:
await page.getAnnotationAttachment(annot.index, { output: annot.fileName ?? 'attachment.bin' });
}
}getFormFields()
Returns all form fields on the page. Returns Promise<FormField[]>.
interface FormField {
type:
| 'unknown'
| 'pushButton'
| 'checkbox'
| 'radioButton'
| 'comboBox'
| 'listBox'
| 'textField'
| 'signature';
name: string; // field name
value: string; // current value
alternateName?: string; // tooltip / alternate field name
exportValue?: string; // export value (checkboxes / radio buttons)
flags: number; // field flags bitmask
bounds?: { left; bottom; right; top };
isChecked: boolean; // whether checkbox / radio is checked
options?: FormFieldOption[]; // options for combo box / list box
}
interface FormFieldOption {
label: string;
isSelected: boolean;
}const fields = await page.getFormFields();
const textFields = fields.filter((f) => f.type === 'textField');
const checked = fields.filter((f) => f.isChecked);close()
Closes the page and frees resources. Must be called when done with the page.
DocumentMetadata
interface DocumentMetadata {
title: string;
author: string;
subject: string;
keywords: string;
creator: string;
producer: string;
creationDate: string;
modDate: string;
pdfVersion: number; // e.g. 17 for PDF 1.7
permissions: {
print: boolean;
modify: boolean;
copy: boolean;
annotate: boolean;
fillForms: boolean;
extractForAccessibility: boolean;
assemble: boolean;
printHighQuality: boolean;
};
isTagged: boolean; // whether the PDF is a tagged PDF
language: string; // document language (e.g. 'en-US')
signatureCount: number; // number of digital signatures
attachmentCount: number; // number of file attachments
permanentId?: string; // permanent file identifier (hex)
changingId?: string; // changing file identifier (hex)
}🚧 Not supported
Deliberate gaps, so you know where the edges are before you hit them:
- Interactive form filling. Setting field values programmatically is not offered. PDFium's
FPDF_FORMFILLINFOis a UI event-loop API — timers, focus tracking, invalidation callbacks — with no meaningful mapping onto a stateless promise-based library, and the shortcut of writing/Vdirectly yields PDFs whose rendered appearance contradicts their stored value. Reading fields (getFormFields()) and flattening them are supported. - Producing PDF/A-3 e-invoices.
addAttachments()embeds files but cannot set/Subtype,/AFRelationship, the catalog/AFarray, an OutputIntent or XMP — all required by ZUGFeRD, Factur-X and XRechnung. Reading such invoices is fully supported. - Signature verification.
getSignatures()reports what the signature dictionary declares; PDFium performs no cryptography. Pass the blob fromgetSignatureContents()and itsbyteRangeto a crypto library to actually validate. - XMP metadata. Not reachable through PDFium's public API at all.
- Executing JavaScript. The bundled PDFium is built with V8 disabled.
getJavaScriptActions()returns scripts as inert text.
One PDFium reading quirk worth knowing: an embedded file that decodes to zero bytes comes back from getAttachment() as its raw compressed stream, because PDFium treats "decoded to empty" as a decode failure and falls back to the undecoded bytes.
⚙️ Concurrency
concurrency(value?): number
Gets or sets the maximum number of concurrent native operations dispatched to the thread pool.
The default is the number of CPU cores (os.availableParallelism()). A value of 0 resets to the default.
PDFium is single-threaded internally — all operations are serialized through a global mutex. The concurrency limiter prevents excess libuv worker threads from being blocked waiting on that mutex.
import { loadDocument, concurrency } from 'pdfium-native';
concurrency(); // 8 (CPU cores)
concurrency(2); // limit to 2 concurrent operations
concurrency(0); // reset to default🙏 Acknowledgements
This project uses prebuilt PDFium binaries from bblanchon/pdfium-binaries, which provides automated builds of the PDFium library for multiple platforms. Thanks to @bblanchon for maintaining this invaluable resource.
🧹 Memory Management
Always call page.close() and doc.destroy() when done. While GC-triggered destructor hooks exist as a safety net, they should not be relied on — explicit cleanup ensures resources are freed promptly.
const doc = await loadDocument('file.pdf');
try {
const page = await doc.getPage(0);
try {
// use page
} finally {
page.close();
}
} finally {
doc.destroy();
}📄 License
MIT
