@cantoo/pdf-lib

中文文档
2.11.1发布于 2026/09/15MITTypeScriptESM + CJS近 30 天下载 896.1万

Create and modify PDF files with JavaScript

原始使用文档
文档目录42 节
  1. Forked by Cantoo
  2. Benefits vs original pdf-lib
  3. Table of Contents
  4. Features
  5. Motivation
  6. Usage Examples
  7. Create Document
  8. Modify Document
  9. Incremental Document Modification
  10. Consecutive Incremental Updates
  11. Create Form
  12. Fill Form
  13. Flatten Form
  14. Work with XFA Forms
  15. Extract XFA JavaScript
  16. Modify XFA JavaScript
  17. Extract Document JavaScript
  18. Copy Pages
  19. Embed PNG and JPEG Images
  20. Embed PDF Pages
  21. Embed Font and Measure Text
  22. Add Attachments
  23. Extract Attachments
  24. Create PDF/A Documents
  25. Embed Factur-X / ZUGFeRD Invoices
  26. Set Document Metadata
  27. Read Document Metadata
  28. Set Viewer Preferences
  29. Read Viewer Preferences
  30. Draw SVG Paths
  31. Draw SVG
  32. Deno Usage
  33. Creating a Document with Deno
  34. Embedding a Font with Deno
  35. Complete Examples
  36. Installation
  37. NPM Module
  38. Vendored PNG & standard fonts
  39. UMD Module
  40. Fontkit Installation
  41. Fontkit NPM Module
  42. Fontkit UMD Module

Forked by Cantoo

@cantoo/pdf-lib is an actively maintained fork of Hopding/pdf-lib. We keep the original API and docs, and ship the features below that upstream does not (or no longer) provide. Install with:

npm install @cantoo/pdf-lib
# or
yarn add @cantoo/pdf-lib

We maintain this project for our own product needs and cannot guarantee support for every issue outside that roadmap — but contributions that fit the library are welcome.

Benefits vs original pdf-lib

Drawing & content

  • Full SVG drawing (page.drawSvg / embedSvg) — not just drawSvgPath: shapes, text, transforms, viewBox cropping, blend modes, rounded rects, CSS colors via culori
  • PDFPage.extractContents() — pull typed assets (text, images, approximate vector graphics as SVG) out of page content streams
  • characterSpacing on drawText

Documents & save

  • Incremental updates (forIncrementalUpdate, save({ useObjectStreams… }), commit()) — append changes without rewriting the whole file (signing / multi-increment workflows)
  • PDF header preserved on save (writers no longer always force %PDF-1.7)
  • Recover truncated PDFs instead of failing hard on a broken trailer object
  • More robust parsing (hex-string whitespace, object streams on older PDFs, HTML closing tags spanning lines, case-insensitive PDF names, …)

Forms & scripts

  • XFA helpers — preserveXFA, read/modify XFA JavaScript, signature-field discovery (static / government-style XFA)
  • Extract / work with document-level JavaScript
  • PDFForm.flatten() also flattens orphaned widget annotations not listed in AcroForm.Fields
  • Attachments — add, extract (including unsaved), and detach embedded files; name trees kept in PDF lexical order

Standards & e-invoicing

  • convertToPDFA() — PDF/A-1/2/3 structural pieces (OutputIntent + sRGB ICC, /ID, XMP synced with Info)
  • embedFacturX() — Factur-X / ZUGFeRD PDF/A-3 hybrid invoices

Security & crypto

  • PDFDocument.encrypt() — AES-256 revision 6 by default; AES-128 / RC4 via algorithm (RC4 needs allowWeakCryptography)
  • Decrypt PDFs whose crypt filters are indirect objects
  • No crypto-js: in-tree AES/RC4/MD5/SHA-256 + Web Crypto getRandomValues

Fonts & layers

  • First-class @cantoo/fontkit support (types, default export, UMD); still compatible with upstream fontkit / @pdf-lib/fontkit
  • postscriptName to pick a face from .ttc / .dfont collections
  • Optional content (PDF layers) — getOptionalContentGroups() / setOptionalContentGroupVisibility()

Packaging & deps

  • Vendored @cantoo/upng and @cantoo/standard-fonts (also as @cantoo/pdf-lib/upng and @cantoo/pdf-lib/standard-fonts)
  • Compression via fflate instead of pako (modular TS UPNG with Photopea quantize / framize / lossy cnum)
  • Lighter color parsing with culori; html-entities as a peer (pulled in by the HTML/SVG parser)

See CHANGELOG.md for version-by-version detail.


pdf-lib

Create and modify PDF documents in any JavaScript environment.
Designed to work in any modern JavaScript runtime. Tested in Node, Browser, Deno, and React Native environments.


Learn more at pdf-lib.js.org

Table of Contents

Features

  • Create new PDFs
  • Modify existing PDFs
  • Incremental document updates (append-only saves / multi-commit)
  • Encrypt / decrypt PDFs (AES-256 by default)
  • Create forms
  • Fill forms
  • Flatten forms
  • Preserve XFA forms
  • Extract XFA JavaScript
  • Modify XFA JavaScript
  • Extract document-level JavaScript
  • Add Pages
  • Insert Pages
  • Remove Pages
  • Copy pages between PDFs
  • Draw Text (including character spacing)
  • Draw Images
  • Draw PDF Pages
  • Draw Vector Graphics
  • Draw SVG Paths
  • Draw full SVG documents (drawSvg / embedSvg)
  • Extract page contents (text, images, vector graphics)
  • Optional content / PDF layer visibility
  • Measure width and height of text
  • Embed Fonts (supports UTF-8 and UTF-16 character sets; TTC/DFont face selection)
  • Set document metadata
  • Read document metadata
  • Set viewer preferences
  • Read viewer preferences
  • Add attachments
  • Extract / detach attachments
  • Create PDF/A documents (parts 1–3)
  • Embed Factur-X / ZUGFeRD e-invoices (PDF/A-3)

Motivation

pdf-lib was created to address the JavaScript ecosystem's lack of robust support for PDF manipulation (especially for PDF modification).

Two of pdf-lib's distinguishing features are:

  1. Supporting modification (editing) of existing documents.
  2. Working in all JavaScript environments - not just in Node or the Browser.

There are other good open source JavaScript PDF libraries available. However, most of them can only create documents, they cannot modify existing ones. And many of them only work in particular environments.

Usage Examples

Create Document

This example produces this PDF.

Try the JSFiddle demo

import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Embed the Times Roman font
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)

// Add a blank page to the document
const page = pdfDoc.addPage()

// Get the width and height of the page
const { width, height } = page.getSize()

// Draw a string of text toward the top of the page
const fontSize = 30
page.drawText('Creating PDFs in JavaScript is awesome!', {
  x: 50,
  y: height - 4 * fontSize,
  size: fontSize,
  font: timesRomanFont,
  color: rgb(0, 0.53, 0.71),
})

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Modify Document

