PDF.js Deep Dive from Demo 1: Rendering a PDF with a Custom Viewer
October 19, 2026 · StreamPDF Team

Introduction
In our previous series of articles, we explored the architecture of PDF.js, as well as technical topics such as the rendering layer and message communication. However, while writing those articles, we identified a few problems:
- The articles could not be connected to one another and failed to form a complete knowledge system.
- They could not be mapped back to the PDF.js source code, so following along carried a heavy mental burden.
Therefore, we decided to start a new series that begins with demo code and gradually follows the call chain deep into the internals of PDF.js. We will explain the principles behind the code from two perspectives:
- Demo interpretation: case-by-case interpretation, linked from shallow to deep, exploring in depth.
- Repository interpretation: generalized interpretation that can be read in chunks, each standing on its own as an article.
Come on, let's start from the beginning.
Rendering a PDF with a Custom Viewer
Let's try building a minimal custom viewer to render a PDF, then follow each line of code into the internals of PDF.js. As we follow along, we will substitute variable values into the code to make the execution flow easier to understand.
import * as pdfjsLib from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.mjs';
const loadingTask = pdfjsLib.getDocument({
url: 'https://assets.streampdf.com/PDF32000_2008.pdf',
});
const pdf = await loadingTask.promise;
// Get the rendering data of the first page
const page = await pdf.getPage(1);
// Get the viewport data of the first page
const viewport = page.getViewport({ scale: 0.5 });
// Get the canvas onto which we will render
const canvas = document.getElementById('canvas-id');
const context = canvas.getContext('2d');
// Match the canvas dimensions to the PDF viewport size
canvas.width = Math.floor(viewport.width);
canvas.height = Math.floor(viewport.height);
canvas.style.width = Math.floor(viewport.width) + 'px';
canvas.style.height = Math.floor(viewport.height) + 'px';
// Prepare the rendering parameters
const renderContext = {
canvasContext: context,
viewport,
};
// Call the render method to perform the actual rendering, awaiting its completion
await page.render(renderContext);demo: https://streampdf.com/blog/demos/a-simple-pdf-render-demo.html
Lines 1-3: Configure the Worker file path
Import the entry file of the PDF.js library and set the path of the Worker file.
This means that when using PDF.js, we can even completely replace the Worker with our own, as long as it follows the interface specification.
Here we set it to the default Worker file path:
GlobalWorkerOptions.workerSrc = https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.mjs
Lines 4-7: Load the PDF document
Call the getDocument method to load the PDF document, passing in an object parameter:
- src: the URL of the PDF document. The value is
https://assets.streampdf.com/PDF32000_2008.pdf.
The getDocument method returns a Promise that resolves to a PDF document object once the document has finished loading.
const loadingTask = pdfjsLib.getDocument(...) is a synchronous call: click to view the call.
loadingTask.promise is a getter that returns a Promise created by the Promise.withResolvers() method.
Here we await loadingTask.promise to wait for the PDF document to finish loading and assign it to the pdf variable.
Lines 8-10: Get the first page of the PDF document
Once the document has been initialized, we can call the getPage method to get the rendering data of the first page.
The getPage method returns a Promise that resolves to a page object once the page data has finished loading.
const page = await pdf.getPage(1); is an asynchronous call: click to view the call.
After the first page's data has finished loading, it is assigned to the page variable.
Lines 11-14: Get the viewport data of the first page
Once the page data has loaded, we can call the getViewport method to get the viewport data of the first page.
const viewport = page.getViewport({ scale: 0.5 }); is a synchronous call: click to view the call.
Get the viewport data of the first page, passing in an object parameter:
- scale: the scale factor. The value is
0.5.
The returned viewport is an object containing the width and height of the viewport. Its value is:
{
width: 297.5,
height: 421,
// others, not used here, omitted for now...
}It is assigned to the viewport variable.
Lines 15-18: Get the canvas onto which we will render
After obtaining the viewport variable, match the canvas's width and height to the PDF viewport size.
canvas.width = 297; // Math.floor(viewport.width);
canvas.height = 421; // Math.floor(viewport.height);Also set the canvas's CSS width and height to the PDF viewport size.
canvas.style.width = 297 + 'px'; // Math.floor(viewport.width) + 'px';
canvas.style.height = 421 + 'px'; // Math.floor(viewport.height) + 'px';Why do we need to set the canvas's CSS width and height? The canvas's
widthandheightattributes control the pixel dimensions of the canvas, while the CSS width and height are the dimensions at which the canvas is displayed on the page. In general, these two are the same. But on high-resolution screens, they differ. For example, when window.devicePixelRatio is 2, the canvas's pixel dimensions should be set to 594 by 842, while the CSS width and height remain 297 by 421, to prevent blurry rendering.
Lines 19-22: Render the first page
Once the canvas is ready, we can call the render method to render the first page.
Prepare the canvas context, viewport data, and rendering parameters so that we can call the render method.
await page.render(renderContext); is an asynchronous call: click to view the call.
At this point, the code execution is complete.
Summary
In this chapter, we followed a custom viewer to render the first page of a PDF file.
The biggest difference from other articles is that we substituted variable values into the chain of code being analyzed, so that different execution paths can be determined. Different files and analysis articles lead into different branch paths, making reading the article feel like debugging the code.
We hope this brings you a different kind of source code reading experience.
happy coding!
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 3: getPageOctober 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 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026