StreamPDFStreamPDF
返回博客

PDF.js 从 demo 深入浅出 5: render 方法全貌

2026年10月27日 · StreamPDF Team

PDF.js 从 demo 深入浅出 5: render 方法全貌

前言

在前面几篇文章中, 我们依次拿到了 page (PDFPageProxy) 与 viewport (PageViewport), 并把 canvas 的尺寸调整好. 回到第一篇的 demo, 最后一步, 第 19-22 行:

const renderContext = {
  canvasContext: context,
  viewport,
};
await page.render(renderContext);

传入 object 参数:

  • canvasContext: canvas 的 2d 上下文. 值为 canvas.getContext('2d') 的返回值
  • viewport: 视口对象. 值为上一篇 getViewport({ scale: 0.5 }) 的返回值

render 返回一个 RenderTask 对象. 本篇跟随这次调用进入源码.

render 调用

依据 api.js 中 PDFPageProxy.render 的定义, 代码执行流程如下.

第 1 步, 参数解构

render({
  canvasContext,
  canvas = canvasContext.canvas,
  viewport,
  intent = "display",
  annotationMode = AnnotationMode.ENABLE,
  transform = null,
  background = null,
  optionalContentConfigPromise = null,
  annotationCanvasMap = null,
  pageColors = null,
  printAnnotationStorage = null,
  isEditing = false,
  recordImages = false,
  recordOperations = false,
  operationsFilter = null,
}) { ... }

共 16 个参数, demo 只传了 2 个, 其余走默认值:

局部变量计算结果取值逻辑
canvascanvas 元素未传则取 canvasContext.canvas, 从上下文反取
intent"display"渲染意图: 显示
annotationMode1 (ENABLE)渲染注释
transformnull附加变换矩阵
backgroundnull背景色
optionalContentConfigPromisenull图层配置, 稍后补默认值
annotationCanvasMapnull注释画布映射
pageColorsnull页面配色 (无障碍场景)
printAnnotationStoragenull打印用注释存储
isEditingfalse是否编辑态
recordImagesfalse是否记录图片坐标
recordOperationsfalse是否记录绘制操作
operationsFilternull操作过滤器

随后第一行 this._stats?.time("Overall"): _stats 为 null (第 3 篇中 PDFPageProxy 构造时 pdfBug 为 false), 跳过.

第 2 步, 计算渲染意图

const intentArgs = this._transport.getRenderingIntent(
  "display", // intent
  1,         // annotationMode
  null,      // printAnnotationStorage
  false      // isEditing
);
const { renderingIntent, cacheKey } = intentArgs;

getRenderingIntent 内部是一组位运算 (RenderingIntentFlag), 代入 demo 的值:

  • intent 为 "display" → renderingIntent = 0x02 (DISPLAY)
  • 非 PRINT → annotationStorage 取 transport 上全局共享的 AnnotationStorage
  • annotationMode 为 ENABLE → 不加标志位
  • isEditing 为 false, 非 opList 调用 → 不加标志位
  • 存储中没有任何修改 → modifiedIds = { ids: Set(0), hash: "" }
  • cacheKey = [2, "", ""].join("_") → "2__"

返回 { renderingIntent: 2, cacheKey: "2__", annotationStorageSerializable: { map: null, hash: "", transfer: undefined }, modifiedIds }.

Q: cacheKey 是干什么的? A: 同一个页面对象上, 不同渲染意图 (显示/打印/注释已修改...) 各自维护一套独立的算子列表与渲染任务集合, 存放在页面的 _intentStates 这个 Map 里, key 就是 cacheKey. 只要意图不同, 缓存就互不干扰. demo 是首次渲染, "2__" 对应一个全新的 intentState.

第 3 步, 准备 intentState

this.#pendingCleanup = false;
optionalContentConfigPromise ||=
  this._transport.getOptionalContentConfig(2);
const intentState = this._intentStates.getOrInsertComputed("2__", makeObj);
const intentPrint = !!(2 & 0x04); // false

依次做了三件事:

  1. #pendingCleanup = false: 取消挂起的清理任务, 保证渲染期间不发生内存回收;
  2. optionalContentConfigPromise: 文档的图层配置 (OCProperties). 调用方未传, 则向 worker 拉取 (传输层带缓存). 图层机制另文展开;
  3. intentState: 本次渲染意图的状态槽, 首次创建为空对象.

第 4 步, 首次渲染, 启动算子列表拉取

if (!intentState.displayReadyCapability) {
  intentState.displayReadyCapability = Promise.withResolvers();
  intentState.operatorList = {
    fnArray: [],      // 操作码数组
    argsArray: [],    // 参数数组
    lastChunk: false, // 是否已收完
    separateAnnots: null,
  };
  this._pumpOperatorList(intentArgs);
}

displayReadyCapability 不存在, 说明这个意图从没请求过算子列表, 立即发起.

_pumpOperatorList 通过 messageHandler.sendWithStream("GetOperatorList", { pageId: 0, pageIndex: 0, intent: 2, cacheKey: "2__", ... }) 与 worker 建立流式通道: worker 解析页面内容流, 把绘制指令 (操作码 + 参数) 分块推回主线程; 主线程每收到一块就追加进 operatorList, 并通知该 intentState 下所有渲染任务「有新指令可画了」. 流式通讯与 worker 端的指令解析, 均另文展开.

