StreamPDFStreamPDF
Back to blog

PDF.js Deep Dive from Demo 2: getDocument

October 27, 2026 · StreamPDF Team

PDF.js Deep Dive from Demo 2: getDocument

Introduction

In the previous article, we passed a single argument: an object parameter with the property

  • src: the URL of the PDF file, set to https://assets.streampdf.com/PDF32000_2008.pdf

The getDocument call

Based on the definition of getDocument in display/api.js, the execution flow is as follows.

Lines 1-2: Initialize the loading task

Instantiate the PDFDocumentLoadingTask class: click to view the call, and assign the return value to the task variable.

Extract docId from task; at this point docId is d1.

Lines 3-15: Validate and process parameters

This block of code expands the user-provided src object into 35+ local variables: provided values are validated and type-converted, while missing ones fall back to defaults. The demo only passes url, so the vast majority of parameters take the default branch.

The only provided url is converted into a URL object by getUrlProp:

const url = URL.parse('https://assets.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;

All other parameters are absent. In source order, the computed results are:

VariableResultLogic
datanullIf provided, normalized to Uint8Array by getDataProp
httpHeadersnullPassed through as-is; null when absent
withCredentialsfalseOnly accepts === true
passwordnull?? null
rangeTransportnullOnly accepts a PDFDataRangeTransport instance
rangeChunkSize2 ** 16 (64KB)Only accepts the provided value if it is a positive integer
workernullOnly accepts a PDFWorker instance
verbosityundefinedTaken verbatim from src.verbosity
docBaseUrlnullOnly accepts a string that is not a data: URL
cMapUrlnullgetFactoryUrlProp: non-string becomes null, and it must end with /
cMapPackedtrue!== false
iccUrlnullSame as cMapUrl
standardFontDataUrlnullSame as cMapUrl
wasmUrlnullSame as cMapUrl
ignoreErrorstruesrc.stopAtErrors !== true, see below
maxImageSize-1 (unlimited)Only accepted if an integer and > -1
isOffscreenCanvasSupportedtrueDefaults to !isNodeJS when absent
isImageDecoderSupportedtrueDefaults to !isNodeJS when absent
canvasMaxAreaInBytes-1 (unlimited)Only accepted if an integer
disableFontFacefalseDefaults to isNodeJS when absent
fontExtraPropertiesfalseOnly accepts === true
enableXfafalseOnly accepts === true
ownerDocumentglobalThis.documentDefaults to the global document when absent
disableRangefalseOnly accepts === true
disableStreamfalseOnly accepts === true
disableAutoFetchfalseOnly accepts === true
pdfBugfalseOnly accepts === true
CanvasFactoryDOMCanvasFactoryEnvironment branch, see below
FilterFactoryDOMFilterFactorySame as above
BinaryDataFactoryDOMBinaryDataFactorySame as above
enableHWAfalseOnly accepts === true
enableWebGPUfalseOnly accepts === true
gpuPromisePromise.resolve(false)WebGPU is off, so GPU initialization does not start
useWasmtrue!== false
pagesMappernew PagesMapper()Created fresh when absent
useSystemFontstrueCascading default, see below
useWorkerFetchfalseCompound check, see the Q&A below
styleElementnullOnly preserved in TESTING builds; always null in production

These are all the local variables produced by this block of code. Most of the checks are trivial "fall back to default when absent" cases, but a few deserve a closer look.

ignoreErrors: a semantically inverted internal variable

The user-facing parameter is called stopAtErrors, yet the internal variable is called ignoreErrors, computed as src.stopAtErrors !== true. In other words, the default behavior is to ignore errors: when parsing fails, try to recover rather than rejecting promises such as getOperatorList / getTextContent / RenderTask. When reading the source, it is very easy to copy the name and value the wrong way around here.

rangeChunkSize: the 64KB setup for later

rangeChunkSize defaults to 64KB. Since disableRange = false, when the transport is created later, range requests will be issued against the PDF, and each fetched chunk will be exactly this size. Keep it in mind for now.

docBaseUrl: why data: URLs are excluded

The source comment explains the reason: a data: URL cannot be used to resolve absolute paths, and it contains the entire PDF document with no upper bound on length, so it is not sent to the worker thread.

gpuPromise: designed to run in parallel with worker startup

const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);

