How to manipulate PDF pages with pdf-lib: Extract, merge, and remove pages
Table of contents
pdf-lib to manipulate PDF pages in Node.js — extract specific pages, merge multiple documents, remove pages, and trim PDFs for processing. This tutorial covers the key operations that PDF.js can’t handle.
PDF.js reads and renders PDFs but can’t modify them structurally. Reach for pdf-lib when you need to:
- Extract specific pages from a source PDF.
- Merge multiple PDFs into one.
- Remove pages by index.
- Trim a PDF to the first N pages before uploading to a processing service.
The core API is PDFDocument.load() → copyPages() → addPage() → save(). Page indices are zero-based, and pdf-lib works in both Node.js and the browser with no external workers or fonts. Nutrient Web SDK handles the same operations client-side via the applyOperations API.
PDF.js reads and renders PDFs but doesn’t modify them structurally. For operations like extracting pages, merging documents, or trimming PDFs, use pdf-lib.
Install
Install pdf-lib with npm:
npm install pdf-libCopying specific pages
Extract the first N pages from a PDF — useful for sending a preview to a processing service:
import { PDFDocument } from "pdf-lib";
async function copyFirstTwoPages(pdfBuffer) { const originalPdf = await PDFDocument.load(pdfBuffer); const pageCount = originalPdf.getPageCount();
if (pageCount === 0) { throw new Error("The PDF has no pages."); }
const newPdf = await PDFDocument.create(); const indices = originalPdf.getPageIndices().slice(0, 2); const pages = await newPdf.copyPages(originalPdf, indices); pages.forEach((page) => newPdf.addPage(page));
return Buffer.from(await newPdf.save());}Key concepts
The core PDFDocument API covers three building blocks: loading, creating, and copying pages.
Loading a PDF
PDFDocument.load() accepts a Buffer, Uint8Array, or base64 string and returns a PDFDocument instance:
// From Buffer.const pdf = await PDFDocument.load(buffer);
// From Uint8Array.const pdf = await PDFDocument.load(uint8Array);
// From base64 string.const pdf = await PDFDocument.load(base64String);Creating a new PDF
Use PDFDocument.create() to start a new, empty document that you can populate with pages copied from existing PDFs:
const newPdf = await PDFDocument.create();Copying pages between documents
copyPages copies pages from a source document into a destination document. It returns page objects that you then add:
// Copy pages at indices 0, 2, 4 from source.const copiedPages = await destPdf.copyPages(sourcePdf, [0, 2, 4]);
// Add them to the destination.for (const page of copiedPages) { destPdf.addPage(page);}Saving
save() serializes the document back to bytes. The return value is a Uint8Array, which you can convert to a Node.js Buffer for writing to disk, passing to an HTTP response, or uploading to storage:
const bytes = await pdf.save(); // Returns Uint8Array.const buffer = Buffer.from(bytes); // Convert to Node.js Buffer.Use case: Trim a PDF before processing
Some services (like GROBID for academic metadata extraction) only need the first few pages. Sending a trimmed PDF is faster and avoids upload size limits:
async function trimForProcessing(pdfBuffer, maxPages = 2) { const original = await PDFDocument.load(pdfBuffer); const pageCount = original.getPageCount(); const pagesToCopy = Math.min(pageCount, maxPages);
const trimmed = await PDFDocument.create(); const indices = Array.from({ length: pagesToCopy }, (_, i) => i); const pages = await trimmed.copyPages(original, indices);
for (const page of pages) { trimmed.addPage(page); }
return Buffer.from(await trimmed.save());}Use case: Merge multiple PDFs
Loop over an array of buffers, copying all pages from each source into a single output document:
async function mergePdfs(pdfBuffers) { const merged = await PDFDocument.create();
for (const buffer of pdfBuffers) { const source = await PDFDocument.load(buffer); const indices = source.getPageIndices(); // [0, 1, 2, ...] const pages = await merged.copyPages(source, indices);
for (const page of pages) { merged.addPage(page); } }
return Buffer.from(await merged.save());}Use case: Remove specific pages
Filter the page indices to exclude the ones you want to remove. Then copy the rest into a new document:
async function removePages(pdfBuffer, pagesToRemove) { const original = await PDFDocument.load(pdfBuffer); const allIndices = original.getPageIndices(); const keepIndices = allIndices.filter((i) => !pagesToRemove.includes(i));
const result = await PDFDocument.create(); const pages = await result.copyPages(original, keepIndices);
for (const page of pages) { result.addPage(page); }
return Buffer.from(await result.save());}pdf-lib vs. PDF.js
| Operation | pdf-lib | PDF.js |
|---|---|---|
| Read text content | No | Yes (getTextContent) |
| Render pages | No | Yes (PDFViewer, canvas) |
| Copy/move pages | Yes | No |
| Create new PDFs | Yes | No |
| Modify PDF metadata | Yes | No |
| Add form fields | Yes | No |
| Embed images/fonts | Yes | No |
Use PDF.js for reading and rendering. Use pdf-lib for creating and modifying.
Key points
pdf-libworks in both Node.js and the browser.copyPagesis the core method — it copies pages between documents.- Always call
addPageaftercopyPages— copied pages aren’t automatically added. save()returns aUint8Array; convert withBuffer.from()for Node.js.- Page indices are zero-based.
pdf-libdoesn’t need any external fonts, workers, or CMaps.
FAQ
PDF.js modify PDFs?PDF.js is a parser and renderer, not a writer. It opens and displays PDFs but has no API to create, modify, or save them. For structural changes — adding, removing, reordering, or merging pages — you need a library like pdf-lib or pdfkit, or a commercial SDK with editing support.
pdf-lib work in the browser?Yes. pdf-lib is pure JavaScript with no native dependencies, so it runs in Node.js and the browser. In the browser, load PDFs from a File, Blob, or Uint8Array instead of a Buffer, and use Blob/URL.createObjectURL to deliver the saved output for download.
addPage after copyPages?copyPages returns an array of PDFPage objects that have been deep-copied from the source document into the destination, but they aren’t inserted into the destination’s page tree until you call addPage (or insertPage). This two-step design lets you copy pages once and reorder them freely before committing.
pdf-lib’s output to a Node.js Buffer?pdfDocument.save() returns a Uint8Array. Wrap it with Buffer.from() to get a Node.js Buffer for fs.writeFile, HTTP responses, or S3 uploads: const buffer = Buffer.from(await pdf.save()).
pdf-lib render or display PDFs?No. pdf-lib builds and modifies PDFs but doesn’t render them. Pair it with PDF.js (for viewing) or a commercial viewer SDK when you need both editing and display.
Nutrient handles add, remove, merge, split, rotate, and reorder operations client-side through instance.applyOperations() and exports the result with instance.exportPDF(). There’s no separate library to install, no manual copyPages and addPage loops, and the same operations apply to a live viewer. See the page manipulation guide for details.
How Nutrient Web SDK handles this
Nutrient Web SDK handles page manipulation directly in the browser with the applyOperations API — no pdf-lib dependency, no manual page copying, and no addPage() loops:
// Add a blank page after page 1.instance.applyOperations([ { type: "addPage", afterPageIndex: 1, backgroundColor: new NutrientViewer.Color({ r: 255, g: 255, b: 255 }), pageWidth: 595, pageHeight: 842, },]);
// Remove a page.instance.applyOperations([{ type: "removePage", pageIndex: 3 }]);
// Export the modified PDF.const buffer = await instance.exportPDF();Nutrient also supports merging, splitting, rotating, cropping, and reordering — all client-side.
See Nutrient Web SDK for an alternative to PDF.js and pdf-lib. The page manipulation and save/export guides cover the API in depth. Follow the migration guide to switch, or talk to Sales about your requirements.