StreamPDFStreamPDF
返回博客

PDF.js 从 demo 深入浅出 2: getDocument 方法全貌

2026年10月27日 · StreamPDF Team

PDF.js 从 demo 深入浅出 2: getDocument 方法全貌

前言

在前一篇文章中, 传入了一个参数, 是一个对象参数, 属性为

  • src: PDF 文件的 URL. 值为 https://assets.streampdf.com/PDF32000_2008.pdf

getDocument 调用

依据display/api.js 中 getDocument 的定义, 代码执行流程如下

第 1-2 行, 初始化 loading 任务

实例化 PDFDocumentLoadingTask 类: 点击查看调用, 并将返回值赋给 task 变量

从 task 中取出 docId 变量, 此时 docId 为 d1

第 3-15 行, 参数校验&处理

这一段代码把用户传入的 src 对象展开成 35+ 个局部变量: 传入的做校验和类型转换, 未传入的取默认值. demo 只传了 url, 因此绝大多数参数走默认分支.

唯一传入的 url, 经 getUrlProp 转换为 URL 对象:

const url = URL.parse('https://assets.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;

其余参数全部未传入. 按源码顺序, 计算结果如下:

局部变量计算结果取值逻辑
datanull传入则经 getDataProp 统一转为 Uint8Array
httpHeadersnull原样透传, 未传为 null
withCredentialsfalse仅接受 === true
passwordnull?? null
rangeTransportnull仅接受 PDFDataRangeTransport 实例
rangeChunkSize2 ** 16 (64KB)正整数才接受传入值
workernull仅接受 PDFWorker 实例
verbosityundefined原样取 src.verbosity
docBaseUrlnull仅接受非 data: 协议的 string
cMapUrlnullgetFactoryUrlProp: 非 string 为 null, 且必须以 / 结尾
cMapPackedtrue!== false
iccUrlnull同 cMapUrl
standardFontDataUrlnull同 cMapUrl
wasmUrlnull同 cMapUrl
ignoreErrorstruesrc.stopAtErrors !== true, 见下文
maxImageSize-1 (不限制)整数且 > -1 才接受
isOffscreenCanvasSupportedtrue未传则取 !isNodeJS
isImageDecoderSupportedtrue未传则取 !isNodeJS
canvasMaxAreaInBytes-1 (不限制)整数才接受
disableFontFacefalse未传则取 isNodeJS
fontExtraPropertiesfalse仅接受 === true
enableXfafalse仅接受 === true
ownerDocumentglobalThis.document未传则取全局 document
disableRangefalse仅接受 === true
disableStreamfalse仅接受 === true
disableAutoFetchfalse仅接受 === true
pdfBugfalse仅接受 === true
CanvasFactoryDOMCanvasFactory环境分支, 见下文
FilterFactoryDOMFilterFactory同上
BinaryDataFactoryDOMBinaryDataFactory同上
enableHWAfalse仅接受 === true
enableWebGPUfalse仅接受 === true
gpuPromisePromise.resolve(false)未开 WebGPU, 不启动 GPU 初始化
useWasmtrue!== false
pagesMappernew PagesMapper()未传则新建
useSystemFontstrue级联默认, 见下文
useWorkerFetchfalse复合判断, 见下文 Q&A
styleElementnull仅 TESTING 构建保留传入值, 生产恒为 null

以上就是这一段代码产生的全部局部变量. 其中大部分判断是平凡的「未传取默认」, 但有几个值得展开.

ignoreErrors: 语义取反的内部变量

用户面向的参数叫 stopAtErrors, 内部变量却叫 ignoreErrors, 取值逻辑是 src.stopAtErrors !== true. 也就是说默认行为是忽略错误: 解析出错时尽量恢复现场, 而不是 reject getOperatorList / getTextContent / RenderTask 等 promise. 看源码时这里极易把名字和值抄反.

rangeChunkSize: 64KB 的伏笔

rangeChunkSize 默认为 64KB. 由于 disableRange = false, 后文创建 transport 时会尝试对 PDF 发起 range 请求, 每次拉取的块大小就是这个值. 先记住它.

docBaseUrl: 为什么排除 data: URL

源码注释给出了原因: data: URL 无法用来还原绝对路径, 而且它内含整个 PDF 文档, 长度没有上限, 因此不将它发往 worker 线程.

gpuPromise: 与 worker 启动并行的设计

const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);

