โ† Back to Blog
TutorialsMay 10, 2026ยท7 min read

Mastering PDF Manipulation with JavaScript

R
The Rapid DevTools Team
Updated May 26, 2026

For years, manipulating PDF files meant a server-side stack: Python's PyPDF, Java's iText, or a command-line tool like Ghostscript. That made simple tasks โ€” merging a few files, adding page numbers โ€” surprisingly heavy to implement. Modern JavaScript has changed that. Today you can perform real PDF surgery directly in the browser, with no backend at all.

In this article we walk through how our PDF Tools merge documents and stamp page numbers entirely on the client side using the excellent pdf-lib library.

Why client-side PDF processing?

  • Privacy: contracts, invoices, and legal documents never leave the user's device.
  • Cost: no servers to run or scale.
  • Speed: no upload/download round trip for large files.

Reading a PDF into memory

The browser's File API gives us an ArrayBuffer, which pdf-libloads into a document object we can edit.

import { PDFDocument } from "pdf-lib";

const arrayBuffer = await file.arrayBuffer();
const pdf = await PDFDocument.load(arrayBuffer);

Merging multiple PDFs

To merge, we create a fresh document and copy the pages from each source file in order. The key method is copyPages, which clones pages (including their fonts and images) into the new document.

const merged = await PDFDocument.create();

for (const file of files) {
  const src = await PDFDocument.load(await file.arrayBuffer());
  const pages = await merged.copyPages(src, src.getPageIndices());
  pages.forEach((page) => merged.addPage(page));
}

const bytes = await merged.save(); // Uint8Array

Adding page numbers

To stamp page numbers, we embed a standard font and draw text onto each page. pdf-libuses a bottom-left origin, so a small y value places the text near the bottom of the page.

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

const font = await pdf.embedFont(StandardFonts.Helvetica);
const pages = pdf.getPages();

pages.forEach((page, i) => {
  const { width } = page.getSize();
  const text = `Page ${i + 1} of ${pages.length}`;
  const textWidth = font.widthOfTextAtSize(text, 10);
  page.drawText(text, {
    x: (width - textWidth) / 2, // centered
    y: 20,
    size: 10,
    font,
    color: rgb(0.4, 0.4, 0.4),
  });
});

Triggering a download

Finally, we wrap the resulting bytes in a Blob and create an object URL the user can download โ€” still without any server involvement.

const blob = new Blob([new Uint8Array(bytes)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
// set <a href={url} download="output.pdf">

Performance tips for large files

  • Process files as ArrayBuffers and avoid holding many copies in memory.
  • Revoke object URLs with URL.revokeObjectURL when you are done to free memory.
  • For very large batches, consider a Web Worker to keep the UI responsive.

Frequently asked questions

Does merging reduce PDF quality?

No. Copying pages preserves the original content streams, fonts, and images. There is no re-encoding, so quality is identical to the source files.

Can pdf-lib edit existing text?

It can add new content (text, images, pages) and modify structure, but it is not designed to reflow or rewrite existing text content. For that you would need a more specialized toolchain.

About Rapid DevTools

Rapid DevTools is a privacy-first collection of utilities for developers. Every tool runs entirely in your browser, so your data never leaves your device.