npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@soulehshaikh99/qpdf-wasm

v0.4.1

Published

QPDF compiled to WASM

Readme

@soulehshaikh99/qpdf-wasm · NPM Version License: ISC

QPDF 12.4.2 compiled to WebAssembly. Runs in the browser, in Web Workers and in Node.js, with no server and no native binaries.

Use it to merge, split, rotate, encrypt, decrypt, linearize, repair and inspect PDFs entirely client side.

This is a maintained fork of neslinesli93/qpdf-wasm, created by Tommaso Pifferi. All credit for the original build pipeline, JS glue and packaging goes to the original author. This fork keeps the same API and updates the toolchain and bundled libraries to their latest releases.


Table of contents


What this fork offers

The upstream package was last built against qpdf 12.2.0 and Emscripten 3.1.74. This fork rebuilds everything on current releases so you get the latest qpdf fixes and up-to-date compression and image libraries.

| Component | Upstream (@neslinesli93/qpdf-wasm 0.3.0) | This fork (@soulehshaikh99/qpdf-wasm 0.4.0) | | ----------------- | ------------------------------------------ | ------------------------------------- | | qpdf | 12.2.0 | 12.4.2 | | Emscripten (emsdk)| 3.1.74 | 6.0.13 | | zlib | pinned commit (pre 1.3.2) | 1.3.2 | | libjpeg-turbo | ImageMagick mirror (older) | 3.2.1 (official libjpeg-turbo/libjpeg-turbo) | | Node.js (build/test image) | 22 | 24 | | Node.js (minimum runtime) | not stated | 20 (engines, -sMIN_NODE_VERSION) |

Other changes:

  • Final link step uses em++ instead of emcc, as needed for the C++ qpdf sources on newer Emscripten.
  • Removed the -s WASM_BIGINT=1 flag. BigInt integration is on by default in current Emscripten.
  • Updated the libjpeg-turbo patch for its new source layout (src/jchuff.c).
  • .gitattributes forces LF line endings on *.sh, *.patch and Dockerfile, so the Docker build also works from a Windows checkout.
  • Fix: libqpdf is compiled with -fexceptions. Upstream built it without exception catching, so the catch blocks inside libqpdf were stripped and password checks on encrypted files and damaged-file recovery (for example a broken xref) aborted instead of working.
  • qpdf messages now start with qpdf: instead of the host script name (e.g. mocha: in Node, ./this.program in browsers). Pass thisProgram to the factory to use another name.
  • noFSInit is no longer an accepted factory option.
  • ENV (already exported upstream) is now in the TypeScript declarations.
  • qpdf built as a Release static library, and the Docker and build warnings cleared.
  • Test suite grew from 2 to 11 tests (zlib, JPEG, AES-256, broken-xref recovery, linearize, in-memory FS, repeated callMain).
  • Otherwise the public API matches upstream. For most users, migrating means changing the package name.

Verified: the Node test suite (docker compose up) passes, and the build has been checked in the browser.

Migrating from upstream

- npm i @neslinesli93/qpdf-wasm
+ npm i @soulehshaikh99/qpdf-wasm
- import createModule from "@neslinesli93/qpdf-wasm";
+ import createModule from "@soulehshaikh99/qpdf-wasm";

Nothing else changes, unless you passed noFSInit (no longer accepted).


Installation

npm i @soulehshaikh99/qpdf-wasm
# or
yarn add @soulehshaikh99/qpdf-wasm
# or
pnpm add @soulehshaikh99/qpdf-wasm

The package ships three files in dist/:

| File | Purpose | | ------------- | ---------------------------------------------------- | | qpdf.js | Emscripten JS loader (factory function, MODULARIZE) | | qpdf.wasm | Compiled qpdf binary | | qpdf.d.ts | TypeScript declarations |


Quick start

Browser, with a bundler (Vite, webpack). For Node.js, see Node.js: there you omit locateFile and use fs instead of fetch.

import createModule from "@soulehshaikh99/qpdf-wasm";

const qpdf = await createModule({
  // Browser only: must return the URL where qpdf.wasm is served (see Bundler setup).
  locateFile: () => "/wasm/qpdf.wasm",
});

// Write the input into the in-memory filesystem
const input = new Uint8Array(await (await fetch("/input.pdf")).arrayBuffer());
qpdf.FS.writeFile("/input.pdf", input);

// Same arguments as the qpdf CLI: keep pages 1-2
const exitCode = qpdf.callMain(["/input.pdf", "--pages", ".", "1-2", "--", "/output.pdf"]);

// Read the result back
if (exitCode === 0 || exitCode === 3) {
  const output = qpdf.FS.readFile("/output.pdf");
  const url = URL.createObjectURL(new Blob([output], { type: "application/pdf" }));
  window.open(url);
}

