Client-side PDF extraction with pdfjs-dist: a deep dive
Mozilla's pdfjs-dist library powers every PDF reader on the web. Here is what it does well, where the sharp edges are, and how we build on top of it.

pdfjs-dist is arguably the most successful open-source rendering engine of the past decade. It ships in every Firefox install, powers PDF preview in Chromium's built-in viewer, and is the foundation for hundreds of PDF SaaS products. It is also 3.4 MB gzipped and full of quiet gotchas.
The three APIs you actually use
pdfjs-dist exposes an intimidating surface. In practice, ninety percent of applications only touch three functions.
- getDocument() — parses the file and returns a Promise for a PDFDocumentProxy.
- getPage(n) — returns a PDFPageProxy for page n.
- page.getTextContent() — returns an array of text items with position, font, and string.
Rendering to a canvas adds one more: page.render({ canvasContext, viewport }). That is the entire toolkit for a document viewer.
The worker
pdfjs-dist ships two builds: a main-thread build and a worker build. The library expects you to configure workerSrc explicitly:
import * as pdfjs from "pdfjs-dist";
import workerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;Skip this and pdfjs falls back to a fake worker that runs on the main thread. Your UI locks up on any file bigger than 20 pages. Ask us how we know.
Fonts, glyphs and the CMap problem
A PDF can embed its own fonts (typically) or reference standard ones (occasionally). When it references standard fonts, pdfjs needs a Character Map (CMap) to translate glyph indices into Unicode. Those maps are 15 MB unpacked and pdfjs will silently render tofu (□□□) if it cannot find them.
We ship the CMaps as a static asset and point pdfjs at them:
pdfjs.getDocument({
data: buffer,
cMapUrl: "/pdfjs/cmaps/",
cMapPacked: true,
standardFontDataUrl: "/pdfjs/standard_fonts/",
});Text extraction quirks
getTextContent() returns items in the order the PDF's content stream places them, which is not always reading order. Multi-column layouts, hyperlinks, and page headers all arrive interleaved. Reconstructing paragraphs requires spatial clustering on x-y position, font size, and baseline delta. There is a good academic paper on this from 2019 by Bast and Korzen; we implement a simplified version.
Rendering at 4K
If a user has a high-DPI display, rendering at CSS pixels produces a fuzzy page. Multiply viewport by devicePixelRatio and pdfjs produces crisp output at any zoom level.
Memory
Each PDFPageProxy holds onto a rendered content stream. For a 500-page document that adds up. Call page.cleanup() after you are done with a page, and pdf.destroy() when the user closes the file. Otherwise the tab OOMs on large batches.
Wrapping up
pdfjs-dist is the right foundation for any browser-based PDF workflow. Its sharp edges are documented, its performance is remarkable, and Mozilla maintains it with care. Everything nctools does with PDFs sits on top of it.
Marek Novák
Engineering Lead at nctools
Keep reading
Engineering
OCR in the browser: how Tesseract.js and WebAssembly changed everything
A deep look at running the world's most-used open-source OCR engine at 200+ MB of trained models directly in a user's tab.
ReadEngineering
Building a zero-upload SaaS: architecture notes
How we structure a product where 90 percent of user activity never touches our server, and what that means for our monitoring, billing and support.
ReadEngineering
PDF to Excel — heuristics for table detection in the wild
PDFs describe positions, not tables. Turning that into rows and columns is an ancient computer-science problem. Here is how we approach it.
Read