Q: 为什么是流式而不是一次性返回? A: 大页面的指令解析耗时较长, 流式可以让渲染尽早开始 (边收边画), 首屏更快; 取消或失败时也能做局部清理. 相应的, worker 解析出首批可显示内容后会回发 StartRenderPage 消息, resolve 上面的 displayReadyCapability — 这就是第 7 步等待的前置条件之一.

第 5 步, 定义完成回调

const complete = error => { ... };

complete 闭包负责收尾: 从 intentState.renderTasks 移除本任务; 出错时 reject capability 并中止算子列表拉取; 正常时 resolve capability. demo 未开启任何记录开关, 相关分支全部跳过.

第 6 步, 创建 InternalRenderTask

const internalRenderTask = new InternalRenderTask({
  callback: complete,
  params: {
    canvas,
    canvasContext,
    dependencyTracker: null,
    imagesTracker: null,
    viewport,
    transform: null,
    background: null,
  },
  objs: this.objs,                  // 本页资源容器 (第 3 篇创建)
  commonObjs: this.commonObjs,      // 全文档共享资源容器
  annotationCanvasMap: null,
  operatorList: intentState.operatorList, // 与流式拉取共享同一个对象
  pageIndex: 0,
  canvasFactory: this._transport.canvasFactory, // 第 2 篇的 DOMCanvasFactory
  filterFactory: this._transport.filterFactory,
  useRequestAnimationFrame: true,   // 非打印
  pdfBug: false,
  pageColors: null,
  enableHWA: false,
  operationsFilter: null,
});
 
(intentState.renderTasks ||= new Set()).add(internalRenderTask);
const renderTask = internalRenderTask.task;

构造函数里有两个关键动作:

  • this.capability = Promise.withResolvers() — 渲染的成功/失败在这里汇合;
  • this.task = new RenderTask(this) — 对外暴露的句柄, 即 render 最终的返回值.

注意 operatorList 传的是引用: InternalRenderTask 与第 4 步的流式拉取共享同一个列表对象, 指令到达后即可被消费.

第 7 步, 等待前置条件, 启动绘制

Promise.all([
  intentState.displayReadyCapability.promise, // 算子列表收到首批可显示内容
  optionalContentConfigPromise,               // 图层配置就绪
])
  .then(([transparency, optionalContentConfig]) => {
    if (this.destroyed) {
      complete();
      return;
    }
    if (!(optionalContentConfig.renderingIntent & 2)) {
      throw new Error(
        "Must use the same `intent`-argument when calling the `PDFPageProxy.render` " +
          "and `PDFDocumentProxy.getOptionalContentConfig` methods."
      );
    }
    internalRenderTask.initializeGraphics({ transparency, optionalContentConfig });
    internalRenderTask.operatorListChanged();
  })
  .catch(complete);
 
return renderTask;

两个前置条件都就绪后:

  1. destroyed 检查: 等待期间页面可能已销毁;
  2. 图层配置的意图必须与本次渲染的意图兼容, 否则抛错 — 图层配置是按 intent 缓存的, 混用会导致图层状态错乱;
  3. initializeGraphics: 创建 CanvasGraphics (真正执行绘制指令的对象), 并检查同一个 canvas 不允许被两个渲染任务并发使用 (#canvasInUse 是一个 WeakSet, 复用会直接抛错);
  4. operatorListChanged: 开始消费 operatorList 中已到达的指令, 逐条画到 canvas 上.

initializeGraphics 与 CanvasGraphics 的绘制细节, 见第 10 篇.

返回值: renderTask

以上 .then 链在后台异步推进, render 本身同步返回 renderTask.

Q: demo 里 await page.render(renderContext) 等待渲染完成了吗? A: 没有. render 返回的是 RenderTask 对象, 它只有 promise getter, 没有 then 方法, 不是 thenable. await 一个非 thenable 值会立即以该值本身 resolve — 这行 await 实际上什么都没等. 正确写法是:

const renderTask = page.render(renderContext);
await renderTask.promise;

demo 看起来正常, 只是因为 await 之后没有其他代码, 渲染在后台推进并画到 canvas 上. 如果后续代码依赖「渲染已完成」(例如把 canvas 导出为图片), 就必须 await renderTask.promise.

至此 demo 的最后一行代码执行结束, 第一页被渲染到 canvas 上.

总结

本节跟随 demo 走完了 render({ canvasContext, viewport }) 的完整调用:

  1. 解构 16 个参数, demo 只传 canvasContext 与 viewport;
  2. getRenderingIntent 位运算: renderingIntent = 2, cacheKey = "2__";
  3. 准备 intentState, 补齐图层配置 promise;
  4. 首次渲染: 建立 GetOperatorList 流式通道, 边收边画;
  5. 创建 InternalRenderTask 与对外句柄 RenderTask;
  6. 前置条件就绪后 initializeGraphics + operatorListChanged, 开始绘制;
  7. 同步返回 renderTask; 渲染完成的信号挂在 renderTask.promise 上.

回看第一篇的 demo: getDocument → getPage → getViewport → render, 每一步都只是「发起」— 真正的解析与绘制发生在 worker 线程 (pdfManager, GetOperatorList) 与主线程的 CanvasGraphics 内部. 从下一篇开始, 我们进入这些「重角色」.

相关文章