Core concepts

Factory function. The default export is an async factory. Each call returns a new, independent qpdf instance with its own memory and virtual filesystem.

callMain(args) runs qpdf exactly like the qpdf CLI. Pass the arguments you would type after qpdf in a terminal. It returns the exit code:

| Exit code | Meaning | | --------- | ------------------------------------- | | 0 | Success | | 2 | Error, no usable output | | 3 | Success with warnings (output written) |

callMain never calls process.exit. Errors come back as exit codes, and qpdf's messages go to stderr (the console by default).

Virtual filesystem (FS). qpdf runs inside the Emscripten filesystem, not your real disk. You move bytes in and out with:

| Method | Use | | ------------------------------ | ----------------------------------------------------- | | FS.writeFile(path, data) | Put a Uint8Array into memory (MEMFS) | | FS.readFile(path) | Read output as a Uint8Array | | FS.mkdir(path) | Create a directory | | FS.mount(type, opts, path) | Mount NODEFS (Node) or WORKERFS (Web Worker) | | FS.unmount(path) | Unmount | | FS.chdir(path) | Change working directory | | FS.unlink(path) | Delete a file (free memory between jobs) |

Initial run. noInitialRun is already set to true by the package, so qpdf does not run when the module loads. It runs only when you call callMain.


Usage recipes

Node.js

Requires Node.js 20 or newer.

Mount a real directory with NODEFS so qpdf reads and writes your disk directly:

// main.mjs (or main.js with "type": "module" in package.json)
import { fileURLToPath } from "node:url";
import createModule from "@soulehshaikh99/qpdf-wasm";

async function run(args, dir) {
  const qpdf = await createModule();
  qpdf.FS.mkdir("/work");
  qpdf.FS.mount(qpdf.NODEFS, { root: dir }, "/work");
  qpdf.FS.chdir("/work");
  return qpdf.callMain(args);
}

const code = await run(
  ["input.pdf", "--pages", ".", "1-2", "--", "output.pdf"],
  fileURLToPath(new URL("./pdfs", import.meta.url))
);
console.log("exit code:", code);

CommonJS works too: const createModule = require("@soulehshaikh99/qpdf-wasm"). Use path.resolve(__dirname, "pdfs"), and wrap the await in an async function, because CommonJS has no top-level await.

In Node, leave out locateFile. The loader finds qpdf.wasm next to qpdf.js. A browser-style URL such as "/wasm/qpdf.wasm" resolves to the drive root (for example C:\wasm\qpdf.wasm) and fails with ENOENT.

Browser (main thread)

import createModule from "@soulehshaikh99/qpdf-wasm";
import wasmUrl from "@soulehshaikh99/qpdf-wasm/dist/qpdf.wasm?url"; // Vite; see bundler section

export async function extractPages(file /* File | Blob */, range = "1-2") {
  const qpdf = await createModule({ locateFile: () => wasmUrl });

  qpdf.FS.writeFile("/in.pdf", new Uint8Array(await file.arrayBuffer()));
  const code = qpdf.callMain(["/in.pdf", "--pages", ".", range, "--", "/out.pdf"]);
  if (code !== 0 && code !== 3) throw new Error(`qpdf failed with exit code ${code}`);

  return new Blob([qpdf.FS.readFile("/out.pdf")], { type: "application/pdf" });
}

Large PDFs block the UI thread while qpdf runs. For anything bigger than a few MB, use a Web Worker.

Web Worker with WORKERFS

WORKERFS mounts File/Blob objects read-only without copying them into wasm memory. That makes it the best option for large inputs. It works only inside a Worker.

// qpdf.worker.js
import createModule from "@soulehshaikh99/qpdf-wasm";
import wasmUrl from "@soulehshaikh99/qpdf-wasm/dist/qpdf.wasm?url";

self.onmessage = async ({ data: { file, args } }) => {
  const qpdf = await createModule({ locateFile: () => wasmUrl });

  qpdf.FS.mkdir("/in");
  qpdf.FS.mount(qpdf.WORKERFS, { blobs: [{ name: "input.pdf", data: file }] }, "/in");

  // WORKERFS is read-only, so write output to MEMFS (root)
  const code = qpdf.callMain(["/in/input.pdf", ...args, "/out.pdf"]);
  const output = code === 0 || code === 3 ? qpdf.FS.readFile("/out.pdf") : null;

  qpdf.FS.unmount("/in");
  self.postMessage({ code, output }, output ? [output.buffer] : []);
};
// main.js
const worker = new Worker(new URL("./qpdf.worker.js", import.meta.url), { type: "module" });
worker.onmessage = ({ data: { code, output } }) => {
  if (output) window.open(URL.createObjectURL(new Blob([output], { type: "application/pdf" })));
  else console.error("qpdf exit code", code);
};
worker.postMessage({ file: fileInput.files[0], args: ["--linearize"] });

