>
Software

building a client-side PDF inverter with PDF.js and PDF-lib

Most PDF tooling on the web falls into two camps. There is the heavy desktop
suite with monthly pricing and a server somewhere in the middle, and there is
the free online “convert your file” service that asks you to upload the
document. A color inverter is the kind of tool that should not need either of
those. The inversion math is a few lines per pixel, and a modern browser can
do the whole thing without a round trip.

The walkthrough below builds that tool from scratch. It is roughly the same
size as a small weekend project (an index page, a stylesheet, and a single
script), and the result is a PDF color inverter that lives in your browser
tab. Nothing is uploaded, the entire pipeline runs on the client, and the
output is a freshly generated PDF you can save with one click.

What the tool actually does

Inverting a PDF is more interesting than it sounds. Each pixel on each page
gets its red, green, and blue channels transformed by subtracting the value
from 255, and the alpha channel is preserved. Light areas become dark, dark
areas become light, and the document structure stays the same. The math is
trivial; the interesting part is fitting PDF rendering, pixel manipulation,
and PDF generation into a single page without freezing the UI.

The browser-side toolchain makes this straightforward:

  • PDF.js renders each page onto an HTML canvas (a drawable bitmap surface in the browser). It runs in a Web Worker (a background thread the page spawns so heavy parsing does not block the main UI), which keeps the page responsive even on a 200-page document.
  • The Canvas API exposes pixel data, which JavaScript can read, modify, and write back.
  • PDF-lib reassembles the modified pages into a fresh PDF that the user can preview and download.

The combination means the file never leaves the user’s device. There is no
server endpoint to maintain, no quota to bump, and no privacy story to write
in the footer.

Project shape and the libraries you actually need

The whole project fits in one directory: an index.html, a style.css, a
script.js, the PDF.js worker file, and an assets folder if you want a
favicon. Two CDN-hosted libraries handle the heavy lifting:

  • pdf.js (Mozilla’s PDF rendering library, available via cdnjs) parses the PDF and renders each page to a canvas. The library includes a Web Worker variant (pdf.worker.min.js) that does the parsing off the main thread.
  • pdf-lib (available via unpkg) takes the modified canvases and emits a brand-new PDF.

Wire the libraries in the HTML head before loading the application script,
then point PDF.js at its worker file. Skipping the worker config is the most
common reason a first attempt silently freezes on the upload step, because
the parser runs on the main thread and stalls the page until it finishes.

A short list of the moving parts to keep in mind before you write a line:

  • The original PDF bytes are preserved untouched, so you can render any page on demand and re-render after settings change.
  • The inversion runs over a copy of the canvas data, never the canvas the user is currently looking at, so flipping the live preview on does not destroy the rendered state.
  • The output PDF is a new document, not a modified version of the input, which keeps the original around for download or comparison.

How the inversion actually works at the pixel level

A pixel in the canvas is four numbers between 0 and 255 (red, green, blue,
alpha). Inversion is the simple subtraction of each color channel from 255,
with alpha left alone. Done for every pixel on every page in the selected
range, this gives you the inverted document while keeping the page layout,
order, and dimensions intact.

In JavaScript the operation is roughly:

red   = 255 - red
green = 255 - green
blue  = 255 - blue

That is the entire algorithm. The work is moving the canvas pixel buffer
through that loop without blocking the page. Two practical habits:

  • Run the inversion loop inside requestAnimationFrame or in chunks, not as a single synchronous pass, so the user can still scroll and click while the tool is processing a long document.
  • Pull the pixel buffer once, invert in place, and put it back. Repeatedly calling getImageData and putImageData on the same canvas is the kind of micro-mistake that turns a 30-millisecond operation into a 3-second one.

Settings worth wiring up

A few controls cover most of what users actually want:

  • Inversion mode. A simple dropdown with options for full invert, grayscale invert (which keeps the page monochrome), and threshold invert (which flips only pixels above a brightness cutoff, useful for documents with colored highlights).
  • Page range. Two number inputs that bound the inversion. Most users want “all pages,” but range support means a designer can invert just the cover and the inside spread.
  • Output quality. A dropdown that scales the canvas resolution before the PDF is generated. Higher quality means larger output files; lower quality keeps the file small enough to email.
  • Live preview. A toggle that re-runs the inversion whenever the user changes a setting, so they see the result before they click generate.

The settings panel only appears after the user has uploaded a PDF, which
keeps the initial screen simple and avoids the “what do these controls do”
problem on first load.

Performance tips that actually pay off

Inverting a 50-page PDF at 2x resolution is roughly 50 render passes plus
50 inversion loops, and each pass touches millions of pixels. A few habits
keep the tool responsive on older laptops:

  • Render each page only when it is on screen, not all of them up front. Most users will only look at a handful of pages before clicking generate.
  • Use OffscreenCanvas if the browser supports it. That moves the canvas work into a worker entirely, which leaves the main thread free for the controls.
  • Skip pages the user did not select. The math is cheap but the rendering is not, and rendering 50 pages to invert 10 is the single biggest waste in the typical pipeline.
  • Lower the canvas scale during preview and only render at full quality when the user clicks generate. Previewing at half resolution and saving at full resolution is the right default.

Common mistakes worth flagging

The walkthrough has a few classic traps:

  • Forgetting to configure the PDF.js worker. The library will still work, but the parser runs on the main thread and the page becomes unresponsive on large PDFs.
  • Re-using the same canvas for both the preview and the inversion source. Once you overwrite the pixel data, the preview stops matching the original and the user cannot compare.
  • Forgetting to preserve the alpha channel. Subtracting alpha from 255 makes semi-transparent overlays go solid black, which is rarely what the user wanted.
  • Not handling password-protected PDFs. PDF.js supports a password option on the getDocument call; ignoring it leaves a class of documents silently broken.
  • Writing the output PDF with the same filename as the input. Users overwrite their source file more often than you would think.

Trade-offs

The first trade is capability versus file size. Running entirely in the
browser means you can ship a single static page, but you cannot process
hundred-page documents at full resolution without putting the user in
front of a progress bar. The second trade is feature surface versus
maintenance. PDF.js and PDF-lib are well-maintained, but pinning their
versions matters because the canvas API and the PDF spec both move slowly
in different directions, and a major-version bump can quietly change how
pages render. The third trade is convenience versus privacy. A server-side
PDF inverter is faster on large documents and can stream progress back,
but the user has to trust the operator with the file. The browser-side
tool is slower per document but the file never leaves the machine.

What I would tell past me

Build the upload-and-preview path first, get one page rendering on screen,
then add the inversion pass. The temptation is to start with the inversion
loop because it is the interesting bit, but the time you spend debugging
worker configuration and canvas sizing will dwarf the time you spend on the
color math. The whole project is roughly 300 lines of JavaScript once the
libraries are wired in, and most of the polish happens after that first
working render.

A few practical notes from real setups:

  • The pdf.worker.min.js file needs to be served from the same origin as the page, not from the cdnjs CDN, or the worker will fail to load under most browsers’ same-origin rules.
  • The “live preview” toggle is the single feature users notice most. Even a half-second delay between changing a setting and seeing the result is enough to make the tool feel sluggish.
  • PDF-lib is happy to write a PDF at any canvas resolution, but the output file size scales linearly with the pixel count. A 2x render of a 200-page PDF can be hundreds of megabytes; warn before generate.
  • Hosting on a static site (GitHub Pages, Netlify, Cloudflare Pages) is enough; there is no server to run, no database to provision, and the only moving parts are the two libraries and your static assets.

Leave a comment