This example produces this PDF (when this PDF is used for the existingPdfBytes variable).

Try the JSFiddle demo

import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const existingPdfBytes = ...

// Load a PDFDocument from the existing PDF bytes
const pdfDoc = await PDFDocument.load(existingPdfBytes)

// Embed the Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)

// Get the first page of the document
const pages = pdfDoc.getPages()
const firstPage = pages[0]

// Get the width and height of the first page
const { width, height } = firstPage.getSize()

// Draw a string of text diagonally across the first page
firstPage.drawText('This text was added with JavaScript!', {
  x: 5,
  y: height / 2 + 300,
  size: 50,
  font: helveticaFont,
  color: rgb(0.95, 0.1, 0.1),
  rotate: degrees(-45),
})


// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Incremental Document Modification

You can load a PDF for incremental update, generating the original document plus the increment, on save. You can also handle incremental update manually. The incremental modification saving is designed to be used for pdf signing. The signature is added to an existing page, then only the 'incremental' PDF is generated and concatenated to the initial version.

This example produces this PDF (when this PDF is used for the existingPdfBytes variable).

import { PDFDocument, StandardFonts } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const existingPdfBytes = ...

// Load a PDFDocument from the existing PDF bytes
const pdfDoc = await PDFDocument.load(existingPdfBytes)

// Take a snapshot of the document
const snapshot = pdfDoc.takeSnapshot();

// Get the first page of the document
const pages = pdfDoc.getPages()
const firstPage = pages[0]

// Mark the page as modified
snapshot.markRefForSave(firstPage.ref)

// Draw a string of text diagonally across the first page
firstPage.drawText('Incremental saving is also awesome!', {
  x: 50,
  y: 4 * fontSize,
  size: fontSize
})

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfIncrementalBytes = await pdfDoc.saveIncremental(snapshot)
const pdfBytes = Buffer.concatenate([ existingPdfBytes, pdfIncrementalBytes ])

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Loading an existing PDF forIncrementalUpdate, makes things easier:

import { PDFDocument, StandardFonts } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
const existingPdfBytes = ...

// Load a PDFDocument from the existing PDF bytes, for incremental update
const pdfDoc = await PDFDocument.load(existingPdfBytes,{forIncrementalUpdate:true})

// Get the first page of the document
const pages = pdfDoc.getPages()
const firstPage = pages[0]

// Draw a string of text diagonally across the first page
firstPage.drawText('Incremental saving is also awesome!', {
  x: 50,
  y: 4 * fontSize,
  size: fontSize
})

// Serialize the PDFDocument to bytes (a Uint8Array), using incremental updates
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

You can force a rewrite of a PDF that was open for incremental update with the right parameter on save:

import { PDFDocument } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
const existingPdfBytes = ...

// Load a PDFDocument from the existing PDF bytes, for incremental update
const pdfDoc = await PDFDocument.load(existingPdfBytes,{forIncrementalUpdate:true})

// Do something..

// Serialize the PDFDocument to bytes (a Uint8Array), NOT using incremental updates
const pdfBytes = await pdfDoc.save({rewrite: true})

Using pdf-lib to generate a placeholder for an electronic signature

@signpdf includes a pdf-lib-placeholder component, but it is based on the pdf-lib package that has no incremental update functionality. This code is taken from that library and modified to use incremental update for not invalidating previous file signatures. Is an example, that can be seen in integration test #20, some @signpdf constants has been changed to arbitrary values for the example to work "out of the box".

import { PDFDocument, StandardFonts, rgb, PDFArray, PDFNumber, PDFName, PDFHexString, PDFString, PDFInvalidObject } from 'pdf-lib';
const pdfBytes = ...

const pdfDoc = await PDFDocument.load(pdfBytes, {forIncrementalUpdate: true});
// visual representation of the signature
const page = pdfDoc.addPage([500, 200]);
const font = pdfDoc.embedStandardFont(StandardFonts.Helvetica);
page.drawRectangle({
  x: 10,
  y: 30,
  width: 280,
  height: 150,
  borderWidth: 2,
  borderColor: rgb(0.45, 0.45, 0.45),
});
page.drawText(`Electronic Signature Example\nSigned on ${(new Date()).toIsoString()}\nThis is not the real signature!!`, {
  x: 20,
  y: 200,
  size: 14,
  font,
});
// Add an AcroForm or update the existing one
let acroForm = pdfDoc.catalog.getOrCreateAcroForm();
// Create a placeholder where the the last 3 parameters of the
// actual range will be replaced when signing is done.
const byteRange = PDFArray.withContext(pdfDoc.context);
byteRange.push(PDFNumber.of(0));
byteRange.push(PDFName.of('*********'));
byteRange.push(PDFName.of('*********'));
byteRange.push(PDFName.of('*********'));
// Fill the contents of the placeholder with 00s.
const placeholder = PDFHexString.of(String.fromCharCode(0).repeat(8096));
// Create a signature dictionary to be referenced in the signature widget.
const appBuild = { App: { Name: 'Signature Example pdf-lib' } };
const signatureDict = pdfDoc.context.obj({
    Type: 'Sig',
    Filter: 'Adobe.PPKLite',
    SubFilter: 'adbe.pkcs7.detached',
    ByteRange: byteRange,
    Contents: placeholder,
    Reason: PDFString.of('Example Signature'),
    M: PDFString.fromDate(new Date()),
    ContactInfo: PDFString.of('[email protected]'),
    Name: PDFString.of('Example Signer'),
    Location: PDFString.of('In a far away galaxy..'),
    Prop_Build: {
      Filter: { Name: 'Adobe.PPKLite' },
      ...appBuild,
    },
});
// Register signatureDict as a PDFInvalidObject to prevent PDFLib from serializing it
// in an object stream.
const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes());
signatureDict.copyBytesInto(signatureBuffer, 0);
const signatureObj = PDFInvalidObject.of(signatureBuffer);
const signatureDictRef = pdfDoc.context.register(signatureObj);
// Create the signature widget
const widgetRect = [0, 0, 0, 0];
const rect = PDFArray.withContext(pdfDoc.context);
widgetRect.forEach((c) => rect.push(PDFNumber.of(c)));
const apStream = pdfDoc.context.formXObject([], {
  BBox: widgetRect,
  Resources: {}, // Necessary to avoid Acrobat bug (see https://stackoverflow.com/a/73011571)
});
const widgetDict = pdfDoc.context.obj({
  Type: 'Annot',
  Subtype: 'Widget',
  FT: 'Sig',
  Rect: rect,
  V: signatureDictRef,
  T: PDFString.of('TestSig'),
  TU: PDFString.of('Electronic Signature Example'),
  F: 2,
  P: page.ref,
  AP: { N: pdfDoc.context.register(apStream) }, // Required for PDF/A compliance
});
const widgetDictRef = pdfDoc.context.register(widgetDict);
// Annotate the widget on the given page
let annotations = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
if (typeof annotations === 'undefined') {
  annotations = pdfDoc.context.obj([]);
}
annotations.push(widgetDictRef);
page.node.set(PDFName.of('Annots'), annotations);
let sigFlags: PDFNumber;
if (acroForm.dict.has(PDFName.of('SigFlags'))) {
  // Already has some flags, will merge
  sigFlags = acroForm.dict.get(PDFName.of('SigFlags')) as PDFNumber;
} else {
  // Create blank flags
  sigFlags = PDFNumber.of(0);
}
const updatedFlags = PDFNumber.of(sigFlags!.asNumber() | 1 | 2);
acroForm.dict.set(PDFName.of('SigFlags'), updatedFlags);
let fields = acroForm.dict.get(PDFName.of('Fields'));
if (!(fields instanceof PDFArray)) {
  fields = pdfDoc.context.obj([]);
  acroForm.dict.set(PDFName.of('Fields'), fields);
}
(fields as PDFArray).push(widgetDictRef);
// Serialize the PDFDocument to bytes (a Uint8Array), using incremental updates
const pdfBytes = await pdfDoc.save()

