Engineering

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.

Marek Novák · Engineering LeadFebruary 4, 2026 12 min read
Share Post LinkedIn
Client-side PDF extraction with pdfjs-dist: a deep dive

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.

  1. getDocument() — parses the file and returns a Promise for a PDFDocumentProxy.
  2. getPage(n) — returns a PDFPageProxy for page n.
  3. 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.

MN

Marek Novák

Engineering Lead at nctools