即使不启用 WebGPU, 这里也立即返回一个 resolved promise, 而不是把判断推迟到使用时. 注释说明了意图: 让 GPU 初始化与 worker 启动并行执行, 解析出的布尔值随后转发给 worker.

三个 Factory: 环境分支

const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC 构建 && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);

FilterFactory, BinaryDataFactory 同理. 判断分三层: 用户自定义 > Node 环境实现 > DOM 实现. demo 运行在浏览器且未自定义, 因此三个工厂都是 DOM 版本. 三者分工:

  • CanvasFactory: 创建渲染用的 canvas
  • FilterFactory: 创建图像滤镜 (如 SVG 滤镜)
  • BinaryDataFactory: 下载内置静态资源, 与下文的 useWorkerFetch 联动

useSystemFonts: 默认值依赖其他参数

const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;

它的默认值依赖前面刚解析出的 disableFontFace. 为什么? PDF 中未嵌入的字体会被转成 FontFace 加载; 如果 disableFontFace 为 true, 这条加载路径被禁用, 此时再用系统字体兜底会导致字形完全错乱. 因此该场景下默认值索性也为 false, 避免渲染出彻底坏掉的字体.

useWorkerFetch: 复合判断

const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
//   ? src.useWorkerFetch
//   : MOZCENTRAL ||
//     !!(
//       BinaryDataFactory === DOMBinaryDataFactory &&
//       cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
//       isValidFetchUrl(cMapUrl, document.baseURI) &&
//       isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
//       isValidFetchUrl(wasmUrl, document.baseURI)
//     );

Q: 这里的判断在做什么? A: 在决定「内置静态资源」(CMap 文件, 标准字体文件, wasm 文件) 由谁去下载: worker 线程自己用 Fetch API 下载 (true), 还是主线程下载后转发给 worker (false).

如果用户显式传了 boolean, 直接用用户的值. 否则计算默认值: MOZCENTRAL (Firefox 内置 viewer) 恒为 true; 普通构建必须同时满足三个条件才为 true:

  1. 没有自定义 BinaryDataFactory —— 因为 useWorkerFetch 为 true 时该参数会被直接忽略 (后面 transportFactory 里 binaryDataFactory 会被置为 null), 用户自定义的工厂必须被尊重, 所以不能默认 true;
  2. cMapUrl / standardFontDataUrl / wasmUrl 都已提供, 且 cMapPacked 为 true —— 没有 URL 就无从 fetch;
  3. 这三个 URL 相对 document.baseURI 解析后是 http/https —— Fetch API 只支持 http/https, 不支持 file/ftp.

本文的 demo 只传了 url, cMapUrl 为 null, 条件 2 不满足, 因此 useWorkerFetch = false: 走主线程路径, 由 DOMBinaryDataFactory 在主线程下载, worker 需要时通过 FetchBinaryData 消息向主线程索取.

第 16 行, 设置主线程日志级别

setVerbosityLevel(verbosity); // verbosity 为 undefined

verbosity 为 undefined, 主线程日志级别保持默认. 注意这只影响主线程; worker 线程的日志级别会在创建 worker 时单独传入.

第 17-25 行, 实例化三个工厂

const transportFactory = {
  canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
  filterFactory: new DOMFilterFactory({ docId: 'd1', ownerDocument: document }),
  binaryDataFactory: new DOMBinaryDataFactory({
    cMapUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
  }),
};
// binaryDataFactory: MOZCENTRAL 或 useWorkerFetch 为 true 时为 null

将前面选出的三个 Factory 类实例化, 打包为 transportFactory, 随后交给 transport 使用. 由于 useWorkerFetch = false, binaryDataFactory 不为 null —— 与上一节的 Q&A 呼应: 内置静态资源走主线程下载.

三个工厂实例内部的能力, 另文展开.

第 26-33 行, 创建 Worker

worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port 来自 GlobalWorkerOptions.workerPort, demo 未设置
task._worker = worker;