// `pdfBytes` should be handled to the signing library to calculate the
//  file hash and fill in the generated placeholder for the signature

Consecutive Incremental Updates

You can load a PDF for incremental update, and then generate multiple increments, over the original document, with commit() method.
This method simplifies the sequence:

import { PDFDocument, StandardFonts } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
const existingPdfBytes = ...
// Load a PDFDocument from the existing PDF bytes, for incremental update
const pdfDoc = await PDFDocument.load(existingPdfBytes,{forIncrementalUpdate:true})
// modify pdf
...
// Serialize the PDFDocument to bytes (a Uint8Array), using incremental updates
const firstUpdatedDoc = await pdfDoc.save()
const pdfDoc2 = await PDFDocument.load(firstUpdatedDoc,{forIncrementalUpdate:true})
// modify pdf
...
// Serialize the PDFDocument to bytes (a Uint8Array), using incremental updates
const secondUpdateDoc = await pdfDoc2.save()
// etc, etc

Allowing this:

import { PDFDocument, StandardFonts } from 'pdf-lib';

// This should be a Uint8Array or ArrayBuffer
const existingPdfBytes = ...
// Load a PDFDocument from the existing PDF bytes, for incremental update
const pdfDoc = await PDFDocument.load(existingPdfBytes,{forIncrementalUpdate:true})
// modify pdf
...
// Serialize the PDFDocument to bytes (a Uint8Array), using incremental updates
const firstUpdatedDoc = await pdfDoc.commit();
// modify pdf
...
const secondUpdateDoc = await pdfDoc.commit();
// etc, etc

The commit method has the same parameters than save method. If document is not loaded forIncrementalUpdate, an exception is raised. After calling commit an update section is added to the original document, and this replaces the original document, and a new snapshot is taken.

Create Form

This example produces this PDF.

Try the JSFiddle demo

See also Creating and Filling Forms

import { PDFDocument } from 'pdf-lib'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Add a blank page to the document
const page = pdfDoc.addPage([550, 750])

// Get the form so we can add fields to it
const form = pdfDoc.getForm()

// Add the superhero text field and description
page.drawText('Enter your favorite superhero:', { x: 50, y: 700, size: 20 })

const superheroField = form.createTextField('favorite.superhero')
superheroField.setText('One Punch Man')
superheroField.addToPage(page, { x: 55, y: 640 })

// Add the rocket radio group, labels, and description
page.drawText('Select your favorite rocket:', { x: 50, y: 600, size: 20 })

page.drawText('Falcon Heavy', { x: 120, y: 560, size: 18 })
page.drawText('Saturn IV', { x: 120, y: 500, size: 18 })
page.drawText('Delta IV Heavy', { x: 340, y: 560, size: 18 })
page.drawText('Space Launch System', { x: 340, y: 500, size: 18 })

const rocketField = form.createRadioGroup('favorite.rocket')
rocketField.addOptionToPage('Falcon Heavy', page, { x: 55, y: 540 })
rocketField.addOptionToPage('Saturn IV', page, { x: 55, y: 480 })
rocketField.addOptionToPage('Delta IV Heavy', page, { x: 275, y: 540 })
rocketField.addOptionToPage('Space Launch System', page, { x: 275, y: 480 })
rocketField.select('Saturn IV')

// Add the gundam check boxes, labels, and description
page.drawText('Select your favorite gundams:', { x: 50, y: 440, size: 20 })

page.drawText('Exia', { x: 120, y: 400, size: 18 })
page.drawText('Kyrios', { x: 120, y: 340, size: 18 })
page.drawText('Virtue', { x: 340, y: 400, size: 18 })
page.drawText('Dynames', { x: 340, y: 340, size: 18 })

const exiaField = form.createCheckBox('gundam.exia')
const kyriosField = form.createCheckBox('gundam.kyrios')
const virtueField = form.createCheckBox('gundam.virtue')
const dynamesField = form.createCheckBox('gundam.dynames')

exiaField.addToPage(page, { x: 55, y: 380 })
kyriosField.addToPage(page, { x: 55, y: 320 })
virtueField.addToPage(page, { x: 275, y: 380 })
dynamesField.addToPage(page, { x: 275, y: 320 })

exiaField.check()
dynamesField.check()

// Add the planet dropdown and description
page.drawText('Select your favorite planet*:', { x: 50, y: 280, size: 20 })

const planetsField = form.createDropdown('favorite.planet')
planetsField.addOptions(['Venus', 'Earth', 'Mars', 'Pluto'])
planetsField.select('Pluto')
planetsField.addToPage(page, { x: 55, y: 220 })

// Add the person option list and description
page.drawText('Select your favorite person:', { x: 50, y: 180, size: 18 })

const personField = form.createOptionList('favorite.person')
personField.addOptions([
  'Julius Caesar',
  'Ada Lovelace',
  'Cleopatra',
  'Aaron Burr',
  'Mark Antony',
])
personField.select('Ada Lovelace')
personField.addToPage(page, { x: 55, y: 70 })

// Just saying...
page.drawText(`* Pluto should be a planet too!`, { x: 15, y: 15, size: 15 })

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Fill Form

This example produces this PDF (when this PDF is used for the formPdfBytes variable, this image is used for the marioImageBytes variable, and this image is used for the emblemImageBytes variable).

Try the JSFiddle demo

See also Creating and Filling Forms

import { PDFDocument } from 'pdf-lib'

// These should be Uint8Arrays or ArrayBuffers
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const formPdfBytes = ...
const marioImageBytes = ...
const emblemImageBytes = ...

