DEV Community

Shaishav Patel
Shaishav Patel

Posted on

How We Run All PDF Operations in the Browser — pdf-lib, No Server, No Upload

Every PDF tool at Ultimate Tools — merge, split, rotate, compress, reorder, watermark, protect — runs entirely in the browser. No file ever leaves your device. Here's how client-side PDF processing works with pdf-lib and what the architecture looks like.


Why Client-Side PDF Processing

Privacy: Financial documents, legal contracts, medical records. Users don't want these on a stranger's server. Client-side processing is the only honest answer to "where does my file go?"

Speed: Network upload of a 30MB PDF takes time. Local processing is instant — no round-trip.

Cost: Server-side PDF processing at scale (CPU, memory, storage) is expensive. Shifting it to the client eliminates that cost entirely.

The tradeoff: Client-side is limited by the user's device RAM and CPU. A 500-page PDF with embedded high-res images can exhaust memory on low-end devices. Our tools cap file input at 100MB and show a warning for operations that will be slow.


The Core Library: pdf-lib

pdf-lib is a pure JavaScript PDF library that runs in both Node.js and the browser. It handles:

  • Loading and parsing existing PDFs
  • Modifying page order, rotation, metadata
  • Merging multiple PDFs
  • Splitting into separate documents
  • Embedding fonts, images, and form fields

It doesn't handle rendering PDF pages to images (that requires pdf.js) or advanced compression (that requires native binaries). But for structural operations — merge, split, rotate, reorder, basic watermark — it covers everything.


Loading a PDF

import { PDFDocument } from 'pdf-lib';

async function loadPdf(file: File): Promise<PDFDocument> {
  const buffer = await file.arrayBuffer();
  return PDFDocument.load(buffer, {
    ignoreEncryption: false, // throw on password-protected PDFs
  });
}
Enter fullscreen mode Exit fullscreen mode

file.arrayBuffer() reads the entire file into memory. For a 50MB PDF, this allocates 50MB in the browser's heap. The parsed PDFDocument object is typically another 2–3× the file size in memory. This is why large file limits matter for client-side processing.


Merge: Copying Pages Between Documents

async function mergePdfs(files: File[]): Promise<Uint8Array> {
  const merged = await PDFDocument.create();

  for (const file of files) {
    const doc = await loadPdf(file);
    const pages = await merged.copyPages(doc, doc.getPageIndices());
    pages.forEach(page => merged.addPage(page));
  }

  return merged.save();
}
Enter fullscreen mode Exit fullscreen mode

copyPages copies page objects from one document into another, including all embedded fonts, images, and resources referenced by those pages. doc.getPageIndices() returns [0, 1, 2, ...] for all pages.

The result is a Uint8Array — the raw bytes of the merged PDF. Pass it to a Blob for download.


Split: Extracting Page Subsets

async function extractPages(file: File, pageNumbers: number[]): Promise<Uint8Array> {
  const source = await loadPdf(file);
  const output = await PDFDocument.create();

  // Convert 1-based user input to 0-based indices
  const indices = pageNumbers.map(n => n - 1).filter(i => i >= 0 && i < source.getPageCount());

  const pages = await output.copyPages(source, indices);
  pages.forEach(page => output.addPage(page));

  return output.save();
}
Enter fullscreen mode Exit fullscreen mode

To split a PDF into individual pages:

async function splitIntoPages(file: File): Promise<Uint8Array[]> {
  const source = await loadPdf(file);
  const results: Uint8Array[] = [];

  for (let i = 0; i < source.getPageCount(); i++) {
    const singlePage = await PDFDocument.create();
    const [page] = await singlePage.copyPages(source, [i]);
    singlePage.addPage(page);
    results.push(await singlePage.save());
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

Each extracted PDF is a new document containing only the copied page and its resources.


Reorder Pages

async function reorderPages(file: File, newOrder: number[]): Promise<Uint8Array> {
  // newOrder is 1-based: [3, 1, 2] = move page 3 first, then 1, then 2
  const source = await loadPdf(file);
  const output = await PDFDocument.create();

  const indices = newOrder.map(n => n - 1);
  const pages   = await output.copyPages(source, indices);
  pages.forEach(page => output.addPage(page));

  return output.save();
}
Enter fullscreen mode Exit fullscreen mode

copyPages accepts an array of indices in any order — the pages are returned in the order specified. This makes reordering as simple as passing the desired sequence.


Memory Management

For large multi-file merges, process and discard documents one at a time:

async function mergeInChunks(files: File[]): Promise<Uint8Array> {
  const merged = await PDFDocument.create();

  for (const file of files) {
    // Process one file at a time — let previous go out of scope
    const doc   = await PDFDocument.load(await file.arrayBuffer());
    const pages = await merged.copyPages(doc, doc.getPageIndices());
    pages.forEach(p => merged.addPage(p));
    // doc goes out of scope here — eligible for GC
  }

  return merged.save();
}
Enter fullscreen mode Exit fullscreen mode

JavaScript's garbage collector will reclaim memory from unreferenced PDFDocument objects, but GC timing is non-deterministic. For very large batches, consider processing in groups of 5–10 files and monitoring performance.memory.usedJSHeapSize to abort if memory gets critically high.


Downloading the Result

function downloadPdf(bytes: Uint8Array, filename: string): void {
  const blob = new Blob([bytes], { type: 'application/pdf' });
  const url  = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  URL.revokeObjectURL(url);
}
Enter fullscreen mode Exit fullscreen mode

The PDF tools at Ultimate Tools use this same pattern for Merge PDF, Split PDF, Rotate PDF, Remove PDF Pages, and the full PDF Studio. All operations run entirely in the browser — your PDFs never leave your device.

Top comments (0)