Bundler setup

The .wasm file has to be served as a static asset, and locateFile has to return its URL.

Browsers can't resolve a bare import "@soulehshaikh99/qpdf-wasm" on their own, so you need a bundler (Vite, webpack, ...) or an import map. Serve the page from a dev server: opening it via file:// stops the wasm from loading.

Vite. Import the URL with ?url:

import wasmUrl from "@soulehshaikh99/qpdf-wasm/dist/qpdf.wasm?url";
const qpdf = await createModule({ locateFile: () => wasmUrl });

webpack 5. Use an asset module:

const wasmUrl = new URL("@soulehshaikh99/qpdf-wasm/dist/qpdf.wasm", import.meta.url).href;
const qpdf = await createModule({ locateFile: () => wasmUrl });

Or copy node_modules/@soulehshaikh99/qpdf-wasm/dist/qpdf.wasm into your public/ folder (or a CDN) and use locateFile: () => "/wasm/qpdf.wasm".

The loader includes code paths for Node. Tell webpack not to polyfill Node built-ins on the client:

// webpack.config.js
module.exports = {
  resolve: {
    fallback: { fs: false, path: false, crypto: false, process: false },
  },
};

Next.js. Same fallback, client side only. Load qpdf only in client components ("use client" or dynamic(..., { ssr: false })):

// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = { ...config.resolve.fallback, fs: false, path: false, crypto: false, process: false };
    }
    return config;
  },
};

Creating folders before the run. Use preRun to set up directories before qpdf starts:

const qpdf = await createModule({
  locateFile: () => wasmUrl,
  preRun: [(mod) => { mod.FS.mkdir("/input"); mod.FS.mkdir("/output"); }],
});

Common qpdf commands

All examples are callMain argument arrays. See the qpdf CLI docs for the full reference.

// Keep pages 1-3 and 7
["/in.pdf", "--pages", ".", "1-3,7", "--", "/out.pdf"]

// Merge several files
["--empty", "--pages", "/a.pdf", "/b.pdf", "/c.pdf", "--", "/merged.pdf"]

// Merge selected pages from different files
["--empty", "--pages", "/a.pdf", "1-2", "/b.pdf", "5,z", "--", "/out.pdf"]   // z = last page

// Split into one file per page -> /page-01.pdf, /page-02.pdf, ...
["/in.pdf", "--split-pages", "/page.pdf"]

// Rotate pages 1-3 by 90 degrees clockwise
["/in.pdf", "--rotate=+90:1-3", "/out.pdf"]

// Encrypt with AES-256
["/in.pdf", "--encrypt", "--user-password=user", "--owner-password=owner", "--bits=256", "--", "/out.pdf"]

// Remove encryption (password needed)
["--password=secret", "--decrypt", "/in.pdf", "/out.pdf"]

// Linearize ("fast web view")
["/in.pdf", "--linearize", "/out.pdf"]

// Shrink: object streams plus maximum compression
["/in.pdf", "--object-streams=generate", "--compression-level=9", "--recompress-flate", "/out.pdf"]

// Validate structure (no output file)
["--check", "/in.pdf"]

// Dump PDF structure as JSON to stdout
["--json", "/in.pdf"]

TypeScript

Types are included (dist/qpdf.d.ts) and declared for the module @soulehshaikh99/qpdf-wasm:

import createModule, { type QpdfInstance } from "@soulehshaikh99/qpdf-wasm";

const qpdf: QpdfInstance = await createModule({ locateFile: () => wasmUrl });
qpdf.FS.writeFile("/in.pdf", bytes);
const code: number = qpdf.callMain(["--check", "/in.pdf"]);

The declarations cover callMain, FS (mkdir, mount, unmount, chdir, writeFile, readFile, unlink), NODEFS and WORKERFS, plus the factory options locateFile, noInitialRun and preRun. For ?url imports in Vite, add /// <reference types="vite/client" />.


Troubleshooting

| Symptom | Fix | | ------- | --- | | failed to asynchronously prepare wasm / 404 on qpdf.wasm | locateFile returns the wrong URL. Open it in the browser and check that it serves the file. | | ENOENT: no such file or directory, open 'C:\wasm\qpdf.wasm' (Node) | You passed a browser URL to locateFile in Node. Remove locateFile (the loader finds the wasm itself) or return an absolute filesystem path. | | Incorrect response MIME type. Expected 'application/wasm' | Configure your server to send Content-Type: application/wasm for .wasm files. | | Module not found: Can't resolve 'fs' (webpack/Next.js) | Add the resolve.fallback config from Bundler setup. | | ReferenceError: window is not defined in Next.js | You are loading qpdf during SSR. Load it in a client component only. | | ErrnoError / No such file or directory | The path in callMain doesn't exist in the virtual FS. Write the file with FS.writeFile first, or check your mount point. | | FS.writeFile fails inside a WORKERFS mount | WORKERFS is read-only. Write outputs outside the mount (e.g. /out.pdf). | | Out of memory on huge files | Memory growth is enabled, but wasm32 tops out at 4 GB. Use WORKERFS for inputs, run in a Worker, and FS.unlink files you no longer need. | | Odd results when reusing one instance for many jobs | Create a new instance per job with await createModule(...). The test suite does the same. | | Exit code 3 | Output was written, but qpdf found recoverable problems. Check the console for warnings. |