// Load a PDF with form fields
const pdfDoc = await PDFDocument.load(formPdfBytes)

// Embed the Mario and emblem images
const marioImage = await pdfDoc.embedPng(marioImageBytes)
const emblemImage = await pdfDoc.embedPng(emblemImageBytes)

// Get the form containing all the fields
const form = pdfDoc.getForm()

// Get all fields in the PDF by their names
const nameField = form.getTextField('CharacterName 2')
const ageField = form.getTextField('Age')
const heightField = form.getTextField('Height')
const weightField = form.getTextField('Weight')
const eyesField = form.getTextField('Eyes')
const skinField = form.getTextField('Skin')
const hairField = form.getTextField('Hair')

const alliesField = form.getTextField('Allies')
const factionField = form.getTextField('FactionName')
const backstoryField = form.getTextField('Backstory')
const traitsField = form.getTextField('Feat+Traits')
const treasureField = form.getTextField('Treasure')

const characterImageField = form.getButton('CHARACTER IMAGE')
const factionImageField = form.getTextField('Faction Symbol Image')

// Fill in the basic info fields
nameField.setText('Mario')
ageField.setText('24 years')
heightField.setText(`5' 1"`)
weightField.setText('196 lbs')
eyesField.setText('blue')
skinField.setText('white')
hairField.setText('brown')

// Fill the character image field with our Mario image
characterImageField.setImage(marioImage)

// Fill in the allies field
alliesField.setText(
  [
    `Allies:`,
    `  • Princess Daisy`,
    `  • Princess Peach`,
    `  • Rosalina`,
    `  • Geno`,
    `  • Luigi`,
    `  • Donkey Kong`,
    `  • Yoshi`,
    `  • Diddy Kong`,
    ``,
    `Organizations:`,
    `  • Italian Plumbers Association`,
  ].join('\n'),
)

// Fill in the faction name field
factionField.setText(`Mario's Emblem`)

// Fill the faction image field with our emblem image
factionImageField.setImage(emblemImage)

// Fill in the backstory field
backstoryField.setText(
  `Mario is a fictional character in the Mario video game franchise, owned by Nintendo and created by Japanese video game designer Shigeru Miyamoto. Serving as the company's mascot and the eponymous protagonist of the series, Mario has appeared in over 200 video games since his creation. Depicted as a short, pudgy, Italian plumber who resides in the Mushroom Kingdom, his adventures generally center upon rescuing Princess Peach from the Koopa villain Bowser. His younger brother and sidekick is Luigi.`,
)

// Fill in the traits field
traitsField.setText(
  [
    `Mario can use three basic three power-ups:`,
    `  • the Super Mushroom, which causes Mario to grow larger`,
    `  • the Fire Flower, which allows Mario to throw fireballs`,
    `  • the Starman, which gives Mario temporary invincibility`,
  ].join('\n'),
)

// Fill in the treasure field
treasureField.setText(['• Gold coins', '• Treasure chests'].join('\n'))

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Flatten Form

This example produces this PDF (when this PDF is used for the formPdfBytes variable).

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const formPdfBytes = ...

// Load a PDF with form fields
const pdfDoc = await PDFDocument.load(formPdfBytes)

// Get the form containing all the fields
const form = pdfDoc.getForm()

// Fill the form's fields
form.getTextField('Text1').setText('Some Text');

form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');

form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();

form.getDropdown('Dropdown7').select('Infinity');

form.getOptionList('List Box6').select('Honda');

// Flatten the form's fields
form.flatten();

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Work with XFA Forms

XFA (XML Forms Architecture) forms are complex, dynamic PDF forms commonly used for government forms, tax documents, and enterprise applications. Unlike standard AcroForms, XFA forms embed their structure and JavaScript in XML format.

Important: To preserve XFA forms when loading and saving PDFs, use the preserveXFA option:

import { PDFDocument } from 'pdf-lib'

const xfaPdfBytes = ... // Load your XFA PDF

// Load with XFA preservation
const pdfDoc = await PDFDocument.load(xfaPdfBytes, { 
  preserveXFA: true 
})

// Make modifications...

// Save the document
const pdfBytes = await pdfDoc.save()

Note: The preserveXFA option must be set to true when loading to preserve XFA data. XFA preservation during save happens automatically if it was preserved during load.

Scope and limitations (v1): XFA support in pdf-lib is intentionally narrow and targets the common government/tax "static" XFA layout:

  • Only the array form of /XFA (alternating name/stream pairs) is supported; the single-stream packaging is not read or written.
  • Only the template packet is inspected — dynamic packaging and other packets (e.g. datasets, config, xdp wrappers) are not (re)generated. Editing JavaScript does not re-render or repackage the form.
  • XFA signature detection reads <signature>/<manifest> entries from the template; it does not validate or create cryptographic signatures.
  • getForm() removes XFA data unless the document was loaded with preserveXFA: true, so call the XFA helpers before getForm() (or load with preserveXFA: true).
  • Template XML is decoded as UTF-8 with a latin1 fallback; exotic encodings may not round-trip cleanly.

Extract XFA JavaScript

XFA forms often contain JavaScript for validation, calculations, and data import/export. You can extract all JavaScript from an XFA form:

import { PDFDocument } from 'pdf-lib'

const xfaPdfBytes = ... // Load your XFA PDF

// Load the PDF with XFA preservation
const pdfDoc = await PDFDocument.load(xfaPdfBytes, { 
  preserveXFA: true 
})

// Extract all XFA JavaScript
const scripts = pdfDoc.getXFAJavaScripts()

// Each script contains:
// - field: The field name (e.g., 'Button1', 'TextField2')
// - event: The event name (e.g., 'event__click', 'event__change')
// - script: The JavaScript code

console.log(`Found ${scripts.length} scripts`)

scripts.forEach((script) => {
  console.log(`Field: ${script.field}`)
  console.log(`Event: ${script.event}`)
  console.log(`Code: ${script.script}`)
})

// Find specific scripts
const clickHandlers = scripts.filter(s => 
  s.event.includes('click')
)

const validationScripts = scripts.filter(s => 
  s.script.includes('validate') || s.script.includes('Validate')
)

Modify XFA JavaScript

You can modify JavaScript in XFA forms to customize behavior, add logging, or fix issues:

import { PDFDocument } from 'pdf-lib'

const xfaPdfBytes = ... // Load your XFA PDF

// Load the PDF with XFA preservation
const pdfDoc = await PDFDocument.load(xfaPdfBytes, { 
  preserveXFA: true 
})

// Extract scripts to find what you want to modify
const scripts = pdfDoc.getXFAJavaScripts()
const importButton = scripts.find(s => s.field === 'ImportButton')

