StreamPDFStreamPDF
返回博客

PDF.js 从 demo 深入浅出 4: getViewport 方法全貌

2026年10月27日 · StreamPDF Team

PDF.js 从 demo 深入浅出 4: getViewport 方法全貌

前言

在上一篇文章中, 我们通过 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.5

userUnit 是 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 = 0
  • y' = 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 }) 的完整计算:

  1. 参数解构: scale = 0.5, rotation 默认取页面 /Rotate 值 0, 其余为 0/false;
  2. scale 吸收 userUnit, 仍为 0.5;
  3. 计算页面中心点 (297.5, 421);
  4. 旋转归一化后取旋转向量 (1, 0, 0, -1), rotateD = -1 承担 y 轴翻转;
  5. 计算宽高 297.5 x 421 与画布偏移 (148.75, 210.5);
  6. 组装 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 上下文. 下一篇展开.

相关文章