PDF.js 从 demo 深入浅出 4: getViewport 方法全貌
2026年10月27日 · StreamPDF Team

前言
在上一篇文章中, 我们通过 await pdf.getPage(1) 拿到了 page 变量, 它是一个 PDFPageProxy 实例, 身上的 _pageInfo 保存着页面元数据: rotate: 0, userUnit: 1, view: [0, 0, 595, 842].
回到第一篇的 demo, 第 11-14 行:
const viewport = page.getViewport({ scale: 0.5 });传入 object 参数:
- scale: 缩放比例. 值为
0.5
与 getPage 不同, getViewport 是同步调用: 不碰 worker, 不做 IO, 只是一次纯数学计算. 本篇跟随这次调用进入源码.
getViewport 调用
依据 api.js 中 PDFPageProxy 的定义, 代码执行流程如下.
第 1 步, 参数解构
getViewport({
scale, // 0.5, 唯一传入的参数
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,
});
}注意 rotation 的默认值取自页面自身: PDF 页面自带的 /Rotate 属性会被自动应用, 调用方无需关心. this.view 与 this.userUnit 则来自上一篇 GetPage 消息返回的 pageInfo.
参数收集完毕, 全部交给 PageViewport 构造函数 (display/page_viewport.js), 后续计算都在其中.
第 2 步, scale 吸收 userUnit
scale *= userUnit; // 0.5 * 1 = 0.5userUnit 是 PDF 1.6 引入的单位换算因子: PDF 用户空间的 1 个单位 = userUnit / 72 英寸, 默认值为 1. 把它乘进 scale, 后续计算就不必再区分「用户空间单位」与「实际尺寸」.
第 3 步, 计算页面中心点
const centerX = (viewBox[2] + viewBox[0]) / 2; // (595 + 0) / 2 = 297.5
const centerY = (viewBox[3] + viewBox[1]) / 2; // (842 + 0) / 2 = 421旋转围绕页面中心进行, 先把中心点算出来备用.
第 4 步, 旋转归一化与旋转向量
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 略
default:
throw new Error(
"PageViewport: Invalid rotation, must be a multiple of 90 degrees."
);
}旋转被归一化到 [0, 360) 区间, 且只允许 90 的倍数, 其他值直接抛错 — 这是 PDF 规范的限制.
demo 的 rotation 为 0, 命中 case 0: rotateA = 1, rotateB = 0, rotateC = 0, rotateD = -1.
Q: 为什么 rotation 为 0 时 rotateD 是 -1? A: 这是 y 轴翻转. PDF 坐标系 y 轴向上 (数学坐标系, 原点在左下角), canvas 坐标系 y 轴向下 (屏幕坐标系, 原点在左上角). 即使不旋转, 也需要一次翻转让两者对齐,
rotateD = -1就是这个翻转.相应的,
dontFlip = true会把rotateC,rotateD取反, 取消这次翻转 — 适用于目标坐标系同样 y 轴向上的场景.
第 5 步, 计算宽高与画布偏移
if (rotateA === 0) {
// rotation 为 90/270 时的分支: 宽高互换
} 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
}rotateA === 0 的分支对应旋转 90/270 度: 页面躺倒, width 与 height 的计算互换. demo 旋转为 0, 走 else 分支.
第 6 步, 组装 transform 矩阵
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]源码注释说明了这 6 个分量对应的复合变换:
translate(-centerX, -centerY), rotate and flip vertically, scale, and translate(offsetCanvasX, offsetCanvasY)
即: 把页面中心移到原点 → 旋转并垂直翻转 → 缩放 → 平移到画布上的目标位置. 它就是 canvas setTransform(a, b, c, d, e, f) 需要的 6 个参数, 渲染时会被直接应用 (见下一篇).
验证一下矩阵的正确性: 取 PDF 坐标系中页面的左上角 (0, 842) (y 轴向上, 「上」是大值), 经矩阵变换:
x' = 0.5 * 0 + 0 * 842 + 0 = 0y' = 0 * 0 + (-0.5) * 842 + 421 = 0
恰好映射到 canvas 坐标 (0, 0), 即画布左上角. ✓
第 6 个分量 421 恰好等于 height: y 轴翻转后页面整体跑到了画布上方 (y 为负), 需要向下平移一个页面高度才能落回画布内.
第 7 步, 存宽高
this.width = width; // 297.5
this.height = height; // 421这两个值就是第一篇 demo 里 canvas.width = Math.floor(viewport.width) 的来源: Math.floor(297.5) 为 297, canvas 的实际尺寸为 297 x 421.
总结
本节跟随 demo 走完了 getViewport({ scale: 0.5 }) 的完整计算:
- 参数解构:
scale = 0.5,rotation默认取页面/Rotate值0, 其余为0/false; scale吸收userUnit, 仍为0.5;- 计算页面中心点
(297.5, 421); - 旋转归一化后取旋转向量
(1, 0, 0, -1),rotateD = -1承担 y 轴翻转; - 计算宽高
297.5 x 421与画布偏移(148.75, 210.5); - 组装
transform = [0.5, 0, 0, -0.5, 0, 421], 完成「PDF 坐标 → canvas 坐标」的映射.
viewport 上还挂着 convertToViewportPoint / convertToPdfPoint (坐标互转, 文字选区与注释定位会用到), clone (派生新 viewport) 等方法, demo 未用到, 不展开.
拿到 viewport 之后, demo 把它和 canvasContext 一起打包传给 page.render(renderContext) — transform 矩阵将在那里被应用到 canvas 上下文. 下一篇展开.
相关文章
- PDF.js 从 demo 深入浅出 10: CanvasGraphics 与 operatorList 绘制2026年10月27日
- PDF.js 从 demo 深入浅出 9: 网络流与 Range 请求2026年10月27日
- PDF.js 从 demo 深入浅出 8: PDFWorker.create 与真假 Worker 握手2026年10月27日
- PDF.js 从 demo 深入浅出 7: WorkerTransport 初始化2026年10月27日
- PDF.js 从 demo 深入浅出 6: MessageHandler 跨线程通讯2026年10月27日
- PDF.js 从 demo 深入浅出 5: render 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 3: getPage 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 2: getDocument 方法全貌2026年10月27日
- PDF.js 源码解读: TextLayer2026年10月27日
- PDF.js 源码解读: font_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_display2026年10月27日
- PDF.js 源码解读: PDFObjects2026年10月27日
- PDF.js 源码解读: 网络流2026年10月27日
- PDF.js 源码解读: internal_evt.js2026年10月27日
- PDF.js 源码解读: math_clamp.js2026年10月27日
- PDF.js 源码解读: css_utils.js2026年10月27日
- PDF.js 源码解读: scripting_utils2026年10月27日
- PDF.js 源码解读: MurmurHash3_642026年10月27日
- PDF.js 源码解读: obj_bin_transform_utils2026年10月27日
- PDF.js 源码解读: image_utils.js2026年10月27日
- PDF.js 源码解读: BasePDFStream2026年10月27日
- PDF.js 源码解读: 共享工具层2026年10月27日
- PDF.js 源码解读: MessageHandler2026年10月27日
- PDF.js 源码解读: api2026年10月27日
- PDF.js 源码解读: fonts2026年10月27日
- PDF.js 源码解读: glyphlist2026年10月27日
- PDF.js 源码解读: binary_cmap2026年10月27日
- PDF.js 源码解读: cmap2026年10月27日
- PDF.js 源码解读: encodings2026年10月27日
- PDF.js 源码解读: metrics2026年10月27日
- PDF.js 源码解读: standard_fonts2026年10月27日
- PDF.js 源码解读: fonts_utils2026年10月27日
- PDF.js 源码解读: default_appearance2026年10月27日
- PDF.js 源码解读: metadata_parser2026年10月27日
- PDF.js 源码解读: name_number_tree2026年10月27日
- PDF.js 源码解读: struct_tree2026年10月27日
- PDF.js 源码解读: catalog2026年10月27日
- PDF.js 源码解读: pdf_manager2026年10月27日
- PDF.js 源码解读: object_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_core2026年10月27日
- PDF.js 源码解读: crypto2026年10月27日
- PDF.js 源码解读: core_utils2026年10月27日
- PDF.js 源码解读: primitives2026年10月27日
- PDF.js 源码解读: worker_stream2026年10月27日
- PDF.js 源码解读: ccitt_stream2026年10月27日
- PDF.js 源码解读: jpx_stream2026年10月27日
- PDF.js 源码解读: jbig2_stream2026年10月27日
- PDF.js 源码解读: jpeg_stream2026年10月27日
- PDF.js 源码解读: decrypt_stream2026年10月27日
- PDF.js 源码解读: brotli_stream2026年10月27日
- PDF.js 源码解读: run_length_stream2026年10月27日
- PDF.js 源码解读: ascii_hex_stream2026年10月27日
- PDF.js 源码解读: ascii_85_stream2026年10月27日
- PDF.js 源码解读: lzw_stream2026年10月27日
- PDF.js 源码解读: predictor_stream2026年10月27日
- PDF.js 源码解读: flate_stream2026年10月27日
- PDF.js 源码解读: decode_stream2026年10月27日
- PDF.js 源码解读: chunked_stream2026年10月27日
- PDF.js 源码解读: stream2026年10月27日
- PDF.js 源码解读: base_stream2026年10月27日
- PDF.js 深入浅出 11: Parser2026年10月26日
- PDF.js 深入浅出 10: XRef2026年10月26日
- PDF.js 深入浅出 9: CanvasGraphics2026年10月26日
- PDF.js 深入浅出 8: Document 与 Page2026年10月26日
- AI 时代, 我们到底应该做什么才能活下去(StreamPDF 创立有感)2026年10月26日
- PDF.js 深入浅出 7: PartialEvaluator2026年10月25日
- PDF.js 深入浅出 6: PDFManager.initDocument2026年10月25日
- PDF.js 深入浅出 5: PDFFetchStream 和 PDFWorkerStream2026年10月25日
- PDF.js 深入浅出 4: MessageHandler2026年10月22日
- PDF.js 深入浅出 3: getViewport & render2026年10月22日
- PDF.js 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF2026年10月19日
- PDF.js 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日