Implementing Browser-Only PDF Operations with qpdf-wasm

Use qpdf-wasm (a WebAssembly build of the qpdf CLI) to process PDFs entirely in the browser without uploading to a server.

Published 2026-01-30

Implementing Browser-Only PDF Operations with qpdf-wasm

If you want to process PDFs entirely in the browser without uploading them to a server, qpdf-wasm is a practical choice. It is the PDF tool qpdf compiled to WebAssembly (WASM).

In this article, using @neslinesli93/qpdf-wasm as an example, we cover practical patterns for page extraction, splitting, linearize (web optimization), and encrypt/decrypt in the browser.

ref: neslinesli93/qpdf-wasm: QPDF compiled to WASM, ready for the browser

How it works

  • Load the WASM module (qpdf itself)
  • Write input.pdf into the FS
  • Run CLI args via qpdf.callMain([...args])
  • Read output.pdf from the FS and convert it to a Blob for download/view

Install

npm i @neslinesli93/qpdf-wasm

In this package, you initialize by specifying the URL of qpdf-wasm via locateFile.

Minimal example: extract pages 1–2

import createModule from "@neslinesli93/qpdf-wasm";
 
export async function extractPages(input: ArrayBuffer) {
  const qpdf = await createModule({
    // Return the URL of the wasm file served by your bundler or CDN
    locateFile: () => "/wasm/qpdf-wasm",
  });
 
  // Write input into the virtual FS
  qpdf.FS.writeFile("/input.pdf", new Uint8Array(input));
 
  // Example: extract pages 1–2 into output.pdf
  qpdf.callMain(["/input.pdf", "--pages", ".", "1-2", "--", "/output.pdf"]);
 
  // Read from the virtual FS
  const out = qpdf.FS.readFile("/output.pdf");
 
  // Convert to a Blob for the browser
  return new Blob([out], { type: "application/pdf" });
}

Usage:

const file = await (await fetch(fileUrl)).arrayBuffer();
const blob = await extractPages(file);
const url = URL.createObjectURL(blob);
window.open(url, "_blank");

Below, we proceed assuming you’ve created a reusable wrapper like this.

import createModule from "@neslinesli93/qpdf-wasm";
 
type RunQpdfOptions = {
  wasmUrl: string; // e.g. "/wasm/qpdf-wasm"
  input: ArrayBuffer;
  args: string[]; // qpdf CLI args
  inputPath?: string; // default "/input.pdf"
  outputPath?: string; // default "/output.pdf"
  print?: (msg: string) => void; // stdout
  printErr?: (msg: string) => void; // stderr
};
 
export async function runQpdf({
  wasmUrl,
  input,
  args,
  inputPath = "/input.pdf",
  outputPath = "/output.pdf",
  print,
  printErr,
}: RunQpdfOptions): Promise<Uint8Array> {
  const qpdf = await createModule({
    locateFile: () => wasmUrl,
    print,
    printErr,
  });
 
  qpdf.FS.writeFile(inputPath, new Uint8Array(input));
  qpdf.callMain([...args, "--", outputPath]);
 
  return qpdf.FS.readFile(outputPath);
}

Common PDF operation recipes

In most cases, anything you can do with qpdf can also be done with qpdf-wasm. Typical examples include page extraction and password settings.

1) Extract pages

This example extracts pages 3–5.

const out = await runQpdf({
  wasmUrl: "/wasm/qpdf-wasm",
  input,
  args: ["/input.pdf", "--pages", ".", "3-5"],
});

2) Web optimization (linearize)

--linearize restructures the PDF so it can start rendering before the full download completes.

const out = await runQpdf({
  wasmUrl: "/wasm/qpdf-wasm",
  input,
  args: ["/input.pdf", "--linearize"],
});

3) Set a password

--encrypt starts encryption options in the official CLI. In practice, you can use it exactly as you would with the qpdf CLI: https://qpdf.readthedocs.io/en/stable/cli.html#encryption

// Example: replace passwords and permissions as needed.
const out = await runQpdf({
  wasmUrl: "/wasm/qpdf-wasm",
  input,
  args: [
    "/input.pdf",
    "--encrypt",
    "user-password",
    "owner-password",
    "256",
    "--", // many qpdf options belong before the final --
  ],
});

4) Remove a password

Using --decrypt returns a PDF with the password removed.

const out = await runQpdf({
  wasmUrl: "/wasm/qpdf-wasm",
  input,
  args: [
    "/input.pdf",
    "--password=YOUR_PASSWORD", // provide password if required
    "--decrypt",
  ],
});

5) Remove restrictions (remove-restrictions)

--remove-restrictions removes restrictions related to encryption/signatures, and can be used with --decrypt.

const out = await runQpdf({
  wasmUrl: "/wasm/qpdf-wasm",
  input,
  args: [
    "/input.pdf",
    "--password=YOUR_PASSWORD",
    "--decrypt",
    "--remove-restrictions",
  ],
});

Serve wasm in Vite / Next.js

Where to place qpdf-wasm

The simplest option is to place it in public/wasm/qpdf-wasm and serve it via /wasm/qpdf-wasm. The README also says to serve it from public or a CDN and return the URL via locateFile.

Using it in Next.js

qpdf-wasm is browser-only, so in Next.js it’s safest to avoid SSR by using a client component or a dynamic import.

"use client";
 
import { useState } from "react";
import dynamic from "next/dynamic";
 
export default function Page() {
  const [url, setUrl] = useState<string | null>(null);
 
  async function onFile(file: File) {
    const { runQpdf } = await import("../lib/runQpdf"); // load on client
    const input = await file.arrayBuffer();
 
    const out = await runQpdf({
      wasmUrl: "/wasm/qpdf-wasm",
      input,
      args: ["/input.pdf", "--linearize"],
    });
 
    const blobUrl = URL.createObjectURL(new Blob([out], { type: "application/pdf" }));
    setUrl(blobUrl);
  }
 
  return (
    <div>
      <input
        type="file"
        accept="application/pdf"
        onChange={(e) => {
          const f = e.target.files?.[0];
          if (f) onFile(f);
        }}
      />
      {url && (
        <a href={url} target="_blank" rel="noreferrer">
          Open output PDF
        </a>
      )}
    </div>
  );
}

Summary

We covered a browser-only PDF workflow using qpdf-wasm.

With qpdf-wasm, you can complete PDF operations in the browser without uploading to a backend server, which helps preserve privacy. RayPDF uses qpdf-wasm to provide PDF tools that run fully in the browser. You can try it here.

References