PDF.js Deep Dive from Demo 4: getViewport
October 27, 2026 · StreamPDF Team

Introduction
In the previous article, we obtained the page variable via await pdf.getPage(1). It is a PDFPageProxy instance whose _pageInfo holds the page metadata: rotate: 0, userUnit: 1, view: [0, 0, 595, 842].
Back to the demo from the first article, lines 11-14:
const viewport = page.getViewport({ scale: 0.5 });An object parameter is passed in:
- scale: the zoom factor. Its value is
0.5
Unlike getPage, getViewport is a synchronous call: it does not touch the worker and performs no IO — it is purely a math computation. This article follows that call into the source code.
The getViewport call
Based on its definition in api.js, the execution flow is as follows.
Step 1: Destructure the parameters
getViewport({
scale, // 0.5, the only parameter passed in
rotation = this.rotate, // this._pageInfo.rotate = 0
offsetX = 0,
offsetY = 0,
dontFlip = false,
} = {}) {
return new PageViewport({
viewBox: this.view, // [0, 0, 595, 842]
userUnit: this.userUnit, // 1
scale,
rotation,
offsetX,
offsetY,
dontFlip,
});
}Note that the default value of rotation comes from the page itself: the PDF page's own /Rotate attribute is applied automatically, so the caller does not need to care about it. this.view and this.userUnit come from the pageInfo returned by the GetPage message in the previous article.
Once the parameters are collected, they are all handed to the PageViewport constructor (display/page_viewport.js), where all subsequent computation takes place.
Step 2: scale absorbs userUnit
scale *= userUnit; // 0.5 * 1 = 0.5userUnit is a unit conversion factor introduced in PDF 1.6: 1 unit in PDF user space = userUnit / 72 inches, with a default value of 1. By folding it into scale, subsequent computations no longer need to distinguish between "user space units" and "actual size".
Step 3: Compute the page center point
const centerX = (viewBox[2] + viewBox[0]) / 2; // (595 + 0) / 2 = 297.5
const centerY = (viewBox[3] + viewBox[1]) / 2; // (842 + 0) / 2 = 421Rotation happens around the page center, so the center point is computed first for later use.
Step 4: Normalize the rotation and compute the rotation vector
rotation %= 360; // 0
if (rotation < 0) {
rotation += 360;
}
switch (rotation) {
case 0:
rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1;
break;
// case 90 / 180 / 270 omitted
default:
throw new Error(
"PageViewport: Invalid rotation, must be a multiple of 90 degrees."
);
}The rotation is normalized into the [0, 360) range, and only multiples of 90 are allowed; any other value throws an error directly — this is a restriction of the PDF specification.
The demo's rotation is 0, which hits case 0: rotateA = 1, rotateB = 0, rotateC = 0, rotateD = -1.
Q: Why is rotateD -1 when rotation is 0? A: This is the y-axis flip. In the PDF coordinate system the y-axis points up (a mathematical coordinate system with the origin at the bottom-left), while in the canvas coordinate system the y-axis points down (a screen coordinate system with the origin at the top-left). Even without any rotation, a flip is needed to align the two, and
rotateD = -1is exactly that flip.Correspondingly,
dontFlip = truenegatesrotateCandrotateD, canceling this flip — useful when the target coordinate system also has an upward y-axis.
Step 5: Compute width, height, and canvas offsets
if (rotateA === 0) {
// The branch for rotation 90/270: width and height are swapped
} else {
offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; // 297.5 * 0.5 + 0 = 148.75
offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; // 421 * 0.5 + 0 = 210.5
width = (viewBox[2] - viewBox[0]) * scale; // 595 * 0.5 = 297.5
height = (viewBox[3] - viewBox[1]) * scale; // 842 * 0.5 = 421
}The rotateA === 0 branch corresponds to a 90/270 degree rotation: the page lies on its side, and the computations of width and height are swapped. The demo's rotation is 0, so it takes the else branch.
Step 6: Assemble the transform matrix
this.transform = [
rotateA * scale, // 0.5
rotateB * scale, // 0
rotateC * scale, // 0
rotateD * scale, // -0.5
offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, // 148.75 - 148.75 - 0 = 0
offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY, // 210.5 - 0 + 210.5 = 421
];
// transform = [0.5, 0, 0, -0.5, 0, 421]The source code comment explains the composite transformation these 6 components correspond to:
translate(-centerX, -centerY), rotate and flip vertically, scale, and translate(offsetCanvasX, offsetCanvasY)
That is: move the page center to the origin → rotate and flip vertically → scale → translate to the target position on the canvas. These are exactly the 6 arguments that canvas setTransform(a, b, c, d, e, f) needs, and they will be applied directly during rendering (covered in the next article).
Let's verify the correctness of the matrix: take the top-left corner of the page in the PDF coordinate system, (0, 842) (with the y-axis pointing up, "top" is the larger value), and apply the matrix:
x' = 0.5 * 0 + 0 * 842 + 0 = 0y' = 0 * 0 + (-0.5) * 842 + 421 = 0
It maps exactly to the canvas coordinate (0, 0), i.e., the top-left corner of the canvas. ✓
The 6th component, 421, happens to equal height: after the y-axis flip, the whole page ends up above the canvas (negative y), so it needs to be translated down by one page height to land back inside the canvas.
Step 7: Store width and height
this.width = width; // 297.5
this.height = height; // 421These two values are the source of canvas.width = Math.floor(viewport.width) in the first article's demo: Math.floor(297.5) is 297, so the actual canvas size is 297 x 421.
Summary
This article followed the demo through the complete computation of getViewport({ scale: 0.5 }):
- Parameter destructuring:
scale = 0.5,rotationdefaults to the page's/Rotatevalue0, and the rest are0/false; scaleabsorbsuserUnit, remaining0.5;- Compute the page center point
(297.5, 421); - After rotation normalization, take the rotation vector
(1, 0, 0, -1), whererotateD = -1performs the y-axis flip; - Compute the width and height
297.5 x 421and the canvas offsets(148.75, 210.5); - Assemble
transform = [0.5, 0, 0, -0.5, 0, 421], completing the "PDF coordinates → canvas coordinates" mapping.
viewport also carries methods like convertToViewportPoint / convertToPdfPoint (coordinate conversion in both directions, used for text selection and annotation positioning) and clone (deriving a new viewport), which the demo does not use, so we will not expand on them here.
With the viewport in hand, the demo packages it together with canvasContext and passes them to page.render(renderContext) — the transform matrix will be applied to the canvas context there. 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 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 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