Axiomatic ToolsFree Fast Essential
All posts

How we made PDF tools fast — and private — with WebAssembly

A look at how Axiomatic Tools merges, splits, rotates, and watermarks PDFs entirely in your browser using pdf-lib and Web Workers.

Axiomatic Tools Teamengineeringwasm

When you click "Merge PDF" on most websites, the file you just selected is uploaded to a server, processed there, and then either sent back as a download or held until you click a second button. The actual merge takes a fraction of a second; the upload and download are what you wait on.

We do it differently. The merge runs in your browser. No file leaves your device.

The pieces

A merge in 25 lines

import { PDFDocument } from 'pdf-lib';

export async function merge(files: File[]): Promise<Blob> {
  const merged = await PDFDocument.create();
  for (const f of files) {
    const bytes = new Uint8Array(await f.arrayBuffer());
    const src = await PDFDocument.load(bytes, { ignoreEncryption: true });
    const pages = await merged.copyPages(src, src.getPageIndices());
    pages.forEach((p) => merged.addPage(p));
  }
  return new Blob([await merged.save()], { type: 'application/pdf' });
}

That's the whole thing. Wrapped in a worker, exposed through Comlink, called from a React component. The result is an ObjectURL you can plug straight into an <a download> link.

When client-side stops working

Three failure modes push us back to the server:

  1. OCR, transcription, AI image work. Tesseract.js exists and is competent for short English text, but for production-quality multilingual OCR you really want full Tesseract + ocrmypdf, plus the option to fall back to a vision model. Same story for Whisper. These live on our GPU pool.
  2. PDF compression. It's possible in WASM (we tried). It's also slow and the resulting files are larger than what Ghostscript produces. Server-side wins.
  3. Files larger than ~1.5 GB. Modern browsers can handle them, but most users on most machines run out of memory long before that.

For everything else, in-browser is faster, cheaper, and more private — three things that don't usually line up. WebAssembly is the rare technology where they do.

If you want to look at the code, the WASM dispatcher and handlers live in packages/wasm-runtime.