if (importButton) {
  // Modify the import button's click handler
  const newScript = `
    // Custom import handler
    try {
      console.println("Starting import...");
      ${importButton.script}
      console.println("Import completed!");
    } catch(e) {
      xfa.host.messageBox("Error: " + e.message);
    }
  `
  
  try {
    pdfDoc.setXFAJavaScript(
      'ImportButton',         // field name
      importButton.event,     // event name (e.g., 'event__click')
      newScript               // new JavaScript code
    )
    console.log('Successfully modified XFA JavaScript')
  } catch (error) {
    console.error('Failed to modify script:', error.message)
  }
}

// Save the document
const pdfBytes = await pdfDoc.save()

// The modified PDF will have the updated JavaScript

Use Cases:

  • Add error handling to existing scripts
  • Modify validation rules
  • Add logging for debugging
  • Customize import/export behavior
  • Fix compatibility issues

Extract Document JavaScript

PDF documents can contain document-level JavaScript that executes when the document is opened. You can extract these scripts:

import { PDFDocument } from 'pdf-lib'

const pdfBytes = ... // Load your PDF

const pdfDoc = await PDFDocument.load(pdfBytes)

// Extract all document-level JavaScript
const scripts = pdfDoc.getDocumentJavaScripts()

// Each script contains:
// - name: The script name
// - script: The JavaScript code

console.log(`Found ${scripts.length} document scripts`)

scripts.forEach((script) => {
  console.log(`Script name: ${script.name}`)
  console.log(`Code: ${script.script}`)
})

// Find specific scripts
const initScripts = scripts.filter(s => 
  s.name.toLowerCase().includes('init')
)

Note: Document-level JavaScript is different from XFA JavaScript. Document-level scripts are stored in the document's Names dictionary and execute when the PDF is opened. XFA JavaScript is embedded in XFA form templates.

Copy Pages

This example produces this PDF (when this PDF is used for the firstDonorPdfBytes variable and this PDF is used for the secondDonorPdfBytes variable).

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// These should be Uint8Arrays or ArrayBuffers
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const firstDonorPdfBytes = ...
const secondDonorPdfBytes = ...

// Load a PDFDocument from each of the existing PDFs
const firstDonorPdfDoc = await PDFDocument.load(firstDonorPdfBytes)
const secondDonorPdfDoc = await PDFDocument.load(secondDonorPdfBytes)

// Copy the 1st page from the first donor document, and
// the 743rd page from the second donor document
const [firstDonorPage] = await pdfDoc.copyPages(firstDonorPdfDoc, [0])
const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742])

// Add the first copied page
pdfDoc.addPage(firstDonorPage)

// Insert the second copied page to index 0, so it will be the
// first page in `pdfDoc`
pdfDoc.insertPage(0, secondDonorPage)

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Embed PNG and JPEG Images

This example produces this PDF (when this image is used for the jpgImageBytes variable and this image is used for the pngImageBytes variable).

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// These should be Uint8Arrays or ArrayBuffers
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const jpgImageBytes = ...
const pngImageBytes = ...

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Embed the JPG image bytes and PNG image bytes
const jpgImage = await pdfDoc.embedJpg(jpgImageBytes)
const pngImage = await pdfDoc.embedPng(pngImageBytes)

// Get the width/height of the JPG image scaled down to 25% of its original size
const jpgDims = jpgImage.scale(0.25)

// Get the width/height of the PNG image scaled down to 50% of its original size
const pngDims = pngImage.scale(0.5)

// Add a blank page to the document
const page = pdfDoc.addPage()

// Draw the JPG image in the center of the page
page.drawImage(jpgImage, {
  x: page.getWidth() / 2 - jpgDims.width / 2,
  y: page.getHeight() / 2 - jpgDims.height / 2,
  width: jpgDims.width,
  height: jpgDims.height,
})

// Draw the PNG image near the lower right corner of the JPG image
page.drawImage(pngImage, {
  x: page.getWidth() / 2 - pngDims.width / 2 + 75,
  y: page.getHeight() / 2 - pngDims.height,
  width: pngDims.width,
  height: pngDims.height,
})

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Embed PDF Pages

This example produces this PDF (when this PDF is used for the americanFlagPdfBytes variable and this PDF is used for the usConstitutionPdfBytes variable).

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// These should be Uint8Arrays or ArrayBuffers
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const americanFlagPdfBytes = ...
const usConstitutionPdfBytes = ...

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Embed the American flag PDF bytes
const [americanFlag] = await pdfDoc.embedPdf(americanFlagPdfBytes)

// Load the U.S. constitution PDF bytes
const usConstitutionPdf = await PDFDocument.load(usConstitutionPdfBytes)

// Embed the second page of the constitution and clip the preamble
const preamble = await pdfDoc.embedPage(usConstitutionPdf.getPages()[1], {
  left: 55,
  bottom: 485,
  right: 300,
  top: 575,
})

// Get the width/height of the American flag PDF scaled down to 30% of
// its original size
const americanFlagDims = americanFlag.scale(0.3)

// Get the width/height of the preamble clipping scaled up to 225% of
// its original size
const preambleDims = preamble.scale(2.25)

// Add a blank page to the document
const page = pdfDoc.addPage()

// Draw the American flag image in the center top of the page
page.drawPage(americanFlag, {
  ...americanFlagDims,
  x: page.getWidth() / 2 - americanFlagDims.width / 2,
  y: page.getHeight() - americanFlagDims.height - 150,
})

// Draw the preamble clipping in the center bottom of the page
page.drawPage(preamble, {
  ...preambleDims,
  x: page.getWidth() / 2 - preambleDims.width / 2,
  y: page.getHeight() / 2 - preambleDims.height / 2 - 50,
})

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Embed Font and Measure Text

pdf-lib relies on @cantoo/fontkit to support embedding custom fonts. You must add the @cantoo/fontkit module to your project and register it using pdfDoc.registerFontkit(...) before embedding custom fonts. Upstream fontkit and the older @pdf-lib/fontkit package still work if you already depend on them.

See below for detailed installation instructions on installing @cantoo/fontkit as an NPM or UMD module.

This example produces this PDF (when this font is used for the fontBytes variable).

Try the JSFiddle demo

import { PDFDocument, rgb } from 'pdf-lib'
import fontkit from '@cantoo/fontkit'

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If you're running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const fontBytes = ...

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Register the `fontkit` instance
pdfDoc.registerFontkit(fontkit)

// Embed our custom font in the document
const customFont = await pdfDoc.embedFont(fontBytes)

// Add a blank page to the document
const page = pdfDoc.addPage()

// Create a string of text and measure its width and height in our custom font
const text = 'This is text in an embedded font!'
const textSize = 35
const textWidth = customFont.widthOfTextAtSize(text, textSize)
const textHeight = customFont.heightAtSize(textSize)

// Draw the string of text on the page
page.drawText(text, {
  x: 40,
  y: 450,
  size: textSize,
  font: customFont,
  color: rgb(0, 0.53, 0.71),
})

