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

前言
在前面几篇文章中, 我们依次拿到了 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 个, 其余走默认值:
| 局部变量 | 计算结果 | 取值逻辑 |
|---|---|---|
canvas | canvas 元素 | 未传则取 canvasContext.canvas, 从上下文反取 |
intent | "display" | 渲染意图: 显示 |
annotationMode | 1 (ENABLE) | 渲染注释 |
transform | null | 附加变换矩阵 |
background | null | 背景色 |
optionalContentConfigPromise | null | 图层配置, 稍后补默认值 |
annotationCanvasMap | null | 注释画布映射 |
pageColors | null | 页面配色 (无障碍场景) |
printAnnotationStorage | null | 打印用注释存储 |
isEditing | false | 是否编辑态 |
recordImages | false | 是否记录图片坐标 |
recordOperations | false | 是否记录绘制操作 |
operationsFilter | null | 操作过滤器 |
随后第一行 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依次做了三件事:
#pendingCleanup = false: 取消挂起的清理任务, 保证渲染期间不发生内存回收;optionalContentConfigPromise: 文档的图层配置 (OCProperties). 调用方未传, 则向 worker 拉取 (传输层带缓存). 图层机制另文展开;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;两个前置条件都就绪后:
destroyed检查: 等待期间页面可能已销毁;- 图层配置的意图必须与本次渲染的意图兼容, 否则抛错 — 图层配置是按 intent 缓存的, 混用会导致图层状态错乱;
initializeGraphics: 创建CanvasGraphics(真正执行绘制指令的对象), 并检查同一个 canvas 不允许被两个渲染任务并发使用 (#canvasInUse是一个 WeakSet, 复用会直接抛错);operatorListChanged: 开始消费operatorList中已到达的指令, 逐条画到 canvas 上.
initializeGraphics 与 CanvasGraphics 的绘制细节, 见第 10 篇.
返回值: renderTask
以上 .then 链在后台异步推进, render 本身同步返回 renderTask.
Q: demo 里
await page.render(renderContext)等待渲染完成了吗? A: 没有.render返回的是RenderTask对象, 它只有promisegetter, 没有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 }) 的完整调用:
- 解构 16 个参数, demo 只传
canvasContext与viewport; getRenderingIntent位运算:renderingIntent = 2,cacheKey = "2__";- 准备 intentState, 补齐图层配置 promise;
- 首次渲染: 建立
GetOperatorList流式通道, 边收边画; - 创建
InternalRenderTask与对外句柄RenderTask; - 前置条件就绪后
initializeGraphics+operatorListChanged, 开始绘制; - 同步返回
renderTask; 渲染完成的信号挂在renderTask.promise上.
回看第一篇的 demo: getDocument → getPage → getViewport → render, 每一步都只是「发起」— 真正的解析与绘制发生在 worker 线程 (pdfManager, GetOperatorList) 与主线程的 CanvasGraphics 内部. 从下一篇开始, 我们进入这些「重角色」.
相关文章
- 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 深入浅出 4: getViewport 方法全貌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日