I built a PDF extraction tool for a law firm that needed to pull specific pages from hundreds of case files. The first version loaded each entire PDF into memory before extracting pages, it crashed on 200MB contracts. Moving to a streaming approach with pdf-lib fixed the memory issues. This guide covers every common PDF operation with the actual code and the real constraints you will hit.
Merge, split, and process PDFs locally.
All PDF operations run in your browser. Your documents never leave your device.
Open PDF Tools βThe Two Libraries for Browser PDF Work
Most browser-based PDF work uses one of two libraries, and they serve different purposes:
| Library | Purpose | Size | Best For |
|---|---|---|---|
| pdf-lib | Create and modify PDF structure | ~300KB | Merge, split, rotate, form filling, metadata |
| PDF.js | Render and display PDFs | ~1.5MB | Page previews, text extraction, thumbnails |
Read File
file.arrayBuffer()
Load in Memory
pdf-lib / PDF.js
Transform
split, rotate, extract
Download
Blob, no upload
Splitting a PDF into Individual Pages
1import { PDFDocument } from 'pdf-lib';2Β 3async function splitPDF(file: File): Promise<Blob[]> {4 const arrayBuffer = await file.arrayBuffer();5 const sourcePdf = await PDFDocument.load(arrayBuffer);6 const pageCount = sourcePdf.getPageCount();7 const pages: Blob[] = [];8Β 9 for (let i = 0; i < pageCount; i++) {10 // Create a new single-page document for each page11 const singlePageDoc = await PDFDocument.create();12 const [copiedPage] = await singlePageDoc.copyPages(sourcePdf, [i]);13 singlePageDoc.addPage(copiedPage);14Β 15 const bytes = await singlePageDoc.save();16 pages.push(new Blob([bytes], { type: 'application/pdf' }));17 }18Β 19 return pages; // Array of single-page PDF Blobs20}21Β 22// Download all pages as separate files23const pageBlobs = await splitPDF(file);24pageBlobs.forEach((blob, index) => {25 const url = URL.createObjectURL(blob);26 const link = document.createElement('a');27 link.href = url;28 link.download = `page-${index + 1}.pdf`;29 link.click();30 URL.revokeObjectURL(url);31});Rotating Pages
1import { PDFDocument, degrees } from 'pdf-lib';2Β 3async function rotatePDFPages(4 file: File,5 pageIndices: number[], // Which pages to rotate (0-indexed)6 rotation: 90 | 180 | 270 // Rotation in degrees7): Promise<Blob> {8 const arrayBuffer = await file.arrayBuffer();9 const pdfDoc = await PDFDocument.load(arrayBuffer);10Β 11 pageIndices.forEach(index => {12 const page = pdfDoc.getPage(index);13 const currentRotation = page.getRotation().angle;14 // Add to existing rotation (in case page was already rotated)15 page.setRotation(degrees((currentRotation + rotation) % 360));16 });17Β 18 const bytes = await pdfDoc.save();19 return new Blob([bytes], { type: 'application/pdf' });20}21Β 22// Rotate only page 1 (index 0) by 90 degrees clockwise23const rotated = await rotatePDFPages(file, [0], 90);Extracting Text from a PDF
PDF.js can extract text content from a PDF without rendering it visually. This only works for PDFs with actual text, scanned PDFs require OCR:
1import * as pdfjsLib from 'pdfjs-dist';2Β 3// Required: set the worker URL (use a CDN or bundle the worker separately)4pdfjsLib.GlobalWorkerOptions.workerSrc =5 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';6Β 7async function extractTextFromPDF(file: File): Promise<string> {8 const arrayBuffer = await file.arrayBuffer();9 const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;10 const pageCount = pdfDoc.numPages;11 const textPages: string[] = [];12Β 13 for (let i = 1; i <= pageCount; i++) {14 const page = await pdfDoc.getPage(i);15 const textContent = await page.getTextContent();16Β 17 // Concatenate text items β maintain word spacing18 const pageText = textContent.items19 .map((item: pdfjsLib.TextItem | pdfjsLib.TextMarkedContent) => {20 if ('str' in item) return item.str;21 return '';22 })23 .join(' ');24Β 25 textPages.push(`--- Page ${i} ---26${pageText}`);27 }28Β 29 return textPages.join('30Β 31');32}33Β 34const text = await extractTextFromPDF(file);35console.log(text); // Full document text, page by pageGenerating Page Thumbnails
PDF.js renders pages to a canvas. Use OffscreenCanvas in a Web Worker to avoid blocking the main thread during rendering:
1// In the main thread: send file to worker2const worker = new Worker('/pdf-thumbnail-worker.js');3const arrayBuffer = await file.arrayBuffer();4worker.postMessage({ buffer: arrayBuffer, pageIndex: 0 }, [arrayBuffer]);5Β 6worker.onmessage = (e) => {7 const { thumbnailBlob } = e.data;8 const img = document.createElement('img');9 img.src = URL.createObjectURL(thumbnailBlob);10 document.body.appendChild(img);11};12Β 13// In pdf-thumbnail-worker.js:14importScripts('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js');15pdfjsLib.GlobalWorkerOptions.workerSrc = ''; // Already in a worker16Β 17self.onmessage = async ({ data: { buffer, pageIndex } }) => {18 const pdf = await pdfjsLib.getDocument({ data: buffer }).promise;19 const page = await pdf.getPage(pageIndex + 1);20 const viewport = page.getViewport({ scale: 0.5 }); // 50% size thumbnail21Β 22 // OffscreenCanvas avoids main-thread paint blocking23 const canvas = new OffscreenCanvas(viewport.width, viewport.height);24 await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;25Β 26 const blob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.85 });27 self.postMessage({ thumbnailBlob: blob }, []);28};Browser Limits and Workarounds
Memory Management for Large Files
Processing a 200MB PDF crashes Chrome tabs on low-memory devices. The peak memory usage during PDF.js rendering is typically 3β5Γ the file size. A 50MB PDF may need 150β250MB of heap during rendering.
| File Size | Main Thread Time | Web Worker Time | Peak Heap |
|---|---|---|---|
| 10MB (100 pages) | 1.8s (UI lag) | 0.4s | ~35MB |
| 50MB (500 pages) | 8.5s (freezes UI) | 1.9s | ~140MB |
| 200MB (2,000 pages) | Crashes tab | 7.2s | ~480MB |
For files over 50MB: always use a Web Worker. For files over 200MB: process pages in batches and release page references between batches withpage.cleanup() in PDF.js.
Encrypted PDFs
Both pdf-lib and PDF.js throw on encrypted PDFs unless you provide the password. PDF.js's getDocument accepts a password option:
1// Handle password-protected PDFs with PDF.js2try {3 const pdfDoc = await pdfjsLib.getDocument({4 data: arrayBuffer,5 password: userPassword, // If you have it6 }).promise;7} catch (err) {8 if (err instanceof pdfjsLib.PasswordException) {9 if (err.code === pdfjsLib.PasswordResponses.NEED_PASSWORD) {10 // Prompt user for password11 } else if (err.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {12 // Wrong password13 }14 }15}Linearised PDFs Load Faster
Linearised (also called βweb-optimisedβ) PDFs are structured so that the first page can be displayed before the entire file is downloaded. For large PDFs served from a URL, linearisation dramatically improves time-to-first-page. PDF.js automatically takes advantage of linearised PDFs when loading from a URL.
1# Linearise a PDF using qpdf (free, cross-platform)2qpdf --linearize input.pdf output-linearised.pdf3Β 4# Linearise with Ghostscript5gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite \6 -dFastWebView=true \7 -sOutputFile=output-linearised.pdf input.pdfSummary: Operation Decision Guide
| Operation | Library | Notes |
|---|---|---|
| Merge PDFs | pdf-lib | Use memory-conscious loop for files over 50MB |
| Split PDF into pages | pdf-lib | Create separate PDFDocument per page |
| Rotate pages | pdf-lib | Respects existing page rotation, adds to it |
| Extract text | PDF.js | Only works on text PDFs (not scanned images) |
| Render page thumbnails | PDF.js + OffscreenCanvas | Use Web Worker to avoid UI freezing |
| Fill PDF forms | pdf-lib | Flatten before merging to avoid field conflicts |
| OCR scanned PDFs | Tesseract.js (slow) | 3β10s per page; better handled server-side for volume |
For the specific details of PDF merging, including handling encrypted files and form field conflicts, read our dedicated merge guide. If your compressed PDF came out larger than the original, our PDF compression size guide explains the six causes and how to fix each one.
Process PDFs locally: merge, split, and more.
All PDF operations run in your browser. No upload. No account. Works offline.
Open PDF Tools β