Even when WebGPU is disabled, this immediately returns a resolved promise instead of deferring the check until it is used. The comment explains the intent: let GPU initialization run in parallel with worker startup, with the resolved boolean forwarded to the worker afterwards.

The three Factories: environment branches

const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC build && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);

The same applies to FilterFactory and BinaryDataFactory. The check has three tiers: user-provided custom > Node environment implementation > DOM implementation. The demo runs in a browser without customization, so all three factories are the DOM versions. Their responsibilities:

  • CanvasFactory: creates canvases for rendering
  • FilterFactory: creates image filters (such as SVG filters)
  • BinaryDataFactory: downloads built-in static assets; works together with useWorkerFetch below

useSystemFonts: the default depends on other parameters

const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;

Its default depends on disableFontFace, which was just parsed above. Why? Fonts not embedded in the PDF are loaded as FontFaces; if disableFontFace is true, that loading path is disabled, and falling back to system fonts at that point would produce completely garbled glyphs. So in that scenario the default is simply false as well, avoiding rendering utterly broken fonts.

useWorkerFetch: a compound check

const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
//   ? src.useWorkerFetch
//   : MOZCENTRAL ||
//     !!(
//       BinaryDataFactory === DOMBinaryDataFactory &&
//       cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
//       isValidFetchUrl(cMapUrl, document.baseURI) &&
//       isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
//       isValidFetchUrl(wasmUrl, document.baseURI)
//     );

Q: What is this check doing? A: It decides who downloads the built-in static assets (CMap files, standard font files, wasm files): the worker thread downloads them itself via the Fetch API (true), or the main thread downloads them and forwards them to the worker (false).

If the user explicitly passes a boolean, the user's value is used directly. Otherwise the default is computed: MOZCENTRAL (the Firefox built-in viewer) is always true; for a regular build, all three of the following conditions must hold at the same time for it to be true:

  1. No custom BinaryDataFactory — because when useWorkerFetch is true, that parameter is simply ignored (inside transportFactory, binaryDataFactory is set to null). A user-provided factory must be respected, so the default cannot be true;
  2. cMapUrl / standardFontDataUrl / wasmUrl are all provided, and cMapPacked is true — without URLs there is nothing to fetch;
  3. These three URLs resolve to http/https relative to document.baseURI — the Fetch API only supports http/https, not file/ftp.

The demo in this article only passes url, so cMapUrl is null, condition 2 is not met, and therefore useWorkerFetch = false: the main-thread path is taken, with DOMBinaryDataFactory downloading on the main thread, and the worker requesting data from the main thread via FetchBinaryData messages when needed.

Line 16: Set the main-thread log level

setVerbosityLevel(verbosity); // verbosity is undefined

verbosity is undefined, so the main-thread log level stays at its default. Note that this only affects the main thread; the worker thread's log level is passed separately when the worker is created.

Lines 17-25: Instantiate the three factories

const transportFactory = {
  canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
  filterFactory: new DOMFilterFactory({ docId: 'd1', ownerDocument: document }),
  binaryDataFactory: new DOMBinaryDataFactory({
    cMapUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
  }),
};
// binaryDataFactory: null when MOZCENTRAL or useWorkerFetch is true

The three Factory classes selected earlier are instantiated and packaged as transportFactory, which is then handed to the transport. Since useWorkerFetch = false, binaryDataFactory is not null — echoing the Q&A in the previous section: built-in static assets are downloaded on the main thread.

The internal capabilities of the three factory instances will be covered in a separate article.

Lines 26-33: Create the worker

worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port comes from GlobalWorkerOptions.workerPort, which the demo does not set
task._worker = worker;

