This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/pdfjs-pdf-page-manipulation-pdf-lib.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. How to manipulate PDF pages with pdf-lib: Extract, merge, and remove pages

Table of contents

    Use 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.
    How to manipulate PDF pages with pdf-lib: Extract, merge, and remove pages
    TL;DR

    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:

    Terminal window
    npm install pdf-lib

    Copying 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

    Operationpdf-libPDF.js
    Read text contentNoYes (getTextContent)
    Render pagesNoYes (PDFViewer, canvas)
    Copy/move pagesYesNo
    Create new PDFsYesNo
    Modify PDF metadataYesNo
    Add form fieldsYesNo
    Embed images/fontsYesNo

    Use PDF.js for reading and rendering. Use pdf-lib for creating and modifying.

    Key points

    • pdf-lib works in both Node.js and the browser.
    • copyPages is the core method — it copies pages between documents.
    • Always call addPage after copyPages — copied pages aren’t automatically added.
    • save() returns a Uint8Array; convert with Buffer.from() for Node.js.
    • Page indices are zero-based.
    • pdf-lib doesn’t need any external fonts, workers, or CMaps.

    FAQ

    Why can’t 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.

    Does 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.

    Why do I need 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.

    How do I convert 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()).

    Can 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.

    How does Nutrient Web SDK compare?

    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.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?