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.pdfinto the FS - Run CLI args via
qpdf.callMain([...args]) - Read
output.pdffrom the FS and convert it to a Blob for download/view
Install
npm i @neslinesli93/qpdf-wasmIn 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.