The demo does not pass a worker, so PDF.js creates and owns one itself. PDFWorker.create synchronously returns a PDFWorker instance; its constructor reads GlobalWorkerOptions.workerSrc (the worker file URL configured in the first article), creates a Worker, and completes the startup handshake through ready / test messages — covered in detail in Article 8. For now, just remember: the returned worker object has a promise on it, which resolves once the worker is ready.

Lines 34-63: Assemble the two parameter packages

The first is docParams, which will later be sent across threads to the worker:

const docParams = {
  docId: 'd1',
  apiVersion: '6.3.289', // build version number
  data: null,
  password: null,
  disableAutoFetch: false,
  rangeChunkSize: 65536,
  docBaseUrl: null,
  enableXfa: false,
  evaluatorOptions: {
    maxImageSize: -1,
    disableFontFace: false,
    ignoreErrors: true,
    isOffscreenCanvasSupported: true,
    isImageDecoderSupported: true,
    canvasMaxAreaInBytes: -1,
    fontExtraProperties: false,
    useSystemFonts: true,
    useWasm: true,
    useWorkerFetch: false,
    cMapUrl: null,
    cMapPacked: true,
    iccUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
    hasGPU: false, // placeholder, backfilled later
  },
};

The second is transportParams, which stays on the main thread:

const transportParams = {
  ownerDocument: document,
  pdfBug: false,
  styleElement: null,
  enableHWA: false,
  loadingParams: { disableAutoFetch: false, enableXfa: false },
};

One sentence to distinguish them: docParams is the "work order" sent to the worker, while transportParams is the "local configuration" kept on the main thread.

From line 64: the handshake after the worker is ready

Promise.all([worker.promise, gpuPromise]).then(...)

Once both worker readiness and GPU probing (resolving to false) complete, the following happens in sequence:

  1. hasGPU = false is backfilled into evaluatorOptions;
  2. A GetDocRequest message is sent to the worker with docParams as the payload. Since data is null, there is no buffer whose ownership needs to be transferred;
  3. Choose the network stream: with data being null and rangeTransport being null, the url branch is taken and a network stream is instantiated (the fetch implementation is preferred, with a fallback to XHR when unsupported; see Article 9 for how range requests are issued internally):
networkStream = new NetworkStream({
  url,
  httpHeaders: null,
  withCredentials: false,
  rangeChunkSize: 65536,
  disableRange: false,
  disableStream: false,
});
  1. After receiving the worker's acknowledgment of GetDocRequest (workerId), a MessageHandler and a WorkerTransport are created and attached to task._transport — covered in Articles 6 and 7 respectively;
  2. A Ready message is sent to the worker, declaring that the main-thread side is ready; the worker's Ready handler then calls setupDoc(docParams).

Return value: task

All of the above .then chain proceeds asynchronously in the background, while getDocument itself returns task synchronously. If any step in the chain fails, the capability inside task is rejected — the very promise that the demo waits on with await loadingTask.promise.

At this point, getDocument has finished executing.

Summary

In this section we followed the demo through the complete getDocument call:

  1. Create PDFDocumentLoadingTask and extract docId = 'd1';
  2. Expand src into 35+ local variables; the demo only passes url, and everything else falls back to defaults;
  3. Instantiate the three DOM factories and package them as transportFactory;
  4. PDFWorker.create creates the worker and starts the handshake;
  5. Assemble docParams (sent to the worker) and transportParams (kept on the main thread);
  6. Once the worker is ready, send GetDocRequest, set up the network stream, create MessageHandler and WorkerTransport, and finally send Ready;
  7. Return task synchronously; the demo waits for the document to be ready via await loadingTask.promise.

Along the way we deliberately skipped four "heavyweight players": the creation and handshake of PDFWorker, the messaging mechanism of MessageHandler, the initialization of WorkerTransport, and the range request strategy of the network stream. Each of them gets its own article, starting with the next one.

Related articles