PDF.js Deep Dive from Demo 3: getPage
October 27, 2026 · StreamPDF Team

Introduction
In the previous article, we obtained the pdf variable via await loadingTask.promise. It is an instance of PDFDocumentProxy.
Back to the demo in the first article, lines 8-10:
const page = await pdf.getPage(1);A numeric argument is passed in:
- pageNumber: the page number; the first page is 1. The value is
1.
getPage returns a Promise that resolves to a PDFPageProxy object. In this article, we follow this call into the source code.
The getPage call
According to the definition in api.js, the execution flow is as follows.
PDFDocumentProxy.getPage: a one-line delegation
getPage(pageNumber) {
return this._transport.getPage(pageNumber);
}The pdf object itself is just a facade; the real logic lives on this._transport — the WorkerTransport instance created in article 2 (task._transport). Everything below happens inside WorkerTransport.getPage.
Step 1: Validate the arguments
if (
!Number.isInteger(pageNumber) ||
pageNumber <= 0 ||
pageNumber > this.pagesMapper.pagesNumber
) {
return Promise.reject(new Error("Invalid page request."));
}Plugging in the demo values:
Number.isInteger(1)istrue1 > 0this.pagesMapper.pagesNumberis756— written frompdfInfo.numPageswhen the document becomes ready. The demo PDF in this article has 756 pages, so1 <= 756.
Validation passes. If you pass 0, a negative number, a decimal, or a number beyond the page count, this returns a rejected promise immediately.
Q: Why use
Promise.rejectinstead ofthrow? A: To keep the asynchronous semantics consistent. Callers ofgetPagealwaysawaita promise, so argument errors go through the same error channel. Callers don't have to handle both a synchronous throw and an asynchronous rejection.
Step 2: Convert the page number
const pageIndex = pageNumber - 1; // 1 - 1 = 0
const newPageIndex = this.pagesMapper.getPageId(pageNumber) - 1; // 1 - 1 = 0Two numbering schemes appear here:
pageNumber: the page number from the user's perspective, 1-based, intuitive.pageIndex: the internal page number, 0-based, used as an array index and cache key.
pagesMapper.getPageId(1) returns 1. PagesMapper was created with new PagesMapper() during argument parsing in article 2. It maintains the mapping between "the page number the user sees" and "the original document page ID". The demo never moves, deletes, or copies pages, so the mapping is an identity mapping (#pageNumberToId is null, and getPageId returns its input unchanged). Therefore newPageIndex === pageIndex === 0.
Q: Why go through PagesMapper at all? A: To support page reordering. In an editor scenario, users can move, delete, and copy pages, after which "page 3" may no longer be the original document's page 3. The worker side only understands the original document's page order, so what gets sent to the worker is
newPageIndex, while the main-thread cache is keyed by the user-facingpageIndex. Under an identity mapping the two are identical and can be ignored.
Step 3: Check the cache
const cachedPromise = this.#pagePromises.get(0);
if (cachedPromise) {
return cachedPromise;
}#pagePromises is a Map<pageIndex, Promise<PDFPageProxy>>. On the first call there is no cache hit, so we continue.
Q: What does the cache actually do? A: In practice, calling
await pdf.getPage(1)twice in a row returns the samePDFPageProxyobject (page === page2istrue). The second call hits the cached promise directly and no message is sent to the worker.
Step 4: Send the GetPage message to the worker
const promise = this.messageHandler
.sendWithPromise("GetPage", {
pageIndex: 0, // newPageIndex
})
.then(pageInfo => { ... });
this.#pagePromises.set(0, promise);
return promise;messageHandler is the MessageHandler created in article 2. The communication mechanism of sendWithPromise will be covered in a separate article. All you need to know here is that it sends the message to the worker thread and returns a promise that waits for the worker's reply.
When the worker receives GetPage (core/worker.js):
handler.on("GetPage", async function ({ pageIndex }) {
const page = await pdfManager.getPage(pageIndex);
const [rotate, ref, userUnit, view] = await Promise.all([
pdfManager.ensure(page, "rotate"),
pdfManager.ensure(page, "ref"),
pdfManager.ensure(page, "userUnit"),
pdfManager.ensure(page, "view"),
]);
return {
rotate,
ref,
refStr: ref?.toString() ?? null,
userUnit,
view,
};
});How the worker parses the page object internally will be covered in a separate article. The reply for the demo's first page is:
pageInfo = {
rotate: 0,
ref: { num: 4, gen: 0 }, // after structured cloning across threads, ref degrades to a plain object
refStr: "4 0 R",
userUnit: 1,
view: [0, 0, 595, 842],
}Q: Why only these 4 properties? A: They are all metadata of the page: rotation angle, object reference, unit conversion factor, and visible area. Reading them only requires parsing the page object dictionary, without touching the page content stream. The real content parsing (drawing commands) is deferred to the
GetOperatorListmessage atrendertime. This is whygetPagecan return quickly.These 4 properties map one-to-one to getters on
PDFPageProxy:rotate,ref,userUnit, andview. The next article'sgetViewportwill use two of them.
Step 5: Create the PDFPageProxy
Once the reply arrives, we enter the .then callback:
.then(pageInfo => {
if (this.destroyed) {
throw new Error("Transport destroyed");
}
if (pageInfo.refStr) {
this.#pageRefCache.set("4 0 R", 0);
}
const page = new PDFPageProxy(0, pageInfo, this, this.pagesMapper, false);
this.#pageCache.set(0, page);
return page;
});Three things happen in sequence:
destroyedcheck: while waiting for the reply, the document may already have been destroyed (loadingTask.destroy()); if so, throw an error.#pageRefCachereverse-lookup cache:"4 0 R"→0. Used bygetPageIndex(ref)for reverse lookups — for example, when an outline link jumps to a page, the PDF internally locates pages by object reference (4 0 R), which must be translated back to a page number.- Create the
PDFPageProxyand store it in#pageCache.
The PDFPageProxy constructor only assigns fields; it does no parsing at all:
this._pageIndex = 0;
this._pageInfo = pageInfo;
this._transport = transport;
this._stats = null; // pdfBug is false, so performance stats are not enabled
this.commonObjs = transport.commonObjs; // resource container shared across the whole document (fonts, etc.)
this.objs = new PDFObjects(); // resource container private to this page
this._intentStates = new Map(); // rendering intent state cache, used by render in article 5
this.destroyed = false;commonObjs and objs are resource containers used later during rendering; PDFObjects will be covered in a separate article.
At this point, the promise returned by getPage(1) resolves to page, and the demo's await receives it.
Summary
In this section we followed the demo through the complete getPage(1) call:
PDFDocumentProxy.getPagedelegates in one line toWorkerTransport.getPage.- Argument validation:
1is a valid page number (756 pages in total). - Page number conversion:
pageNumber(1)→pageIndex(0)→newPageIndex(0); under the identity mapping all three agree. - The
#pagePromisescache is checked and missed on the first call. - A
GetPagemessage is sent to the worker, and 4 pieces of page metadata are received. #pageRefCacheis written, thePDFPageProxyis created, and#pageCacheand#pagePromisesare populated.- The promise is returned, and the demo's
awaityieldspage.
Along the way we deliberately skipped two "heavyweight" components: the cross-thread communication of MessageHandler.sendWithPromise, and the worker-side page parsing of pdfManager.getPage. Each gets its own article.
With page in hand, the demo's next step is page.getViewport({ scale: 0.5 }) — which will use the view and userUnit from this article's pageInfo. That will be covered in the next article.
Related articles
- PDF.js Deep Dive from Demo 10: CanvasGraphics and operatorList RenderingOctober 27, 2026
- PDF.js Deep Dive from Demo 9: Network Streams and Range RequestsOctober 27, 2026
- PDF.js Deep Dive from Demo 8: PDFWorker.create and the Real/Fake Worker HandshakeOctober 27, 2026
- PDF.js Deep Dive from Demo 7: WorkerTransport InitializationOctober 27, 2026
- PDF.js Deep Dive from Demo 6: MessageHandler Cross-Thread CommunicationOctober 27, 2026
- PDF.js Deep Dive from Demo 5: renderOctober 27, 2026
- PDF.js Deep Dive from Demo 4: getViewportOctober 27, 2026
- PDF.js Deep Dive from Demo 2: getDocumentOctober 27, 2026
- PDF.js Source Walkthrough: TextLayerOctober 27, 2026
- PDF.js Source Walkthrough: font_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_displayOctober 27, 2026
- PDF.js Source Walkthrough: PDFObjectsOctober 27, 2026
- PDF.js Source Walkthrough: Network StreamsOctober 27, 2026
- PDF.js Source Walkthrough: internal_evt.jsOctober 27, 2026
- PDF.js Source Walkthrough: math_clamp.jsOctober 27, 2026
- PDF.js Source Walkthrough: css_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: scripting_utilsOctober 27, 2026
- PDF.js Source Walkthrough: MurmurHash3_64October 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_utilsOctober 27, 2026
- PDF.js Source Walkthrough: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: standard_fontsOctober 27, 2026
- PDF.js Source Walkthrough: fonts_utilsOctober 27, 2026
- PDF.js Source Walkthrough: default_appearanceOctober 27, 2026
- PDF.js Source Walkthrough: metadata_parserOctober 27, 2026
- PDF.js Source Walkthrough: name_number_treeOctober 27, 2026
- PDF.js Source Walkthrough: struct_treeOctober 27, 2026
- PDF.js Source Walkthrough: catalogOctober 27, 2026
- PDF.js Source Walkthrough: pdf_managerOctober 27, 2026
- PDF.js Source Walkthrough: object_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_coreOctober 27, 2026
- PDF.js Source Walkthrough: cryptoOctober 27, 2026
- PDF.js Source Walkthrough: core_utilsOctober 27, 2026
- PDF.js Source Walkthrough: primitivesOctober 27, 2026
- PDF.js Source Walkthrough: worker_streamOctober 27, 2026
- PDF.js Source Walkthrough: ccitt_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_streamOctober 27, 2026
- PDF.js Source Walkthrough: jbig2_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpeg_streamOctober 27, 2026
- PDF.js Source Walkthrough: decrypt_streamOctober 27, 2026
- PDF.js Source Walkthrough: brotli_streamOctober 27, 2026
- PDF.js Source Walkthrough: run_length_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_hex_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_85_streamOctober 27, 2026
- PDF.js Source Walkthrough: lzw_streamOctober 27, 2026
- PDF.js Source Walkthrough: predictor_streamOctober 27, 2026
- PDF.js Source Walkthrough: flate_streamOctober 27, 2026
- PDF.js Source Walkthrough: decode_streamOctober 27, 2026
- PDF.js Source Walkthrough: chunked_streamOctober 27, 2026
- PDF.js Source Walkthrough: streamOctober 27, 2026
- PDF.js Source Walkthrough: base_streamOctober 27, 2026
- PDF.js Deep Dive 11: ParserOctober 26, 2026
- PDF.js Deep Dive 10: XRefOctober 26, 2026
- PDF.js Deep Dive 9: CanvasGraphicsOctober 26, 2026
- PDF.js Deep Dive 8: PDFDocumentOctober 26, 2026
- The AI Era: What Should We Do to Survive? (Reflections on Founding StreamPDF)October 26, 2026
- PDF.js Deep Dive 7: PartialEvaluatorOctober 25, 2026
- PDF.js Deep Dive 6: PDFManager.initDocumentOctober 25, 2026
- PDF.js Deep Dive 5: PDFFetchStream and PDFWorkerStreamOctober 25, 2026
- PDF.js Deep Dive 4: MessageHandlerOctober 22, 2026
- PDF.js Deep Dive 3: getViewport & renderOctober 22, 2026
- PDF.js Deep Dive from Demo 1: Rendering a PDF with a Custom ViewerOctober 19, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026