This guide is for the people who deploy, integrate, build, and extend PDF.js: web/application administrators embedding the viewer, platform engineers bundling pdfjs-dist, and contributors working on the engine itself. It covers the system architecture, the build pipeline, the complete configuration surface, security posture, deployment strategies, and the internals deep enough that an engineer (or an AI) could reason about — or reconstruct — the system.
For end-user and library-consumer documentation (how to call getDocument, render a page, use the viewer toolbar), see the companion User Guide.
Table of Contents
- Executive Summary
- System Architecture
- Repository Structure
- Requirements
- The Build System
- Configuration Reference
- Worker Configuration
- Deployment Strategies
- Bundler Integration
- Security
- Engine Internals (Deep Dive)
- Testing Strategy
- Operations & Maintenance
- Browser Extensions
- Troubleshooting (Operational)
- Potential Improvements & Tech Debt
- Quick Reference
1. Executive Summary
PDF.js is a pure-JavaScript PDF parsing and rendering engine. It takes raw PDF bytes and produces pixels on an HTML5 <canvas>, selectable text overlays, interactive form layers, and structured document metadata — without any native code, browser plugin, or server-side rendering.
Key architectural properties an administrator should know:
- Two-thread design. A thin main-thread API (
src/display/) drives a heavy Web Worker engine (src/core/). They communicate over a structured message protocol. This keeps host pages responsive during CPU-bound parsing. - Zero runtime dependencies. The shipped library bundles everything it needs. Every entry in
package.jsonis build/test tooling. There is no runtime npm dependency tree to audit or patch. - Self-contained, static deployment. The distribution is static files (
.mjsmodules + data folders). It needs only an HTTP server. There is no application server, database, or background service. - WebAssembly acceleration, optional. Image codecs (JBIG2, OpenJPEG/JPEG 2000) and the embedded-JavaScript sandbox (QuickJS) ship as WASM and are loaded on demand.
- Defense-in-depth for untrusted input. PDFs are untrusted by definition; the architecture isolates parsing on a worker and confines any embedded document JavaScript to a sandboxed engine.
Two consumption models:
| Model | What you ship | Who configures it |
|---|---|---|
Library (pdfjs-dist) |
pdf.mjs + pdf.worker.mjs (+ data folders) bundled into your app |
Application developers, via getDocument parameters and GlobalWorkerOptions. |
Viewer (web/) |
The full prebuilt reader (viewer.html + assets) |
Administrators, via the viewer AppOptions/preferences surface. |
2. System Architecture
2.1 The threads
MAIN THREAD WORKER THREAD
┌───────────────────────────────┐ ┌──────────────────────────────────┐
│ src/display/ (the public API)│ │ src/core/ (the parser + engine) │
│ │ │ │
│ getDocument() │ │ WorkerMessageHandler.setup() │
│ PDFDocumentProxy ───────────►│ │ PDFDocument / Catalog / XRef │
│ PDFPageProxy │ │ Parser / Lexer / evaluator │
│ RenderTask (canvas painting) │◄──┤ Fonts, images, CMaps, colors │
│ TextLayer / AnnotationLayer │ │ OperatorList builder │
└───────────────┬───────────────┘ └─────────────────┬────────────────┘
│ structured postMessage protocol │
└────────── MessageHandler ──────────┘
(src/shared/message_handler.js)
┌───────────────────────────────┐ ┌──────────────────────────────────┐
│ web/ (viewer application) │ │ QuickJS WASM sandbox │
│ PDFViewerApplication, tools │ │ src/scripting_api/ (PDF JS) │
└───────────────────────────────┘ └──────────────────────────────────┘
Main thread (src/display/). Hosts the API surface consumers call. Its objects (PDFDocumentProxy, PDFPageProxy) are proxies — they don't hold parsed data; they marshal requests to the worker and apply results (drawing operator lists onto a canvas, building text/annotation layers in the DOM).
Worker thread (src/core/). The actual PDF engine. It parses the cross-reference table and object graph, decrypts, decodes fonts and images, evaluates content streams, and emits operator lists — compact arrays of drawing instructions — back to the main thread for painting. This is the largest and most complex module (≈1.85M chars of source).
Shared (src/shared/). Code used by both threads: the MessageHandler transport, the constants/enums (OPS, AnnotationType, PermissionFlag, …), utility math, and the MurmurHash used for caching.
Scripting sandbox (src/scripting_api/). Implements the Acrobat-compatible JavaScript object model (app, doc, field, …) that PDFs can script. It runs inside a QuickJS WebAssembly engine, isolated from the host.
XFA (src/core/xfa/). A self-contained renderer for the legacy XML Forms Architecture (≈405K chars). Off by default; enabled with enableXfa.
2.2 The message protocol
src/shared/message_handler.js implements a small RPC layer over postMessage. Three call styles:
| Method | Semantics |
|---|---|
send(action, data, transfers) |
Fire-and-forget message. |
sendWithPromise(action, data) |
Request/response; resolves with the worker's reply. |
sendWithStream(action, data, strategy) |
Backpressure-aware ReadableStream — used for operator lists and text content, which arrive in chunks. |
Errors are marshalled across the boundary and re-thrown as the correct exception subclass (PasswordException, InvalidPDFException, ResponseException, AbortException, UnknownErrorException) via wrapReason().
Note
The streaming channel is what enables progressive rendering: a page's operator list streams from the worker so the main thread can begin painting before parsing finishes, and rendering can be cancelled mid-stream (the worker stops producing).
2.3 The rendering data flow
PDF bytes
└─► XRef + Catalog parse (worker)
└─► Page content stream (worker: Lexer → Parser)
└─► PartialEvaluator (worker: resolves fonts, images, color)
└─► OperatorList (worker → main, streamed)
└─► CanvasGraphics.executeOperatorList() (main)
└─► pixels on <canvas>
Fonts are converted (where possible) to OpenType and loaded via the Font Loading API; when that's disabled (disableFontFace, the Node default), a built-in glyph-path renderer draws them directly.
2.4 The fake-worker fallback
If Web Workers are unavailable (Node.js, or a CSP/security failure creating the worker), PDFWorker transparently sets up a fake worker: the same engine runs on the main thread behind a LoopbackPort that mimics the message channel. Functionality is identical; only concurrency is lost. This is why the library "just works" in Node without a worker file.
3. Repository Structure
pdf.js/
├── src/ Engine source (the library)
│ ├── pdf.js Main-thread entry → builds globalThis.pdfjsLib
│ ├── pdf.worker.js Worker entry → WorkerMessageHandler
│ ├── pdf.sandbox.js Scripting sandbox entry (QuickJS bridge)
│ ├── pdf.scripting.js Scripting initialization
│ ├── pdf.image_decoders.js Standalone image-decoder entry
│ ├── display/ Main-thread API (api.js, canvas.js, layers…) (36 files)
│ ├── core/ Parser + render engine (86 files, ~1.85M chars)
│ │ └── xfa/ XFA forms renderer (27 files)
│ ├── scripting_api/ Acrobat JS object model (18 files)
│ └── shared/ Cross-thread code: util.js, message_handler.js …
├── web/ Prebuilt viewer application (101 files)
│ ├── app.js PDFViewerApplication (viewer controller)
│ ├── app_options.js AppOptions / defaultOptions (the config surface)
│ ├── viewer.html Viewer markup
│ └── internal/ Internal viewer support modules
├── extensions/chromium/ Chrome/Chromium extension wrapper
├── examples/ Integration examples (node, webpack, components…)
├── l10n/ Localization — 130+ locales (Fluent .ftl)
├── test/ Unit (jasmine), integration (puppeteer), reference
├── docs/ Project website / generated API docs sources
├── gulpfile.mjs The build orchestrator (all build tasks)
└── package.json Dev/build tooling only — no runtime deps
Important
Three source folders dominate complexity and deserve the most review attention:
src/core/(the parser/engine),web/(the viewer), andtest/unit/(the largest test corpus, ≈1.2M chars). The localization tree (l10n/, 130+ single-file locale folders) inflates the file count but is low-complexity.
4. Requirements
4.1 Build / development environment
| Item | Requirement |
|---|---|
| Node.js | >= 22.13.0 or >= 24 (from package.json engines). |
| Package manager | npm (a package-lock.json is maintained). |
| Build driver | Gulp 5 (gulp / gulp-cli), invoked via npx gulp <task>. |
| Bundler | Webpack 5 + Babel 7 + Terser (driven by gulp). |
| Browsers for testing | Firefox and Chrome, automated through Puppeteer. |
| License | Apache-2.0. |
4.2 Runtime environment (deploying the viewer/library)
| Item | Requirement |
|---|---|
| Host | Any static HTTP server (nginx, Apache, a CDN, an object store with web hosting). |
| Protocol | Serve over http(s)://. The file:// protocol breaks workers and fetch. |
| MIME types | .mjs → text/javascript; .wasm → application/wasm; .pdf → application/pdf. |
| Browser | Modern evergreen browsers. Use the legacy/ build for older targets / Node. |
Warning
Misconfigured MIME types are the most common deployment failure. If your server serves
.mjsastext/plainor.wasmasapplication/octet-stream, module loading and WASM instantiation fail silently or with opaque errors. Verify these mappings before going live.
5. The Build System
PDF.js builds everything through a single gulpfile.mjs. Gulp 5 composes tasks as async functions with series() (sequential) and parallel() (concurrent); each task pipes source globs through Babel, PostCSS, and Webpack/Terser to an output folder.
5.1 Common tasks
# Install dev tooling first
npm install
# Discover every available task and its dependency tree
npx gulp --tasks
# Produce the generic (browser) build → build/generic/
npx gulp generic
# Produce the minified generic build
npx gulp minified
# Build the standalone components bundle
npx gulp components
# Build the image-decoders-only bundle
npx gulp image_decoders
# Build the Firefox extension / mozcentral integration
npx gulp mozcentral
# Build the Chromium extension → build/chromium/
npx gulp chromium
# Install a freshly built dist into ./node_modules (used by examples)
npx gulp dist-install
# Run the linters / formatters
npx gulp lint
# Run the test suites
npx gulp unittest
npx gulp test
Note
Task names above are the conventional PDF.js gulp targets. Always run
npx gulp --tasksagainst your checked-out revision for the authoritative, version-exact list — the gulpfile is the single source of truth and evolves with the codebase.
5.2 Build-time configuration: PDFJSDev
The source is peppered with PDFJSDev.test("...") and PDFJSDev.eval("...") guards. PDFJSDev is a compile-time preprocessor (resolved by a Babel plugin during the build), not a runtime object. It selects code paths per build target:
| Flag | Build target |
|---|---|
GENERIC |
The general-purpose browser/npm build. |
MOZCENTRAL |
The build integrated into Firefox (mozilla-central). |
CHROME |
The Chromium extension build. |
LIB |
The library/components build. |
TESTING |
Test builds (enables internal assertions and test hooks). |
GECKOVIEW |
Firefox-for-Android (GeckoView) build. |
Because these are resolved at build time, dead branches for other targets are eliminated from the output — one source tree, many tailored bundles. Constants like BUNDLE_VERSION and INTERNAL_EVT are injected via PDFJSDev.eval(...).
5.3 Build outputs
| Output | Contents |
|---|---|
build/generic/build/pdf.mjs |
Main library. |
build/generic/build/pdf.worker.mjs |
Worker engine. |
build/generic/build/pdf.sandbox.mjs |
Scripting sandbox. |
build/generic/web/ |
Prebuilt viewer + data folders (cmaps, standard_fonts, iccs, wasm). |
build/image_decoders/ |
Image-decoder-only bundle. |
build/chromium/ |
Packaged Chrome extension. |
6. Configuration Reference
There are two distinct configuration surfaces. Don't confuse them.
- Library (
getDocument) parameters — per-document options passed in code. Documented exhaustively in the User Guide → API Reference. These control parsing/rendering behavior for a single document. - Viewer options (AppOptions) — settings for the prebuilt
web/viewer, defined inweb/app_options.js. Documented below.
6.1 How viewer options work
Each option in web/app_options.js's defaultOptions is tagged with an OptionKind bitmask that determines where it applies and whether end users can change it:
OptionKind |
Meaning |
|---|---|
BROWSER |
Provided by the host browser environment (capabilities, integration flags). Not user-settable. |
VIEWER |
Controls the viewer UI/behavior. |
API |
Forwarded to the underlying getDocument call. |
WORKER |
Worker location (workerSrc, workerPort). |
PREFERENCE |
A subset of VIEWER/API options that are exposed as user-persisted preferences. |
EVENT_DISPATCH |
Changing it dispatches a runtime event. |
An option marked VIEWER + PREFERENCE is a viewer setting that can be persisted as a user preference; one marked API is passed straight to the engine.
6.2 Viewer (UI) options
| Option | Default | Effect |
|---|---|---|
annotationMode |
2 (ENABLE_FORMS) |
Which annotations render in the viewer. |
annotationEditorMode |
0 |
Initial annotation-editor mode (-1 disables editing). |
cursorToolOnLoad |
0 |
0 = select tool, 1 = hand tool. |
defaultZoomValue |
"" |
Initial zoom (e.g. "page-width", "auto", "150"); empty = automatic. |
defaultZoomDelay |
400 |
Debounce (ms) before applying zoom changes. |
scrollModeOnLoad |
-1 |
Scroll mode (-1 = use document/default). |
spreadModeOnLoad |
-1 |
Page-spread mode at load. |
sidebarViewOnLoad |
-1 |
Which sidebar panel opens initially. |
viewOnLoad |
0 |
Whether to restore the last view position. |
textLayerMode |
1 |
Text layer: 0 off, 1 on, 2 on + enhanced. |
viewerCssTheme |
0 |
UI theme (0 automatic, light, or dark per build). |
toolbarDensity |
0 |
0 normal, 1 compact, 2 touch. |
externalLinkTarget |
0 |
Where external links open (new tab, same, etc.). |
externalLinkRel |
"noopener noreferrer nofollow" |
rel applied to external links. |
disablePageLabels |
false |
Ignore custom page labels. |
disableHistory |
false |
Don't update browser history on navigation. |
historyUpdateUrl |
false |
Reflect the current page/position in the URL. |
printResolution |
150 |
Print rasterization DPI. |
enablePrintAutoRotate |
true |
Auto-rotate landscape pages when printing. |
maxCanvasPixels |
2**25 |
Per-canvas pixel ceiling (prevents huge allocations). |
maxCanvasDim |
32767 |
Maximum canvas dimension. |
imageResourcesPath |
"./images/" |
Path to viewer UI images. |
6.3 Editor / feature-flag options
| Option | Default | Effect |
|---|---|---|
enableScripting |
true (non-Chrome) |
Allow embedded PDF JavaScript execution. |
enableComment |
dev-only | Enable the comment editor. |
enableSignatureEditor |
test/dev | Enable the signature editor. |
enableAltText |
false |
Image alt-text authoring. |
enableGuessAltText |
true |
ML-assisted alt-text suggestions. |
enableAltTextModelDownload |
true |
Allow downloading the alt-text ML model. |
enableAutoLinking |
true |
Detect and linkify URLs in text. |
enableHighlightFloatingButton |
dev/test | Floating highlight button. |
enablePermissions |
false |
Honor the PDF's declared permission flags in the UI. |
enableHWA |
true |
Hardware-accelerated canvas (API option). |
enableWebGPU |
true |
Attempt WebGPU acceleration. |
highlightEditorColors |
yellow/green/blue/pink/red set | Highlighter palette (incl. high-contrast variants). |
forcePageColors / pageColorsBackground / pageColorsForeground |
false / Canvas / CanvasText |
High-contrast color overrides. |
6.4 API-forwarded options (viewer → engine)
These viewer options are passed through to getDocument:
| Option | Default |
|---|---|
cMapUrl |
build-relative cmaps/ path |
cMapPacked |
true |
standardFontDataUrl |
build-relative standard_fonts/ path |
iccUrl |
build-relative iccs/ path |
wasmUrl |
build-relative wasm/ path |
disableAutoFetch |
false |
disableFontFace |
false |
disableRange |
false |
disableStream |
false |
enableXfa |
true (in the viewer) |
maxImageSize |
-1 (no limit) |
useSystemFonts |
environment-dependent |
verbosity |
1 (WARNINGS) |
pdfBug |
false |
docBaseUrl |
current document URL |
Tip
To harden a kiosk/embedded viewer, the highest-leverage changes are:
enableScripting: false(no embedded JS),enablePermissions: true(respect author restrictions), a fixeddefaultZoomValue, and disabling editing viaannotationEditorMode: -1.
7. Worker Configuration
The worker location is mandatory for the library and is set through GlobalWorkerOptions (one of these two):
// Option A: point at the worker file (most common)
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.min.mjs";
// Option B: provide a preconstructed Worker/port (advanced; e.g. preloaded)
pdfjsLib.GlobalWorkerOptions.workerPort = new Worker("/pdfjs/build/pdf.worker.min.mjs", { type: "module" });
Setter validation (from source): workerSrc must be a string, workerPort must be a Worker instance (or null) — otherwise the setters throw Invalid \workerSrc` type./Invalid `workerPort` type.`
Same-origin handling. When workerSrc is cross-origin (e.g. a CDN), the GENERIC build automatically wraps it in a Blob URL (_createCDNWrapper) so the worker still satisfies the browser's same-origin worker rules. In MOZCENTRAL builds, the worker is preloaded by the host (workerPort via pdfjsPreloadedWorker).
Caution
A Content-Security-Policy that forbids
worker-src/blob:will block real workers and silently force the fake-worker fallback (everything runs on the main thread). If you seeWarning: Setting up fake worker.in production, inspect your CSP first.
8. Deployment Strategies
8.1 Static viewer deployment (simplest)
- Build (
npx gulp generic) or download the prebuilt package. - Copy
build/generic/web/andbuild/generic/build/to your web root. - Ensure correct MIME types for
.mjsand.wasm. - Open
web/viewer.html?file=<same-origin-pdf-url>.
# nginx: serve PDF.js assets with correct types
types {
text/javascript mjs;
application/wasm wasm;
application/pdf pdf;
}
location /pdfjs/ {
alias /var/www/pdfjs/;
add_header Cache-Control "public, max-age=31536000, immutable";
}
8.2 Library deployment (embed in your app)
Install pdfjs-dist, bundle pdf.mjs, ensure pdf.worker.mjs and the data folders ship to your output, and set workerSrc + the asset URLs at runtime. See Bundler Integration.
8.3 CDN deployment
Reference both library and worker from the same pinned version:
const V = "5.x.y"; // pin one version for both
import * as pdfjsLib from `https://cdn.jsdelivr.net/npm/pdfjs-dist@${V}/build/pdf.min.mjs`;
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${V}/build/pdf.worker.min.mjs`;
Warning
Version skew between
pdf.mjsandpdf.worker.mjsis a top production incident. The API checks its version against the worker's and throws if they differ. Pin both to one version; never mix a CDN worker with a locally bundled library.
8.4 Verification checklist
- [ ]
.mjsserved astext/javascript,.wasmasapplication/wasm. - [ ]
pdf.worker.mjsloads directly in the browser (returns JS, not 404). - [ ] Library and worker are the same version.
- [ ] Asset folders (
cmaps/,standard_fonts/,iccs/,wasm/) reachable and pointed at by the four URLs. - [ ] Cross-origin PDFs have CORS headers (or are proxied same-origin).
- [ ] CSP permits the worker (and
blob:if cross-origin worker). - [ ]
enableScriptingset deliberately for your threat model.
9. Bundler Integration
The repository ships a reference webpack setup at examples/webpack/. The essential concerns when bundling pdfjs-dist:
-
Worker reachability. The worker is a separate file. Either set
workerSrcto a path your bundler emits, or use the bundler's worker support. Webpack 5 understandsnew Worker(new URL(...), { type: "module" })natively and will code-split the worker. -
Copy the data folders. Use
copy-webpack-pluginto emitcmaps/,standard_fonts/,iccs/, andwasm/into your output, then setcMapUrl,standardFontDataUrl,iccUrl,wasmUrlto those output paths.
// webpack.config.js (essentials)
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
// …
plugins: [
new CopyPlugin({
patterns: [
{ from: "node_modules/pdfjs-dist/cmaps/", to: "cmaps/" },
{ from: "node_modules/pdfjs-dist/standard_fonts/", to: "standard_fonts/" },
{ from: "node_modules/pdfjs-dist/wasm/", to: "wasm/" },
],
}),
],
};
// app code
import * as pdfjsLib from "pdfjs-dist";
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.mjs", import.meta.url
).toString();
const pdf = await pdfjsLib.getDocument({
url, cMapUrl: "/cmaps/", cMapPacked: true,
standardFontDataUrl: "/standard_fonts/", wasmUrl: "/wasm/",
}).promise;
Note
Framework starters (React, Vue, Angular, Vite, Next.js) follow the same two rules — emit the worker, copy the data folders. The mechanics differ per bundler, but
examples/webpack/andexamples/components/are the canonical references.
10. Security
PDF.js treats every PDF as untrusted input and is engineered accordingly. Administrators should understand the layers.
10.1 Threat model
A malicious PDF may attempt: malformed structures to trigger parser bugs, hostile embedded JavaScript, abusive external links, decompression bombs (huge images), or data exfiltration via form-submit actions. PDF.js mitigates each.
10.2 Isolation layers
| Layer | Mechanism |
|---|---|
| Parsing isolation | All parsing/decoding runs on a Web Worker, separate from the host page's main context. |
| Embedded-JS sandbox | PDF JavaScript runs in a QuickJS WebAssembly engine (src/scripting_api + pdf.sandbox.js). The sandbox communicates only through a narrow string-serialized bridge (SandboxSupportBase) exposing a fixed allowlist: timers, alert/confirm/prompt, URL parsing, and a send event channel. It has no direct DOM, network, or filesystem access. |
| Recovery over crash | By default (stopAtErrors: false) the engine recovers from malformed data rather than aborting, but it caps work (e.g. maxImageSize, maxCanvasPixels) to resist resource-exhaustion attacks. |
10.3 Link and form-submission safety
- External links carry
rel="noopener noreferrer nofollow"by default (externalLinkRel), preventing reverse-tabnabbing and SEO abuse. externalLinkTargetcontrols where links open.- Form-submit and similar actions are gated by the scripting layer; disable
enableScriptingto neutralize document-driven actions entirely.
10.4 Permissions
PDFDocumentProxy.getPermissions() exposes the document's declared permission flags (PermissionFlag: PRINT, COPY, MODIFY_CONTENTS, …). These are advisory — PDF.js surfaces them so integrators can enforce policy. The viewer honors them when enablePermissions: true.
10.5 Administrator hardening recommendations
Important
For untrusted-document deployments (e.g. user uploads viewed by others):
- Set
enableScripting: falseunless you specifically need PDF JavaScript.- Serve the viewer/library from your own origin under a strict CSP that still permits the worker.
- Keep
maxImageSize/maxCanvasPixelsat sane limits to bound memory.- Serve user PDFs from a separate origin/sandbox where feasible.
- Keep PDF.js current — parser hardening fixes land continuously.
10.6 Reporting vulnerabilities
PDF.js is a Mozilla project; security issues should be reported through Mozilla's responsible-disclosure process rather than as public issues.
11. Engine Internals (Deep Dive)
This section is for engine contributors and anyone reconstructing the system.
11.1 Document model (worker side, src/core/)
- XRef parses the cross-reference table/streams to locate every indirect object; handles incremental updates and rebuilds a broken table when needed.
- Catalog reads the document root: page tree, named destinations, outlines, page labels, attachments, optional content groups, viewer preferences, and the open action.
- PDFDocument ties it together and answers high-level requests (
numPages, fingerprints, metadata, permissions). - Parser / Lexer tokenize content streams and object data.
- Decryption handles standard security handlers (RC4/AES) keyed by the user/owner password.
11.2 Content evaluation → operator lists
PartialEvaluator walks a page's content stream and produces an OperatorList: parallel fnArray (operator codes from the OPS enum) and argsArray (their arguments). Operators cover paths (moveTo, lineTo, curveTo, fill, stroke, clip), text (beginText, showText, setFont, setTextMatrix), color (setFillRGBColor, setStrokeCMYKColor, …), images (paintImageXObject, paintImageMaskXObject, inline images), shadings, marked content, and annotation boundaries. The list streams to the main thread in chunks.
Note
The
OPSenum is deliberately append-only — removed operators stay commented out and numbers are never reused — so third-party tools that read operator lists don't break across versions. A parallel compactDrawOPSenum exists for hot-path path construction.
11.3 Canvas execution (main side, src/display/canvas.js)
CanvasGraphics consumes the operator list and issues the corresponding HTML5 Canvas 2D calls, managing the graphics-state stack, clipping, soft masks (beginSMaskMode/endSMaskMode), patterns/shadings, transforms, and image painting. InternalRenderTask drives this incrementally via requestAnimationFrame (so rendering yields to the browser) and supports cancellation with a deferred timeout to avoid thrashing on rapid re-renders.
11.4 Fonts
The font subsystem (in src/core/) parses embedded font programs (TrueType, CFF/Type1/Type2, Type3, CIDFonts) and, where possible, repackages them as OpenType for the browser's Font Loading API. FontLoader (display side) installs them. When font-face loading is disabled, glyphs are rasterized from primitive path commands. The standard-14 fonts are filled from the standard_fonts/ data folder when not embedded.
11.5 Images and color
Image XObjects and inline images are decoded on the worker. Codecs include JBIG2, CCITT fax, JPEG (built-in), and JPEG 2000 (OpenJPEG via WebAssembly, gated by useWasm). OffscreenCanvas/ImageDecoder are used when available to offload conversion. Color conversion utilities (ColorConverters in src/shared/scripting_utils.js) map between Gray/RGB/CMYK/HTML. image_utils.js provides fast bit-twiddling paths for 1-bpp and RGB→RGBA conversion with endianness awareness.
11.6 Annotations and forms
Annotations are parsed on the worker into data objects; the display side renders them either onto the canvas (appearance streams) or as interactive HTML (AnnotationLayer) for links and widgets. Form field state lives in an AnnotationStorage; saveDocument() serializes it back into a new PDF, and ENABLE_STORAGE rendering reflects live values (used for printing/saving).
11.7 The scripting sandbox
pdf.scripting.js exposes pdfjsScripting.initSandbox inside QuickJS. pdf.sandbox.js (SandboxSupport extends SandboxSupportBase) JSON-serializes values across the WASM boundary and wires the externals (timers, dialogs, URL parsing, the updatefromsandbox event). The Acrobat object model lives in src/scripting_api/ (≈132K chars): app, doc, field, color, event, and AForm calculation functions, plus the date/time format tables.
11.8 XFA
src/core/xfa/ is an independent renderer for XFA (XML Forms Architecture) documents, producing a fake-DOM tree that the display-side XfaLayer turns into HTML. It's substantial (27 files, ≈405K chars) and disabled unless enableXfa is set. PDFDocumentProxy.isPureXfa indicates a document with only an XFA form.
12. Testing Strategy
| Suite | Tooling | Location | What it covers |
|---|---|---|---|
| Unit tests | Jasmine | test/unit/ (≈1.2M chars, 69 files) |
Parser, fonts, CMaps, primitives, API surface, utilities. |
| Integration / browser | Puppeteer | test/integration/ |
End-to-end behavior in real Firefox/Chrome (viewer, editor, forms). |
| Reference (regression) tests | Custom harness | test/pdfs/, test/stats/ |
Pixel-comparison rendering against a large PDF corpus (456 test PDFs) to catch rendering regressions. |
| Font tests | Jasmine | test/font/ |
Font parsing/conversion correctness. |
| Bidi tests | — | test/bidi/ |
Bidirectional (RTL) text handling. |
| Type tests | TypeScript | test/types/ |
The generated pdfjs-dist type declarations compile and resolve. |
npx gulp unittest # Jasmine unit tests (also runnable in-browser & via CLI)
npx gulp test # Full reference/regression run
npx gulp lint # ESLint + Stylelint + Prettier + svglint
The unit tests can run both in a browser harness and headless via a CLI helper (test/unit/clitests_helper.js), with coverage output controlled by UNITTESTCLI_COVERAGE_FILE.
Tip
The reference-test corpus is the project's safety net: any rendering change is validated against hundreds of real PDFs by image diff. When contributing engine changes, expect to update or add reference baselines deliberately, never blindly.
13. Operations & Maintenance
13.1 Observability
PDF.js logs through a verbosity gate (VerbosityLevel: ERRORS/WARNINGS/INFOS). Set verbosity per document (library) or via the verbosity option (viewer). Warning:-prefixed lines are non-fatal; Info: lines are dev-oriented. Enable pdfBug: true to expose internal debugging hooks (web/debugger.js) — for development only.
13.2 Performance levers
| Lever | Effect |
|---|---|
Progressive loading (disableRange/Stream/AutoFetch = false) |
Render early pages before full download; cuts time-to-first-page on large remote PDFs. |
enableHWA / enableWebGPU |
Offload canvas work to the GPU. |
useWasm |
WASM image decoding (faster JPEG 2000/JBIG2). |
OffscreenCanvas / ImageDecoder |
Off-main-thread image conversion when supported. |
page.cleanup() / pdf.cleanup() |
Reclaim memory between pages/documents. |
maxCanvasPixels / maxImageSize |
Bound memory for very large pages/images. |
Shared PDFWorker |
Avoid spawning a worker per document when handling many. |
13.3 Upgrades
Because there are no runtime dependencies, upgrading is replacing the static assets. The two rules: (1) upgrade pdf.mjs and pdf.worker.mjs together, and (2) re-copy the data folders (cmaps/, standard_fonts/, iccs/, wasm/) so they match the new engine. Review release notes for viewer-option changes if you persist preferences.
13.4 Localization
The viewer is localized via Fluent (@fluent/bundle, @fluent/dom) with .ftl files under l10n/ (130+ locales). localeProperties / the host locale selects the active translation. Custom builds can prune locales to reduce size.
14. Browser Extensions
extensions/chromium/ packages PDF.js as a Chrome/Chromium extension that replaces the browser's native PDF handling with the PDF.js viewer. It is built with npx gulp chromium and carries its own manifest, options page (extensions/chromium/options/), and content scripts. The CHROME PDFJSDev flag tailors the engine for this target (e.g. different default viewerCssTheme, telemetry handling, and enableScripting defaults). The Firefox integration is produced by the mozcentral build and lives in mozilla-central rather than as a separate extension.
15. Troubleshooting (Operational)
Issue: Viewer/library forces the fake worker in production
Symptom. Warning: Setting up fake worker.; UI sluggish under load.
Causes & fixes. (1) workerSrc unset or 404 — verify the path. (2) CSP blocks worker-src/blob: — relax the policy for the worker. (3) Cross-origin worker without the Blob wrapper — use the GENERIC build or host the worker same-origin.
Issue: The API version does not match the Worker version
Cause. Mixed versions of pdf.mjs and pdf.worker.mjs.
Fix. Serve both from one pdfjs-dist version; re-copy assets on upgrade.
Issue: WASM features fail (slow/incorrect JPEG 2000 or JBIG2 images)
Cause. wasm/ folder not deployed, wrong wasmUrl, or .wasm served with the wrong MIME type.
Fix. Copy wasm/, set wasmUrl, ensure application/wasm. As a fallback the engine can decode without WASM but more slowly.
Issue: CJK / non-embedded-standard-font documents render with missing glyphs
Cause. cmaps/ or standard_fonts/ not deployed / not configured.
Fix. Deploy both folders and set cMapUrl (+ cMapPacked: true) and standardFontDataUrl.
Issue: Cross-origin PDFs fail to load
Cause. Same-origin policy / missing CORS.
Fix. Enable CORS on the file host, proxy same-origin, and set withCredentials when auth cookies are required.
Issue: High memory usage / tab crashes on large PDFs
Cause. Many uncleaned pages, or oversized pages/images.
Fix. Call page.cleanup()/pdf.cleanup(), lower maxCanvasPixels/maxImageSize, and keep progressive loading on.
Issue: Module scripts don't load at all
Cause. .mjs served as text/plain/application/octet-stream.
Fix. Map .mjs → text/javascript on the server.
16. Potential Improvements & Tech Debt
Observations from the source, framed for maintainers:
src/core/concentration. The engine module is very large (≈1.85M chars across 86 files). Continued decomposition (parser vs. evaluator vs. codecs) eases review and onboarding.- Compile-time forking via
PDFJSDev. The manyPDFJSDev.test(...)branches are powerful but increase cognitive load; the build-target matrix (GENERIC/MOZCENTRAL/CHROME/LIB/GECKOVIEW/TESTING) must be kept in mind when reading any file. MathClampshim.src/shared/math_clamp.jscarries a TODO to be removed once the TC39Math.clampproposal ships — a deliberate, tracked piece of forward-looking debt.- Feature flags awaiting graduation. Several editor features (comments, signatures, split/merge, updated add-image, new alt-text) are gated to dev/test builds pending Firefox-release experiments; their
enable*defaults are explicitly marked temporary inapp_options.js. - Known open issues. The community tracker reports active rendering edge cases — font rendering at shape intersections (#21353), Edge bullet rendering (#21303), toolbar dismissal after page deletion (#21241), RU-keyboard
Ctrl+Z(#18856), Romanian-tax-authority XFA documents (#17510), and Firefox form submit/export actions (#13266). These are good orientation points for the parser/viewer/XFA subsystems respectively. - Unused-l10n / Babel cleanups. Tracked housekeeping (#21270 unused l10n ids, #21337 remove unused top-level constants via the Babel plugin) — low-risk maintenance.
17. Quick Reference
17.1 Build commands
npm install # install dev tooling
npx gulp --tasks # list all tasks (authoritative)
npx gulp generic # general browser/npm build
npx gulp minified # minified build
npx gulp chromium # Chrome extension
npx gulp mozcentral # Firefox integration
npx gulp dist-install # install built dist into node_modules
npx gulp lint # linters/formatters
npx gulp unittest # Jasmine unit tests
npx gulp test # reference/regression tests
17.2 Critical runtime setup (library)
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/build/pdf.worker.min.mjs";
// + cMapUrl, standardFontDataUrl, iccUrl, wasmUrl for full fidelity
17.3 Build targets (PDFJSDev)
GENERIC · MOZCENTRAL · CHROME · LIB · GECKOVIEW · TESTING
17.4 Hardening defaults for untrusted PDFs
| Option | Hardened value |
|---|---|
enableScripting |
false |
enablePermissions |
true |
annotationEditorMode |
-1 (disable editing) |
maxImageSize |
a finite cap (not -1) |
externalLinkRel |
keep noopener noreferrer nofollow |
17.5 Asset folders & their URL options
| Folder | Option | Default packed? |
|---|---|---|
cmaps/ |
cMapUrl |
cMapPacked: true |
standard_fonts/ |
standardFontDataUrl |
— |
iccs/ |
iccUrl |
— |
wasm/ |
wasmUrl |
— |
17.6 Module map
| Path | Role |
|---|---|
src/display/ |
Main-thread API (consumed by apps). |
src/core/ |
Worker engine (parser, evaluator, codecs). |
src/core/xfa/ |
XFA forms renderer. |
src/scripting_api/ |
Acrobat JS object model (sandboxed). |
src/shared/ |
Cross-thread transport, constants, utils. |
web/ |
Prebuilt viewer + app_options.js. |
extensions/chromium/ |
Chrome extension wrapper. |