demo 未传入 worker, 因此 PDF.js 自己创建并拥有一个. PDFWorker.create 同步返回一个 PDFWorker 实例, 其构造函数会读取 GlobalWorkerOptions.workerSrc (即第一篇中配置的 worker 文件地址), 创建 Worker 并通过 ready / test 消息完成启动握手 —— 这是重点, 已在第 8 篇详细展开. 这里只需记住: 返回的 worker 对象上有一个 promise, worker 就绪后 resolve.

第 34-63 行, 组装两份参数包

第一份 docParams, 随后会跨线程发往 worker:

const docParams = {
  docId: 'd1',
  apiVersion: '6.3.289', // 构建版本号
  data: null,
  password: null,
  disableAutoFetch: false,
  rangeChunkSize: 65536,
  docBaseUrl: null,
  enableXfa: false,
  evaluatorOptions: {
    maxImageSize: -1,
    disableFontFace: false,
    ignoreErrors: true,
    isOffscreenCanvasSupported: true,
    isImageDecoderSupported: true,
    canvasMaxAreaInBytes: -1,
    fontExtraProperties: false,
    useSystemFonts: true,
    useWasm: true,
    useWorkerFetch: false,
    cMapUrl: null,
    cMapPacked: true,
    iccUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
    hasGPU: false, // 占位, 稍后回填
  },
};

第二份 transportParams, 只留在主线程:

const transportParams = {
  ownerDocument: document,
  pdfBug: false,
  styleElement: null,
  enableHWA: false,
  loadingParams: { disableAutoFetch: false, enableXfa: false },
};

一句话区分: docParams 是发给 worker 的「工作指令」, transportParams 是主线程自留的「本地配置」.

第 64 行起, worker 就绪后的握手

Promise.all([worker.promise, gpuPromise]).then(...)

worker 就绪与 GPU 探测 (resolve 为 false) 都完成后, 依次发生:

  1. 将 hasGPU = false 回填进 evaluatorOptions;
  2. 向 worker 发送 GetDocRequest 消息, 载荷为 docParams. data 为 null, 因此没有需要转移所有权的 buffer;
  3. 选择网络流: data 为 null 且 rangeTransport 为 null, 走 url 分支, 实例化网络流 (优先 fetch 实现, 不支持则回退 XHR; 内部如何发 range 请求, 见第 9 篇):
networkStream = new NetworkStream({
  url,
  httpHeaders: null,
  withCredentials: false,
  rangeChunkSize: 65536,
  disableRange: false,
  disableStream: false,
});
  1. 收到 worker 对 GetDocRequest 的回执 (workerId) 后, 创建 MessageHandler 与 WorkerTransport, 并挂到 task._transport 上 —— 两个核心类分别在第 6、7 篇展开;
  2. 向 worker 发送 Ready 消息, 主线程侧宣告就绪; worker 的 Ready 处理函数随后调用 setupDoc(docParams).

返回值: task

以上 .then 链全部在后台异步推进, getDocument 本身同步返回 task. 链路中任何一步出错都会 reject task 内部的 capability, 也就是 demo 中 await loadingTask.promise 所等待的那个 promise.

至此 getDocument 执行完毕.

总结

本节跟随 demo 走完了 getDocument 的完整调用:

  1. 创建 PDFDocumentLoadingTask, 取出 docId = 'd1';
  2. 将 src 展开为 35+ 个局部变量, demo 只传了 url, 其余全部落到默认值;
  3. 实例化三个 DOM 工厂, 打包为 transportFactory;
  4. PDFWorker.create 创建 worker 并启动握手;
  5. 组装 docParams (发往 worker) 与 transportParams (留在主线程);
  6. worker 就绪后发送 GetDocRequest, 建立网络流, 创建 MessageHandler 与 WorkerTransport, 最后发送 Ready;
  7. 同步返回 task, demo 通过 await loadingTask.promise 等待文档就绪.

过程中刻意略过了四个「重角色」: PDFWorker 的创建与握手, MessageHandler 的消息机制, WorkerTransport 的初始化, 以及网络流的 range 请求策略. 它们各自独立成文, 从下一篇开始逐个深入.

相关文章