// Draw a box around the string of text
page.drawRectangle({
  x: 40,
  y: 450,
  width: textWidth,
  height: textHeight,
  borderColor: rgb(1, 0, 0),
  borderWidth: 1.5,
})

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Add Attachments

This example produces this PDF (when this image is used for the jpgAttachmentBytes variable and this PDF is used for the pdfAttachmentBytes variable).

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// These should be Uint8Arrays or ArrayBuffers
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const jpgAttachmentBytes = ...
const pdfAttachmentBytes = ...

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Add the JPG attachment
await pdfDoc.attach(jpgAttachmentBytes, 'cat_riding_unicorn.jpg', {
  mimeType: 'image/jpeg',
  description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
  creationDate: new Date('2019/12/01'),
  modificationDate: new Date('2020/04/19'),
})

// Add the PDF attachment
await pdfDoc.attach(pdfAttachmentBytes, 'us_constitution.pdf', {
  mimeType: 'application/pdf',
  description: 'Constitution of the United States 🇺🇸🦅',
  creationDate: new Date('1787/09/17'),
  modificationDate: new Date('1992/05/07'),
})

// Add a page with some text
const page = pdfDoc.addPage();
page.drawText('This PDF has two attachments', { x: 135, y: 415 })

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Extract Attachments

If you load a PDF that has cars.csv as an attachment, you can use the following to extract the attachments:

const pdfDoc = await PDFDocument.load(...)
const attachments = pdfDoc.getAttachments()
const csv = attachments.find(({ name }) => name === 'cars.csv')
fs.writeFileSync(csv.name, csv.data)

NOTE: The method also finds attachments added after the last call to save().

Create PDF/A Documents

Convert a document to PDF/A by calling convertToPDFA(). This adds the structural pieces PDF/A requires (document /ID, OutputIntent with an embedded sRGB ICC profile, uncompressed XMP metadata with pdfaid, and the appropriate PDF header version). It does not rewrite arbitrary content to make it compliant — in particular, drawn text must use an embedded font (the 14 standard fonts are not PDF/A compliant). Validate the result with a tool such as veraPDF.

import { PDFDocument } from '@cantoo/pdf-lib'
import fontkit from '@cantoo/fontkit'

const fontBytes = ... // e.g. fs.readFileSync('Roboto-Regular.ttf')

const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)

const font = await pdfDoc.embedFont(fontBytes)
const page = pdfDoc.addPage()
page.drawText('Hello PDF/A', { x: 50, y: 700, size: 24, font })

pdfDoc.setTitle('Archival document')
pdfDoc.setAuthor('ACME GmbH')

// '1B' | '2B' | '2U' | '3B' | '3U' — defaults to '3B'
pdfDoc.convertToPDFA({ conformance: '3B' })

const pdfBytes = await pdfDoc.save()

After conversion, pdf-lib keeps the Info dictionary and XMP metadata in sync on save(), while preserving any strictly foreign XMP rdf:Description blocks (for example Factur-X schemas passed via extensions). Descriptions that mix owned namespaces (dc / xmp / pdf / pdfaid) with custom ones are not preserved — re-supply them via extensions or embedFacturX(). Loading an existing PDF/A file does not enable that sync by itself: call convertToPDFA() (or embedFacturX()) again if you change Info fields and need them mirrored.

Embed Factur-X / ZUGFeRD Invoices

embedFacturX() wraps a human-readable PDF and a machine-readable Factur-X / ZUGFeRD XML into a PDF/A-3 hybrid invoice: it ensures PDF/A-3 (converting to 3B if needed, or keeping an existing 3U/3B level), writes the required fx: XMP properties (plus the PDF/A extension schema), and attaches the XML with an associated-file relationship.

It does not generate or validate the Cross Industry Invoice XML — pass a complete factur-x.xml from your invoicing stack.

import { PDFDocument, embedFacturX } from '@cantoo/pdf-lib'
import fontkit from '@cantoo/fontkit'

const fontBytes = ...
const invoiceXmlBytes = ... // Factur-X / ZUGFeRD XML (Uint8Array)

const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)

const font = await pdfDoc.embedFont(fontBytes)
const page = pdfDoc.addPage()
page.drawText('Invoice 2026-0001', { x: 50, y: 700, size: 18, font })
// ... draw the rest of the human-readable invoice with the embedded font ...

pdfDoc.setTitle('Invoice 2026-0001')
pdfDoc.setAuthor('ACME GmbH')

await embedFacturX(pdfDoc, invoiceXmlBytes, {
  // MINIMUM | BASIC WL | BASIC | EN 16931 | EXTENDED | XRECHNUNG
  conformanceLevel: 'EN 16931',
})

const pdfBytes = await pdfDoc.save()

Set Document Metadata

This example produces this PDF.

Try the JSFiddle demo

import { PDFDocument, StandardFonts } from 'pdf-lib'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Embed the Times Roman font
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)

// Add a page and draw some text on it
const page = pdfDoc.addPage([500, 600])
page.setFont(timesRomanFont)
page.drawText('The Life of an Egg', { x: 60, y: 500, size: 50 })
page.drawText('An Epic Tale of Woe', { x: 125, y: 460, size: 25 })

// Set all available metadata fields on the PDFDocument. Note that these fields
// are visible in the "Document Properties" section of most PDF readers.
pdfDoc.setTitle('🥚 The Life of an Egg 🍳')
pdfDoc.setAuthor('Humpty Dumpty')
pdfDoc.setSubject('📘 An Epic Tale of Woe 📖')
pdfDoc.setKeywords(['eggs', 'wall', 'fall', 'king', 'horses', 'men'])
pdfDoc.setProducer('PDF App 9000 🤖')
pdfDoc.setCreator('pdf-lib (https://github.com/Hopding/pdf-lib)')
pdfDoc.setCreationDate(new Date('2018-06-24T01:58:37.228Z'))
pdfDoc.setModificationDate(new Date('2019-12-21T07:00:11.000Z'))

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Read Document Metadata

Try the JSFiddle demo

import { PDFDocument } from 'pdf-lib'

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const existingPdfBytes = ...

// Load a PDFDocument without updating its existing metadata
const pdfDoc = await PDFDocument.load(existingPdfBytes, {
  updateMetadata: false
})

// Print all available metadata fields
console.log('Title:', pdfDoc.getTitle())
console.log('Author:', pdfDoc.getAuthor())
console.log('Subject:', pdfDoc.getSubject())
console.log('Creator:', pdfDoc.getCreator())
console.log('Keywords:', pdfDoc.getKeywords())
console.log('Producer:', pdfDoc.getProducer())
console.log('Creation Date:', pdfDoc.getCreationDate())
console.log('Modification Date:', pdfDoc.getModificationDate())

This script outputs the following (when this PDF is used for the existingPdfBytes variable):

