DEV Community

toolzip
toolzip

Posted on Originally published at toolzip.app

PDF Manipulation in the Browser With pdf-lib — A Practical Guide

PDF manipulation used to mean server-side tools. pdf-lib changed that.

It's a pure JavaScript library that runs in Node.js and the browser. No native dependencies. No server calls. Here's what you can do with it.

Setup

npm install pdf-lib
Enter fullscreen mode Exit fullscreen mode

Or from CDN:

<script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

1. Merge PDFs

import { PDFDocument } from "pdf-lib";

async function mergePDFs(files) {
  const merged = await PDFDocument.create();

  for (const file of files) {
    const bytes = await file.arrayBuffer();
    const pdf = await PDFDocument.load(bytes);
    const pageIndices = pdf.getPageIndices(); // [0, 1, 2, ...]
    const pages = await merged.copyPages(pdf, pageIndices);
    pages.forEach(page => merged.addPage(page));
  }

  const mergedBytes = await merged.save();
  return new Blob([mergedBytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

2. Split PDF (Extract Page Range)

async function splitPDF(file, startPage, endPage) {
  // startPage and endPage are 1-indexed (human-friendly)
  const bytes = await file.arrayBuffer();
  const sourcePdf = await PDFDocument.load(bytes);

  const newPdf = await PDFDocument.create();

  // Convert to 0-indexed array
  const pageIndices = [];
  for (let i = startPage - 1; i < endPage; i++) {
    pageIndices.push(i);
  }

  const pages = await newPdf.copyPages(sourcePdf, pageIndices);
  pages.forEach(page => newPdf.addPage(page));

  const bytes = await newPdf.save();
  return new Blob([bytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

3. Rotate Pages

import { PDFDocument, degrees } from "pdf-lib";

async function rotatePDF(file, rotation, pageIndices = null) {
  // rotation: 90, 180, 270
  const bytes = await file.arrayBuffer();
  const pdf = await PDFDocument.load(bytes);
  const pages = pdf.getPages();

  const targetPages = pageIndices 
    ? pageIndices.map(i => pages[i])
    : pages; // rotate all if no specific pages

  targetPages.forEach(page => {
    const currentRotation = page.getRotation().angle;
    page.setRotation(degrees((currentRotation + rotation) % 360));
  });

  const rotatedBytes = await pdf.save();
  return new Blob([rotatedBytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

4. Add Text Annotations

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

async function addWatermark(file, text) {
  const bytes = await file.arrayBuffer();
  const pdf = await PDFDocument.load(bytes);
  const font = await pdf.embedFont(StandardFonts.HelveticaBold);

  pdf.getPages().forEach(page => {
    const { width, height } = page.getSize();
    const fontSize = 60;
    const textWidth = font.widthOfTextAtSize(text, fontSize);

    page.drawText(text, {
      x: (width - textWidth) / 2,
      y: height / 2,
      size: fontSize,
      font,
      color: rgb(0.8, 0.8, 0.8), // light gray
      opacity: 0.3,
      rotate: degrees(45),
    });
  });

  const bytes = await pdf.save();
  return new Blob([bytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

5. Embed Images Into PDF

import { PDFDocument } from "pdf-lib";

async function addImageToPDF(pdfFile, imageFile, pageIndex = 0) {
  const pdfBytes = await pdfFile.arrayBuffer();
  const imageBytes = await imageFile.arrayBuffer();

  const pdf = await PDFDocument.load(pdfBytes);

  // Embed image (supports PNG and JPEG)
  let image;
  if (imageFile.type === "image/png") {
    image = await pdf.embedPng(imageBytes);
  } else {
    image = await pdf.embedJpg(imageBytes);
  }

  const page = pdf.getPage(pageIndex);
  const { width, height } = page.getSize();

  // Scale image to fit page width
  const scale = width / image.width;
  const scaledWidth = image.width * scale;
  const scaledHeight = image.height * scale;

  page.drawImage(image, {
    x: 0,
    y: height - scaledHeight,
    width: scaledWidth,
    height: scaledHeight,
  });

  const resultBytes = await pdf.save();
  return new Blob([resultBytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

6. Create PDF From Scratch

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

async function createPDF(content) {
  const pdf = await PDFDocument.create();
  const font = await pdf.embedFont(StandardFonts.Helvetica);

  const page = pdf.addPage([595, 842]); // A4 in points
  const { width, height } = page.getSize();

  page.drawText(content, {
    x: 50,
    y: height - 100,
    size: 12,
    font,
    color: rgb(0, 0, 0),
    lineHeight: 18,
    maxWidth: width - 100,
  });

  const bytes = await pdf.save();
  return new Blob([bytes], { type: "application/pdf" });
}
Enter fullscreen mode Exit fullscreen mode

What pdf-lib Cannot Do

Be upfront about limitations:

Cannot render PDF to image. For that, use PDF.js (pdfjs-dist).

Cannot read existing text reliably. Text extraction from PDFs is notoriously unreliable. Use pdf.js or server-side tools for OCR.

Cannot parse form fields well. The library has basic AcroForm support, but complex interactive forms may not work as expected.

No font subsetting. If you embed a custom font, the entire font file is embedded. For CJK fonts (Chinese, Japanese, Korean), this means potentially 5-10MB per font.

Combining pdf-lib With PDF.js

For "read then modify" workflows, combine both libraries:

// pdf.js for reading/rendering
import * as pdfjs from "pdfjs-dist";

// pdf-lib for modifying
import { PDFDocument } from "pdf-lib";

// Render page to canvas with pdf.js
const pdfDoc = await pdfjs.getDocument(url).promise;
const page = await pdfDoc.getPage(1);
const viewport = page.getViewport({ scale: 1.5 });
await page.render({ canvasContext: ctx, viewport }).promise;

// Modify with pdf-lib
const doc = await PDFDocument.load(bytes);
// ... modifications
Enter fullscreen mode Exit fullscreen mode

Performance Notes

For large PDFs (100+ pages), PDFDocument.load() can take several seconds. Consider:

// Show loading state
setLoading(true);
// Process in next tick to avoid blocking UI
await new Promise(resolve => setTimeout(resolve, 0));
const pdf = await PDFDocument.load(bytes);
setLoading(false);
Enter fullscreen mode Exit fullscreen mode

For very large files, consider using a Web Worker to avoid blocking the main thread.


pdf-lib powers the PDF tools at ToolZip — PDF merge, split, rotate, and more, all client-side.

Top comments (0)