Introduction
The demo in the previous article called getDocument with only { url: '...' }. This article follows that single call through the full execution chain of getDocument.
The getDocument call
The source parameter is named src. The demo passes only one key:
Key in src | Value |
|---|---|
url | 'https://assets-blog.streampdf.com/PDF32000_2008.pdf' |
Based on the definition of getDocument in display/api.js, the execution flow is as follows.
Lines 235-236: Initialize the loading task
Instantiate the PDFDocumentLoadingTask class: view source, and assign the return value to the task variable.
Extract docId from task; at this point docId is d0.
Lines 238-335: 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-blog.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:
| Variable | Result | Logic |
|---|---|---|
data | null | If provided, normalized to Uint8Array by getDataProp |
httpHeaders | null | Passed through as-is; null when absent |
withCredentials | false | Only accepts === true |
password | null | ?? null |
rangeTransport | null | Only accepts a PDFDataRangeTransport instance |
rangeChunkSize | 2 ** 16 (64KB) | Only accepts the provided value if it is a positive integer |
worker | null | Only accepts a PDFWorker instance |
verbosity | undefined | Taken verbatim from src.verbosity |
docBaseUrl | null | Only accepts a string that is not a data: URL |
cMapUrl | null | getFactoryUrlProp: non-string becomes null, and it must end with / |
cMapPacked | true | !== false |
iccUrl | null | Same as cMapUrl |
standardFontDataUrl | null | Same as cMapUrl |
wasmUrl | null | Same as cMapUrl |
ignoreErrors | true | src.stopAtErrors !== true, see below |
maxImageSize | -1 (unlimited) | Only accepted if an integer and > -1 |
isOffscreenCanvasSupported | true | Defaults to !isNodeJS when absent |
isImageDecoderSupported | true | Defaults to !isNodeJS when absent |
canvasMaxAreaInBytes | -1 (unlimited) | Only accepted if an integer |
disableFontFace | false | Defaults to isNodeJS when absent |
fontExtraProperties | false | Only accepts === true |
enableXfa | false | Only accepts === true |
ownerDocument | globalThis.document | Defaults to the global document when absent |
disableRange | false | Only accepts === true |
disableStream | false | Only accepts === true |
disableAutoFetch | false | Only accepts === true |
pdfBug | false | Only accepts === true |
CanvasFactory | DOMCanvasFactory | Environment branch, see below |
FilterFactory | DOMFilterFactory | Same as above |
BinaryDataFactory | DOMBinaryDataFactory | Same as above |
enableHWA | false | Only accepts === true |
enableWebGPU | false | Only accepts === true |
gpuPromise | Promise.resolve(false) | WebGPU is off, so GPU initialization does not start |
useWasm | true | !== false |
pagesMapper | new PagesMapper() | Created fresh when absent |
useSystemFonts | true | Cascading default, see below |
useWorkerFetch | false | Compound check, see the Q&A below |
styleElement | null | Only 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 withuseWorkerFetchbelow
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 betrue:
- No custom
BinaryDataFactory— because whenuseWorkerFetchistrue, that parameter is simply ignored (insidetransportFactory,binaryDataFactoryis set tonull). A user-provided factory must be respected, so the default cannot betrue;cMapUrl/standardFontDataUrl/wasmUrlare all provided, andcMapPackedistrue— without URLs there is nothing to fetch;- 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, socMapUrlisnull, condition 2 is not met, and thereforeuseWorkerFetch = false: the main-thread path is taken, withDOMBinaryDataFactorydownloading on the main thread, and the worker requesting data from the main thread viaFetchBinaryDatamessages when needed.
Lines 338: 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 342-350: Instantiate the three factories
const transportFactory = { canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }), filterFactory: new DOMFilterFactory({ docId: 'd0', 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.
Instantiate DOMCanvasFactory: view source
Instantiate DOMFilterFactory: view source
Instantiate DOMBinaryDataFactory: view source
Lines 352-360: 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: worker.promise is a Promise<void> that resolves (to undefined) once the test reply succeeds, and rejects if startup fails or the worker is destroyed.
Lines 362-402: Assemble the two parameter packages
The first is docParams, which will later be sent across threads to the worker:
const docParams = { docId: 'd0', 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.
Lines 404-473: 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:
-
hasGPU = falseis backfilled intoevaluatorOptions; - A
GetDocRequestmessage is sent to the worker withdocParamsas the payload. Sincedataisnull, there is no buffer whose ownership needs to be transferred; - Choose the network stream: with
databeingnullandrangeTransportbeingnull, theurlbranch 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, }); NetworkStream here is not a fixed imported class; it is picked at runtime inside this branch:
const NetworkStream = getNetworkStream(url); // api.js#L431 getNetworkStream makes a three-way choice:
function getNetworkStream(url) { // eslint-disable-next-line no-nested-ternary return isValidFetchUrl(url) ? PDFFetchStream : typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC') && isNodeJS ? PDFNodeStream : PDFNetworkStream; } | Condition | Stream implementation |
|---|---|
isValidFetchUrl(url): protocol is http / https | PDFFetchStream |
| Otherwise, GENERIC build running in Node | PDFNodeStream |
| Everything else | PDFNetworkStream (XHR) |
The demo's url is https://..., so it actually instantiates PDFFetchStream. The other inputs have their own streams too: rangeTransport uses PDFDataTransportStream, while data means the whole PDF is already in memory and no stream is created at all.
In summary: const NetworkStream = PDFFetchStream.
Execution continues:
- Receive the worker's acknowledgment of
GetDocRequest(workerId): click to view the event. The value returned here isd0_worker, assigned to theworkerIdvariable - Create a MessageHandler with
{ docId: 'd0', workerId: 'd0_worker', port: Worker }: click to view the call, assigned to themessageHandlervariable - Create a WorkerTransport with
{ messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper }: click to view the call, attached totask._transport - Send a
Readymessage to the worker, declaring the main-thread side ready: click to view the event
At the end of the chain, task._setupCapability.resolve is called to settle the task's asynchronous setup.
Lines 474-475: Return value
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:
- Create
PDFDocumentLoadingTaskand extractdocId = 'd0'; - Expand
srcinto 35+ local variables; the demo only passesurl, and everything else falls back to defaults; - Instantiate the three DOM factories and package them as
transportFactory; -
PDFWorker.createcreates the worker and starts the handshake; - Assemble
docParams(sent to the worker) andtransportParams(kept on the main thread); - Once the worker is ready, send
GetDocRequest, set up the network stream, createMessageHandlerandWorkerTransport, and finally sendReady; - Return
tasksynchronously; the demo waits for the document to be ready viaawait loadingTask.promise.
Back to the demo in the first article: lines 4-7 call exactly the getDocument dissected here.
Following the caller chain upward, the next call site is: getPage
About me: Creator of StreamPDF, focused on research and development in AI and PDF.
Original article: https://streampdf.com/blog/pdfjs-deep-dive-from-demo-1-custom-viewer