Title: Microsoft Word - Basic Curriculum Vitae example.doc
Author: Administrator
Subject: undefined
Creator: PScript5.dll Version 5.2
Keywords: undefined
Producer: Acrobat Distiller 8.1.0 (Windows)
Creation Date: 2010-07-29T14:26:00.000Z
Modification Date: 2010-07-29T14:26:00.000Z

Set Viewer Preferences

import {
  PDFDocument,
  StandardFonts,
  NonFullScreenPageMode,
  ReadingDirection,
  PrintScaling,
  Duplex,
  PDFName,
} from 'pdf-lib'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Embed the Times Roman font
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)

// Add a page and draw some text on it
const page = pdfDoc.addPage([500, 600])
page.setFont(timesRomanFont)
page.drawText('The Life of an Egg', { x: 60, y: 500, size: 50 })
page.drawText('An Epic Tale of Woe', { x: 125, y: 460, size: 25 })

// Set all available viewer preferences on the PDFDocument:
const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
viewerPrefs.setHideToolbar(true)
viewerPrefs.setHideMenubar(true)
viewerPrefs.setHideWindowUI(true)
viewerPrefs.setFitWindow(true)
viewerPrefs.setCenterWindow(true)
viewerPrefs.setDisplayDocTitle(true)

// Set the PageMode (otherwise setting NonFullScreenPageMode has no meaning)
pdfDoc.catalog.set(PDFName.of('PageMode'), PDFName.of('FullScreen'))

// Set what happens when fullScreen is closed
viewerPrefs.setNonFullScreenPageMode(NonFullScreenPageMode.UseOutlines)

viewerPrefs.setReadingDirection(ReadingDirection.L2R)
viewerPrefs.setPrintScaling(PrintScaling.None)
viewerPrefs.setDuplex(Duplex.DuplexFlipLongEdge)
viewerPrefs.setPickTrayByPDFSize(true)

// We can set the default print range to only the first page
viewerPrefs.setPrintPageRange({ start: 0, end: 0 })

// Or we can supply noncontiguous ranges (e.g. pages 1, 3, and 5-7)
viewerPrefs.setPrintPageRange([
  { start: 0, end: 0 },
  { start: 2, end: 2 },
  { start: 4, end: 6 },
])

viewerPrefs.setNumCopies(2)

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Read Viewer Preferences

import { PDFDocument } from 'pdf-lib'

// This should be a Uint8Array or ArrayBuffer
// This data can be obtained in a number of different ways
// If your running in a Node environment, you could use fs.readFile()
// In the browser, you could make a fetch() call and use res.arrayBuffer()
const existingPdfBytes = ...

// Load a PDFDocument without updating its existing metadata
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()

// Print all available viewer preference fields
console.log('HideToolbar:', viewerPrefs.getHideToolbar())
console.log('HideMenubar:', viewerPrefs.getHideMenubar())
console.log('HideWindowUI:', viewerPrefs.getHideWindowUI())
console.log('FitWindow:', viewerPrefs.getFitWindow())
console.log('CenterWindow:', viewerPrefs.getCenterWindow())
console.log('DisplayDocTitle:', viewerPrefs.getDisplayDocTitle())
console.log('NonFullScreenPageMode:', viewerPrefs.getNonFullScreenPageMode())
console.log('ReadingDirection:', viewerPrefs.getReadingDirection())
console.log('PrintScaling:', viewerPrefs.getPrintScaling())
console.log('Duplex:', viewerPrefs.getDuplex())
console.log('PickTrayByPDFSize:', viewerPrefs.getPickTrayByPDFSize())
console.log('PrintPageRange:', viewerPrefs.getPrintPageRange())
console.log('NumCopies:', viewerPrefs.getNumCopies())

This script outputs the following (when this PDF is used for the existingPdfBytes variable):

HideToolbar: true
HideMenubar: true
HideWindowUI: false
FitWindow: true
CenterWindow: true
DisplayDocTitle: true
NonFullScreenPageMode: UseNone
ReadingDirection: R2L
PrintScaling: None
Duplex: DuplexFlipLongEdge
PickTrayByPDFSize: true
PrintPageRange: [ { start: 1, end: 1 }, { start: 3, end: 4 } ]
NumCopies: 2

Draw SVG Paths

This example produces this PDF.

Try the JSFiddle demo

import { PDFDocument, rgb } from 'pdf-lib'

// SVG path for a wavy line
const svgPath =
  'M 0,20 L 100,160 Q 130,200 150,120 C 190,-40 200,200 300,150 L 400,90'

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create()

// Add a blank page to the document
const page = pdfDoc.addPage()
page.moveTo(100, page.getHeight() - 5)

// Draw the SVG path as a black line
page.moveDown(25)
page.drawSvgPath(svgPath)

// Draw the SVG path as a thick green line
page.moveDown(200)
page.drawSvgPath(svgPath, { borderColor: rgb(0, 1, 0), borderWidth: 5 })

// Draw the SVG path and fill it with red
page.moveDown(200)
page.drawSvgPath(svgPath, { color: rgb(1, 0, 0) })

// Draw the SVG path at 50% of its original size
page.moveDown(200)
page.drawSvgPath(svgPath, { scale: 0.5 })

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save()

// For example, `pdfBytes` can be:
//   • Written to a file in Node
//   • Downloaded from the browser
//   • Rendered in an <iframe>

Draw SVG

import { PDFDocument, rgb } from 'pdf-lib';

// SVG of a square inside a square
const svg = `<svg width="100" height="100">
  <rect y="0" x="0" width="100" height="100" fill="none" stroke="black"/>
  <rect y="25" x="25" width="50" height="50" fill="black"/>
</svg>`;
const svg2 =
  '<svg><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="/></svg>';

// Create a new PDFDocument
const pdfDoc = await PDFDocument.create();

// Add a blank page to the document
const page = pdfDoc.addPage();

// drawSvg can accept the svg as a string, as long as there are no images in it
page.moveTo(100, 10);
page.drawSvg(svg);

// If the svg has images, or if you don't know if it does, you should call embedSVG first
page.moveTo(200, 10);
const pdfSvg = await pdfDoc.embedSvg(svg2);
page.drawSvg(pdfSvg);

// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();

Deno Usage

pdf-lib fully supports the exciting new Deno runtime! All of the usage examples work in Deno. The only thing you need to do is change the imports for pdf-lib and fontkit to use CDN URLs, because Deno requires all modules to be referenced via URLs.

See also How to Create and Modify PDF Files in Deno With pdf-lib

Creating a Document with Deno

Below is the create document example modified for Deno:

import {
  PDFDocument,
  StandardFonts,
  rgb,
} from 'https://cdn.skypack.dev/pdf-lib@^1.11.1?dts';

const pdfDoc = await PDFDocument.create();
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);

