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
Or from CDN:
<script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
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" });
}
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" });
}
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" });
}
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" });
}
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" });
}
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" });
}
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
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);
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.