Building from source

Requirements: Docker with Compose. Emscripten and the libraries are fetched inside the container.

git clone https://github.com/soulehshaikh99/qpdf-wasm.git
cd qpdf-wasm
docker compose up

This runs these steps:

  1. Builds the emscripten/emsdk:6.0.13 stage and shallow clones zlib, libjpeg-turbo and qpdf at pinned commits.
  2. Runs build.sh, which compiles zlib and libjpeg-turbo (patched, SIMD off) as static libs, then qpdf, then links dist/qpdf.js and dist/qpdf.wasm with em++.
  3. Copies the artifacts into a node:24 image. The entrypoint puts them in ./dist.
  4. Runs npm ci && npm test (mocha, tests/all.js).

When it finishes, ./dist holds a ready-to-publish build.

Key link flags in build.sh:

| Flag | Why | | ---- | --- | | -Oz -flto -fvisibility=hidden, --closure 1 | Smallest output | | -s MODULARIZE=1 | Exports a factory function, not a global | | -s ALLOW_MEMORY_GROWTH=1 | Handles PDFs of any size, up to the wasm32 limit | | -s NO_DISABLE_EXCEPTION_CATCHING=1 | qpdf relies on C++ exceptions for error handling | | -s EXPORTED_RUNTIME_METHODS=[callMain,FS,NODEFS,WORKERFS,ENV] | Public runtime API | | -lnodefs.js -lworkerfs.js | Filesystem backends for Node and Workers |

On Windows, keep the LF line endings enforced by .gitattributes. CRLF in build.sh or the patch breaks the Linux container build. build.sh compiles qpdf with -j 4. Change it if your machine has more or fewer cores.


Project layout

.
├── Dockerfile              # emsdk build stage + node test/runtime stage, pinned library commits
├── docker-compose.yml      # `docker compose up` = build + test
├── build.sh                # compiles zlib, libjpeg-turbo, qpdf and links the wasm module
├── shallow-clone.sh        # fetches one commit of a git repo (fast, reproducible)
├── entrypoint.sh           # copies built artifacts into ./dist
├── patches/
│   └── jpeg-turbo.patch    # makes libjpeg-turbo detect 32-bit word size under Emscripten
├── js/
│   ├── pre.js              # sets noInitialRun, exposes FS methods past Closure minification
│   └── post.js
├── types/
│   └── qpdf.d.ts           # TypeScript declarations (copied to dist/)
├── tests/
│   ├── all.js              # mocha tests (qpdf ops, zlib, JPEG, AES, recovery, FS)
│   └── assets/sample.pdf
└── .github/workflows/
    ├── ci.yml              # build + test on every push
    └── deploy.yml          # publish to npm when a GitHub release is created

Updating bundled versions

  1. Edit the pinned commit SHAs in Dockerfile (shallow-clone.sh <repo> <path> <sha>) and the version comments above them.
  2. To change Emscripten, bump the FROM emscripten/emsdk:<version> tag.
  3. If libjpeg-turbo changes its source layout, regenerate patches/jpeg-turbo.patch.
  4. Run docker compose up and confirm the tests pass.
  5. Bump version in package.json, update the table in What this fork offers, then create a GitHub release. deploy.yml publishes to npm (needs the NPM_TOKEN repo secret).

Credits

  • Tommaso Pifferi (@neslinesli93): original author of qpdf-wasm, including the build pipeline, Emscripten glue, tests and CI. This fork exists because of that work. If it helps you, consider starring the upstream repo.
  • Upstream contributors, for browser usage docs and the qpdf 12.2.0 update.
  • qpdf by Jay Berkenbilt and contributors: the PDF transformation library this package compiles (Apache-2.0).
  • zlib by Jean-loup Gailly and Mark Adler.
  • libjpeg-turbo.
  • Emscripten.

Fork maintained by Souleh Shaikh (@soulehshaikh99).


License

ISC. Copyright (c) Tommaso Pifferi. The bundled qpdf, zlib and libjpeg-turbo are distributed under their own licenses (Apache-2.0, zlib and BSD-style/IJG respectively).