Most PDF readers let you adjust brightness, but very few will invert every color on every page and give you a clean, downloadable file. The freeCodeCamp tutorial by Bhavin Sheth walks through building exactly that: a browser-only PDF color inverter using PDF.js (a JavaScript library for rendering PDF pages onto an HTML canvas) and PDF-lib (a library for creating and modifying PDF files in JavaScript). The interesting part is not the inversion math. It is the choice to do everything client-side and what that means for accessibility, dark mode, and review workflows.
The premise is simple. Reading a PDF with bright backgrounds for hours is tiring. Designers and developers sometimes need a quick way to preview how a document looks inverted for dark mode or to check what an accessibility-friendly version would look like. Most free online tools upload the file to a server, which is fine for public documents and unacceptable for anything confidential. The tutorial’s answer is to keep the file in the browser, do the work locally, and let the user download a freshly generated PDF. No server, no upload, no retention risk.
Color inversion itself is the easy part. Each pixel has four values: red, green, blue, and alpha (the transparency channel). To invert a color channel, subtract its value from 255. To invert a full RGB pixel, do that three times. Repeat for every pixel on every page you want to process. The result is a document where light areas become dark, dark areas become light, and the layout, page order, and dimensions are preserved.
The actual engineering work is in the four moving parts and how they hand data to each other.
How the four pieces fit together
The pipeline has a strict handoff. Each library does one job and does not know about the others.
- PDF.js renders pages to a canvas. This is the slow step. PDF.js takes a PDF binary, parses it, and paints each page onto an HTML canvas element. The canvas is now a grid of pixel values in RGBA format (four bytes per pixel: red, green, blue, alpha). The tutorial recommends running PDF.js inside a Web Worker (a JavaScript thread that runs in the background, off the main page) so the UI stays responsive while rendering happens.
- The Canvas API does the inversion. Once a page is on the canvas, the JavaScript code calls
getImageDatato read the raw pixel buffer, walks every pixel, applies the 255-minus-channel math, and writes the result back withputImageData. This is fast. On a modern laptop, inverting a single page takes tens of milliseconds. - PDF-lib assembles the output. After inversion, each modified canvas is converted back into a PDF page and combined into a new PDF file using PDF-lib’s
PDFDocumentandcopyPagesAPIs. The user gets a real PDF, not a stack of images. - A settings panel drives the choice. The user picks a page range, output quality (the canvas rendering scale), inversion mode (full invert or grayscale-invert), and whether to preview before downloading. Each setting is a parameter to the pipeline above.
The interesting design choice is keeping these four steps strictly sequential. You cannot parallelize the canvas step before the render step finishes, and you cannot skip the assembly step if you want a real PDF at the end. The tutorial handles this with a clean async/await chain.
Where the typical first attempt breaks
Three failure modes show up in nearly every build that follows this shape. None of them are bugs in the libraries. They are bugs in how the libraries are wired together.
- The PDF.js worker path is wrong. PDF.js needs a worker script to do the heavy parsing off the main thread. The CDN-hosted PDF.js looks for
pdf.worker.min.jsnext to itself by default. If your project structure puts the worker somewhere else, you have to setpdfjsLib.GlobalWorkerOptions.workerSrcexplicitly. A wrong worker path silently degrades to single-threaded rendering, which still works but freezes the UI on large PDFs. - The canvas size does not match the page. PDF.js renders at the canvas’s pixel dimensions. If you set the canvas width too narrow for the page size, you get a downscaled preview, not the real page. The fix is to scale the canvas by a quality factor so the canvas width matches the page’s pixel-equivalent width at your chosen DPI (dots per inch).
- The PDF-lib assembly forgets the canvas-to-image conversion. PDF-lib does not natively accept canvas data. The bridge is
canvas.toDataURL('image/png')followed by embedding that image as a full-page PDF page. Skip that step and you get a PDF with one blank page where your inverted content should be.
Each of these is a one-line fix once you know what to look for.
Performance tips that actually help
The tutorial covers performance, but the most useful tips are not the ones in the headline list.
- Cap the canvas size before processing. Rendering an oversized canvas for an inversion is overkill. A reasonable upper bound on canvas width is enough for screen review and produces much smaller downloads. The size cap cuts inversion time significantly on long documents.
- Process pages in batches with a progress indicator. Users will wait for an inversion if they see progress. They will close the tab if they see a spinner with no signal. A simple page counter (
Inverting page 4 of 23...) makes a long wait feel shorter. - Skip pages that are mostly images. Inverting a scanned image produces an unreadable document. If the user only wants to invert text pages, let them deselect image-heavy pages in the preview step.
- Cache the PDF.js render. If the user toggles between inversion modes (full invert, grayscale invert, brightness-only), the page rendering is identical between modes. Cache the rendered canvas and re-invert it. The tutorial does not mention this, but it is the single biggest performance win for an interactive tool.
None of these are hard to add. All of them show up in real review workflows.
Trade-offs
A browser-based PDF inverter is not a replacement for a desktop tool. It cannot process 500-page documents without freezing the tab. It does not have batch processing. It does not integrate with Acrobat or Preview. The browser sandbox also blocks reading files from the local filesystem unless the user actively picks them, which means the tool cannot auto-watch a folder for new PDFs.
For the workflow the tool is built for (one PDF at a time, dark mode preview, accessibility check, quick invert for review), the trade-off is the right one. The file never leaves the browser. There is no upload to worry about, no retention policy to read, no account to create. The cost is a measurable slowdown compared to a native app on long documents, because every pixel goes through a JavaScript loop.
Anyone reading sensitive documents on a regular basis should treat this as a baseline. If your workflow involves uploading PDFs to a free online tool to invert colors, stop and use something local. The PDF.js and PDF-lib combination gets you there with a moderate amount of JavaScript, and both libraries are permissively licensed per the official repositories, so the resulting tool can be shipped as part of a larger product without unexpected licensing issues. The tutorial walks through every step.
If you only need to invert PDFs occasionally, the hosted version of the same tool on All In One Tools is the lighter-weight choice. If you need it as a recurring part of a review pipeline, the local build is the cleaner answer, and the four-pipeline architecture above is the structure to copy.