const page = pdfDoc.addPage();
const { width, height } = page.getSize();
const fontSize = 30;
page.drawText('Creating PDFs in JavaScript is awesome!', {
  x: 50,
  y: height - 4 * fontSize,
  size: fontSize,
  font: timesRomanFont,
  color: rgb(0, 0.53, 0.71),
});

const pdfBytes = await pdfDoc.save();

await Deno.writeFile('out.pdf', pdfBytes);

If you save this script as create-document.ts, you can execute it using Deno with the following command:

deno run --allow-write create-document.ts

The resulting out.pdf file will look like this PDF.

Embedding a Font with Deno

Here's a slightly more complicated example demonstrating how to embed a font and measure text in Deno:

import {
  degrees,
  PDFDocument,
  rgb,
  StandardFonts,
} from 'https://cdn.skypack.dev/pdf-lib@^1.11.1?dts';
import fontkit from 'https://esm.sh/@cantoo/[email protected]';

const url = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

const pdfDoc = await PDFDocument.create();

pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);

const page = pdfDoc.addPage();

const text = 'This is text in an embedded font!';
const textSize = 35;
const textWidth = customFont.widthOfTextAtSize(text, textSize);
const textHeight = customFont.heightAtSize(textSize);

page.drawText(text, {
  x: 40,
  y: 450,
  size: textSize,
  font: customFont,
  color: rgb(0, 0.53, 0.71),
});
page.drawRectangle({
  x: 40,
  y: 450,
  width: textWidth,
  height: textHeight,
  borderColor: rgb(1, 0, 0),
  borderWidth: 1.5,
});

const pdfBytes = await pdfDoc.save();

await Deno.writeFile('out.pdf', pdfBytes);

If you save this script as custom-font.ts, you can execute it with the following command:

deno run --allow-write --allow-net custom-font.ts

The resulting out.pdf file will look like this PDF.

Complete Examples

The usage examples provide code that is brief and to the point, demonstrating the different features of pdf-lib. You can find complete working examples in the apps/ directory. These apps are used to do manual testing of pdf-lib before every release (in addition to the automated tests).

There are currently four apps:

  • node - contains tests for pdf-lib in Node environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib from the filesystem. They also allow you to quickly open your PDFs in different viewers (Acrobat, Preview, Foxit, Chrome, Firefox, etc...) to ensure compatibility.
  • web - contains tests for pdf-lib in browser environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib in a browser environment.
  • rn - contains tests for pdf-lib in React Native environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib in a React Native environment.
  • deno - contains tests for pdf-lib in Deno environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib from the filesystem.

Installation

NPM Module

To install the latest stable Cantoo fork:

# With npm
npm install --save @cantoo/pdf-lib

# With yarn
yarn add @cantoo/pdf-lib

(pdf-lib without the scope is still the original Hopding package on npm.)

Vendored PNG & standard fonts

@cantoo/pdf-lib vendors @cantoo/upng and @cantoo/standard-fonts under src/packages/ (same pattern as @cantoo/fontkit). Compression uses fflate (no pako).

Install the subpackages alone if needed:

npm install @cantoo/upng @cantoo/standard-fonts
# or via pdf-lib subpath exports:
# import UPNG from '@cantoo/pdf-lib/upng'
# import { FontNames } from '@cantoo/pdf-lib/standard-fonts'

UMD Module

You can also download pdf-lib as a UMD module from unpkg or jsDelivr. The UMD builds have been compiled to ES5, so they should work in any modern browser. UMD builds are useful if you aren't using a package manager or module bundler. For example, you can use them directly in the <script> tag of an HTML page.

The following builds are available:

NOTE: if you are using the CDN scripts in production, you should include a specific version number in the URL, for example:

When using a UMD build, you will have access to a global window.PDFLib variable. This variable contains all of the classes and functions exported by pdf-lib. For example:

// NPM module
import { PDFDocument, rgb } from 'pdf-lib';

// UMD module
var PDFDocument = PDFLib.PDFDocument;
var rgb = PDFLib.rgb;

Fontkit Installation

pdf-lib relies upon @cantoo/fontkit to support embedding custom fonts. You must add a fontkit-compatible module to your project and register it using pdfDoc.registerFontkit(...) before embedding custom fonts (see the font embedding example). This module is not included by default because not all users need it, and it increases bundle size.

Prefer Cantoo's maintained @cantoo/fontkit fork (based on fontkit v2). It ships TypeScript types and UMD builds (including a minified file). Upstream fontkit and the older @pdf-lib/fontkit package remain supported for compatibility.

Fontkit NPM Module

# With npm
npm install --save @cantoo/fontkit

# With yarn
yarn add @cantoo/fontkit

Register the fontkit instance before embedding custom fonts. With @cantoo/fontkit ≥2.0.9 you can use a default import (recommended). A namespace import also works. Upstream fontkit still has no default export under Node ESM, so use import * as fontkit from 'fontkit' if you depend on that package.

import { PDFDocument } from 'pdf-lib'
import fontkit from '@cantoo/fontkit'

const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)

Fontkit UMD Module

You can also download @cantoo/fontkit as a UMD module from unpkg or jsDelivr:

  • https://unpkg.com/@cantoo/fontkit/dist/fontkit.umd.min.js
  • https://unpkg.com/@cantoo/fontkit/dist/fontkit.umd.js
  • https://cdn.jsdelivr.net/npm/@cantoo/fontkit/dist/fontkit.umd.min.js
  • https://cdn.jsdelivr.net/npm/

版本与发布记录

@cantoo/pdf-lib 共发布 122 个版本,首次发布于 2021/03/12;过去 12 个月发布了 21 个版本。

版本发布日期距今
2.11.1latest2026/09/151 个月前
2.11.02026/09/111 个月前
2.9.22026/09/082 个月前
2.9.12026/08/182 个月前
2.9.02026/08/182 个月前
2.8.42026/08/172 个月前

其他发布通道:next → 2.2.3-0。

关于 @cantoo/pdf-lib 的常见问题

@cantoo/pdf-lib 是什么?

Create and modify PDF files with JavaScript

如何安装 @cantoo/pdf-lib?

在项目目录运行 npm install @cantoo/pdf-lib,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:html-entities(>=2.3.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cantoo/pdf-lib 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cantoo/pdf-lib 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cantoo/pdf-lib 可以商用吗?

@cantoo/pdf-lib 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cantoo/pdf-lib 的下载量有多少?

根据 npm 官方下载统计,@cantoo/pdf-lib 在 2026/10/09 – 2026/11/07 这 30 天内被下载 8,960,926 次,日均约 298,698 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cantoo/pdf-lib 还在维护吗?

最新版本 2.11.1 发布于 2026/09/15(1 个月前);至今共发布 122 个版本,其中过去 12 个月发布了 21 个;首次发布于 2021/03/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。