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

@disphere/pdf-reducer

v0.3.1

Published

Reduce the file size of phone-scanned PDFs by re-compressing embedded images, preserving all other content.

Readme

pdf-reducer

npm version CI license: 0BSD

Reduce the file size of PDFs and images by reducing the size and/or quality of the images. When reducing the size of images within a PDF, everything else is preserved. When reducing images directly, JPG and PNG are supported.

Images are only ever reduced, never scaled up. If the size after the call is equal to or bigger than the original, the original is returned.

Never throws an error or broken result, always returns a smaller result or the original. An onError hook can be used to diagnose problems without interrupting the call chain.

Runs in both Node.js and the browser. In Node.js it works out of the box; in the browser it needs a little bundler setup, described below.

Installation

npm install @disphere/pdf-reducer

Embedding in your application

This is the primary integration path. The public boundary is base64 string in → base64 string out.

import { reducePdf } from "@disphere/pdf-reducer";

const smallerBase64Pdf = await reducePdf(originalBase64Pdf);
import { reduceImage } from "@disphere/pdf-reducer";

const smallerBase64Image = await reduceImage(originalBase64Image);

Using it in the browser

The browser build carries no codec of its own. It uses the @jsquash codecs, which locate their .wasm payloads at runtime with new URL("…_bg.wasm", import.meta.url). Your bundler has to serve those assets and leave that lookup intact.

The second half is where it usually goes wrong. Bundlers pre-bundle dependencies by default and rewrite module URLs while doing so; the lookup then 404s, the codec never initialises, and because this library never throws you get your file back unchanged. The result is a page that looks like it works and silently does nothing.

For Vite, keeping the codecs out of dependency optimization is the whole fix:

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    exclude: ["@jsquash/jpeg", "@jsquash/png", "@jsquash/oxipng", "@jsquash/resize"],
  },
});

Two things are not needed, despite what the usual wasm-threading advice suggests:

  • No COOP/COEP headers / cross-origin isolation. @jsquash/oxipng only takes its multi-threaded path inside a Worker, so a main-thread call always loads the single-threaded build.
  • No wasm plugin. Once the packages are excluded from pre-bundling, Vite handles new URL(…, import.meta.url) natively.

A complete, runnable setup — file picker, download, and an end-to-end suite that drives a real browser — lives in example/web.

Diagnosing a silent pass-through

Because neither reducer ever throws, "returned the original" is the answer to several very different questions: the image was already optimal, the PDF was signed, the base64 was malformed — or your bundler isn't serving the @jsquash .wasm assets.

onError can be used for diagnostic purposes:

const smaller = await reduceImage(base64Image, {
  maxDimension: 192,
  onError: (err) => console.error("[pdf-reducer]", err),
});

It fires on a codec failure, an unrecognized format, malformed base64, a non-string input, and a missing encoder — and on each image the PDF reducer had to skip. It does not fire when the image simply could not be made smaller, since that is normal operation rather than a failure. An error thrown by the callback is ignored, so a broken listener cannot turn into an exception.

Custom codec

Both reducePdf() and reduceImage() accept an encoder option implementing the ImageEncoder interface, overriding the built-in default.

EncodeRequest is a union discriminated on format, so the tuning fields you get are the ones that actually apply — quality/wantGrayscale for JPEG, level for PNG:

import { reducePdf, type ImageEncoder, type EncodeRequest } from "@disphere/pdf-reducer";

const myEncoder: ImageEncoder = {
  async encode(req: EncodeRequest): Promise<EncodedImage> {
    return (...);
  },
};

const smallerBase64Pdf = await reducePdf(originalBase64Pdf, { encoder: myEncoder });

The encoder must not change the container.

Commands

The library exposes several commands for use in a shell.

Read-only inspection

The analyze-pdf command can be used to inspect a given PDF in terms of the amount of eligible images contained within and what might prevent a reduction in size. It attributes every byte to a role (content stream, image, embedded font, metadata, …), and reports the dominant contributor. Its "Embedded raster images" section lists every image XObject with the fields the re-compression gate depends on, and why each was or wasn't eligible.

npx -p @disphere/pdf-reducer analyze-pdf <input.pdf> --json

Reduce a PDF on disk

reduce-pdf reduces a PDF on disk. It writes its output to a copy, so the original is always preserved.

npx -p @disphere/pdf-reducer reduce-pdf <input.pdf> [output.pdf]
  • output.pdf defaults to <input>.reduced.pdf next to the input.
  • Prints the input size, output size, and the percentage saved. If nothing could be improved, it writes an identical copy and says so.

Reduce a JPEG or PNG image on disk

reduce-image reduces an image on disk. It writes its output to a copy, so the original is always preserved.

npx -p @disphere/pdf-reducer reduce-image <input.jpg|png> [output]
  • The output path defaults to <input>.reduced<ext> next to the input, reusing the input's extension.
  • The format is identified by magic bytes, not the file extension. Anything that is neither JPEG nor PNG is refused with exit 2 rather than silently copied.

Development

The project uses trunk-based development:

  • main is always releasable and is the source for published releases.
  • Work happens on short-lived feature/* branches, merged into main via pull request. Every push and PR runs build + test in CI (no publishing).
  • Releases are cut deliberately (a maintainer runs the release workflow with the target version); publishing is never automatic on a push to main.

Commands

  • npm run build — compile src/dist/ (JS + .d.ts + source maps).
  • npm run typecheck — strict type-check of src/ + test/ (no emit).
  • npm run typecheck:src — strict type-check of src/ (no emit).
  • npm run typecheck:test — strict type-check of test/ (no emit).
  • npm run test — run the test suite (node --test, native TypeScript type-stripping — no build needed).
  • npm run pack:local —build and pack into pack/disphere-pdf-reducer.tgz, for installing into another project without publishing.
  • npm run format — formats the code using Prettier.
  • npm run format:check — checks the code format using Prettier.
  • npm run licenses — production dependency license summary.
  • npm run licenses:checkfails on AGPL/GPL* in the production tree

Examples

example/web is an isolated project that installs the packed tarball and runs the library in a real browser. It is the only place the browser code path is exercised, and it is not part of npm test or CI — see its README for how to run it.