StreamPDFStreamPDF
返回博客

PDF.js 深入浅出 2: getDocument & getPage

2026年10月19日 · StreamPDF Team

PDF.js 深入浅出 2: getDocument & getPage

前言

这是 PDF.js 深入浅出系列的第二篇,从第一篇的架构总览出发,深入 Display 层,追踪 getDocument 和 getPage 的执行路径。

本系列所有代码片段均基于 PDF.js v6.3.289。

getDocument 方法

第一步:创建加载任务与全局 ID 生成器

// Source: src/display/api.js
const task = new PDFDocumentLoadingTask();
const { docId } = task;

PDFDocumentLoadingTask 同时充当 PDF.js 的全局 ID 生成器——它产出的 docId 会在整个 Worker 内部用于标识这份文档。

第二步:处理用户参数

const url = src.url ? getUrlProp(src.url) : null;
const data = src.data ? getDataProp(src.data) : null;
const httpHeaders = src.httpHeaders || null;
const withCredentials = src.withCredentials === true;
const password = src.password ?? null;
// ...
const transportFactory = {
  canvasFactory: new CanvasFactory({ ownerDocument, enableHWA }),
  filterFactory: new FilterFactory({ docId, ownerDocument }),
  binaryDataFactory:
    (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) || useWorkerFetch
      ? null
      : new BinaryDataFactory({ cMapUrl, standardFontDataUrl, wasmUrl }),
};

第三步:创建 Worker

if (!worker) {
  // Worker was not provided -- creating and owning our own. If message port
  // is specified in global worker options, using it.
  worker = PDFWorker.create({
    verbosity,
    port: GlobalWorkerOptions.workerPort,
  });
  task._worker = worker;
}

调用方可以传入自己的 Worker 实例,但常规场景下 PDF.js 会自行处理 Worker 的创建。

PDFWorker 是一个类,我们调用它的静态 create 方法。

进入 getDocument → PDFWorker.create

// Source: src/display/api.js
static create(params) {
  const cachedPort = this.#workerPorts.get(params?.port);
  if (cachedPort) {
    if (cachedPort._pendingDestroy) {
      throw new Error(
        "PDFWorker.create - the worker is being destroyed.\n" +
          "Please remember to await `PDFDocumentLoadingTask.destroy()`-calls."
      );
    }
    return cachedPort;
  }
  return new PDFWorker(params);
}

PDFWorker 内部维护了一个静态 #workerPorts Map,以 port 为 key 缓存已创建的 Worker 实例,避免重复构造。如果有缓存直接返回,否则新建一个 PDFWorker。

进入 getDocument → PDFWorker.create → PDFWorker

// Source: src/display/api.js
constructor({
  name = null,
  port = null,
  verbosity = getVerbosityLevel(),
} = {}) {
  this.name = name;
  this.destroyed = false;
  this.verbosity = verbosity;
 
  if (port) {
    if (PDFWorker.#workerPorts.has(port)) {
      throw new Error("Cannot use more than one PDFWorker per port.");
    }
    PDFWorker.#workerPorts.set(port, this);
    this.#initializeFromPort(port);
  } else {
    this.#initialize();
  }
 
  if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
    // For testing purposes.
    Object.defineProperty(this, "_webWorker", {
      get() {
        return this.#webWorker;
      },
    });
  }
}

我们没有传入 port,所以构造函数会走 #initialize 分支。

进入 getDocument → PDFWorker.create → PDFWorker → #initialize

if (PDFWorker.#isWorkerDisabled || PDFWorker.#mainThreadWorkerMessageHandler) {
  this.#setupFakeWorker();
  return;
}
let { workerSrc } = PDFWorker;
// ...
if (
  typeof PDFJSDev !== 'undefined' &&
  PDFJSDev.test('GENERIC') &&
  !PDFWorker._isSameOrigin(window.location, workerSrc)
) {
  workerSrc = PDFWorker._createCDNWrapper(new URL(workerSrc, window.location).href);
}
const worker = new Worker(workerSrc, { type: 'module' });
const messageHandler = new MessageHandler('main', 'worker', worker);
// ...

首先判断是否需要回退到 "fake worker"(在主线程里跑 PDF 解析)。我们走正常路径,跳过 fake-worker 分支,从一个静态 getter 拿到 workerSrc。

进入 getDocument → PDFWorker.create → PDFWorker → #initialize → workerSrc

static get workerSrc() {
  if (GlobalWorkerOptions.workerSrc) {
    return GlobalWorkerOptions.workerSrc;
  }
  throw new Error('No "GlobalWorkerOptions.workerSrc" specified.');
}

这就是我们在 demo 最开始设置的那个 options:

pdfjsLib.GlobalWorkerOptions.workerSrc =
  'https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.min.mjs';

回到 getDocument → PDFWorker.create → PDFWorker → #initialize

接下来处理 Worker 可能的跨域问题:如果 workerSrc 和当前页面不同源,PDF.js 会把它包进 Blob 生成 blob URL 来绕过浏览器同源限制。我们的 demo 中 Worker 脚本是同源的,所以跳过这个分支。

什么是 Worker 的跨域问题?

浏览器原生 new Worker(url) 只接受同源的脚本 URL。如果 workerSrc 指向 CDN(比如 https://cdn.jsdelivr.net/...),与当前页面不同域,浏览器会直接拒绝创建 Worker,报错 Failed to construct 'Worker'。

PDF.js 的解法是 _createCDNWrapper:动态生成一行 await import("${url}"); 的 JS 代码,用 Blob 包起来,再通过 URL.createObjectURL 生成一个 blob URL。blob URL 的 origin 继承自创建它的页面,所以 new Worker(blobUrl) 对浏览器来说就是同源的;而 wrapper 里的 import 本身支持跨域加载 ES Module,这样就间接绕过了 Worker 的同源限制。

我们的 demo 里 Worker 脚本同源,_isSameOrigin 检测返回 true,这个 wrapper 分支不会触发,直接用原始 URL 创建 Worker。

紧接着 Worker 以 ES Module 方式被实例化:

const worker = new Worker(workerSrc, { type: 'module' });

主线程到 Worker 的通信管道也创建好了:

const messageHandler = new MessageHandler('main', 'worker', worker);

MessageHandler 是对底层 postMessage 通信的封装抽象。

Worker ↔ main 握手

然后 PDF.js 在两个线程之间做一次简短的握手:

const terminateEarly = () => {
  ac.abort();
  messageHandler.destroy();
  worker.terminate();
  if (this.destroyed) {
    this.#capability.reject(new Error('Worker was destroyed'));
  } else {
    // Fall back to fake worker if the termination is caused by an
    // error (e.g. NetworkError / SecurityError).
    this.#setupFakeWorker();
  }
};
 
const ac = new AbortController();
worker.addEventListener(
  'error',
  () => {
    if (!this.#webWorker) {
      // Worker failed to initialize due to an error. Clean up and fall
      // back to the fake worker.
      terminateEarly();
    }
  },
  { signal: ac.signal },
);
 
messageHandler.on('test', data => {
  ac.abort();
  if (this.destroyed || !data) {
    terminateEarly();
    return;
  }
  this.#messageHandler = messageHandler;
  this.#port = worker;
  this.#webWorker = worker;
 
  this.#resolve();
});
 
messageHandler.on('ready', data => {
  ac.abort();
  if (this.destroyed) {
    terminateEarly();
    return;
  }
  try {
    sendTest();
  } catch {
    // We need fallback to a faked worker.
    this.#setupFakeWorker();
  }
});
 
const sendTest = () => {
  const testObj = new Uint8Array();
  // Ensure that we can use `postMessage` transfers.
  messageHandler.send('test', testObj, [testObj.buffer]);
};
 
// It might take time for the worker to initialize. We will try to send
// the "test" message immediately, and once the "ready" message arrives.
// The worker shall process only the first received "test" message.
sendTest();

它监听 error、ready 和 test 三个事件,然后立即调用一次 sendTest 来探测通道。

我们来看看 new Worker(...) 时 Worker 那边发生了什么。

进入 main → new Worker

// Source: src/core/worker.js
static {
  // Worker thread (and not Node.js)?
  if (
    typeof window === "undefined" &&
    !isNodeJS &&
    typeof self !== "undefined" &&
    /* isMessagePort = */
    typeof self.postMessage === "function" &&
    "onmessage" in self
  ) {
    this.initializeFromPort(self);
  }
}

Worker 的入口是 WorkerMessageHandler 类里的一个 static 代码块,它检测自己是否跑在 Worker 环境中,如果是就调用 initializeFromPort。

进入 main → new Worker → initializeFromPort

static initializeFromPort(port) {
  const handler = new MessageHandler("worker", "main", port);
  this.setup(handler, port);
  handler.send("ready", null);
}

它先创建一个 worker→main 方向的 MessageHandler,调用 setup,然后立刻给 main 回一条 ready。

进入 main → new Worker → initializeFromPort → setup

// Source: src/core/worker.js (WorkerMessageHandler.setup)
static setup(handler, port) {
  let testMessageProcessed = false;
  handler.on("test", data => {
    if (testMessageProcessed) {
      return; // we already processed 'test' message once
    }
    testMessageProcessed = true;
 
    // Ensure that `TypedArray`s can be sent to the worker.
    handler.send("test", data instanceof Uint8Array);
  });
 
  handler.on("configure", data => {
    setVerbosityLevel(data.verbosity);
  });
 
  handler.on("GetDocRequest", data => this.createDocumentHandler(data, port));
 
  if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
    handler.on("GetWorkerCoverage", function () {
      return globalThis.__coverage__ ?? {};
    });
  }
}

Worker 注册了三个消息处理器:test、configure 和 GetDocRequest。其中 test 处理器是一次性的——收到第一条就把 testMessageProcessed 置为 true,后续再来的 test 消息会被静默丢弃。

现在回到主线程:main 发完了初始 test 探测,Worker 又发来了 ready,main 收到 ready 后会再次调用 sendTest。但 Worker 的 testMessageProcessed 大概率已经是 true,所以这条重试会被丢掉——只有最早那一条 test 会得到 Worker 的回执。main 拿到回执后调用 #resolve(),worker.promise 完成。

完整的握手时序图如下:

main ↔ worker 启动握手时序图

两条竖线分别代表 main(左)和 worker(右),时间自上而下流动。

① new Worker 之后 main 立即发出一条 test 探测,用来验证 postMessage 的 transfer 能力;

② Worker 执行完 initializeFromPort 后回一条 ready,告知 main 自己已经就绪;

③ main 收到 ready 后重发 test,但 Worker 只认第一条——testMessageProcessed 是一次性开关,消费过第一条就置为 true,所以这条重发的 test(图中以虚线表示)会在 if (testMessageProcessed) { return; } 处被直接丢弃;

④ Worker 仅对第一条 test 作出回执;main 收到回执后移除 error 监听器并 #resolve(),worker.promise 完成。

Q: 被消费的是哪一条 test 消息?为什么还要重发一次? A: 被消费的始终是第一条,也就是 ① 里 new Worker 之后立刻发出去的那条。Worker 侧的 testMessageProcessed 是一次性开关:第一条进来时还是 false,于是置为 true 并回一条 test 回执;之后到达的任何 test 都会在 if (testMessageProcessed) { return; } 处被直接丢掉。

为什么能确定是第一条?main 与 worker 之间的消息走同一个 MessagePort,是有序的:第一次 sendTest()(创建后立即发送)一定排在 ready 之后补发的那次前面。而 Worker 只有在初始脚本执行完、setup() 注册好 test 监听之后才会开始派发消息队列,所以第一条不会被"错过",必然落到那个开关上。

那重发机制的意义是什么?源码注释点明了动机——"It might take time for the worker to initialize." Worker 起得比 main 的调用慢,main 并不知道它何时可用,于是采取「先乐观地发一次 + 收到 ready 后再补发一次」的双保险。极端情况下第一次没有被事件循环派发,第二次也能兜底,两次里只要有一次被消费,握手就成立;testMessageProcessed 则保证重复的那条不会带来重复回执,让整个过程保持幂等。

另外,ready 回调里的 try { sendTest(); } catch { this.#setupFakeWorker(); } 还让这次补发顺带充当了一次「通道体检」:如果连消息都发不出去,说明这条 Worker 链路不可用,直接降级到 fake worker。所以重发的本质是握手探测的兜底与幂等,而不是"第二条才真正生效"。

阶段小结 ✅ Worker 握手完成:main 创建 Worker 线程,通过 test / ready / test 三次消息交互确认通道可用,Worker 实例就绪。

main → Worker:GetDocRequest

握手完成后,main 等待 Worker 和 GPU 都就绪——GPU 检查(gpuPromise)用于决定后续渲染路径是否能启用硬件加速。

Promise.all([worker.promise, gpuPromise])
  .then(function ([, hasGPU]) {
    if (worker.destroyed) {
      throw new Error("Worker was destroyed");
    }
 
    docParams.evaluatorOptions.hasGPU = hasGPU;
 
    const workerIdPromise = worker.messageHandler.sendWithPromise(
      "GetDocRequest",
      docParams,
      data ? [data.buffer] : null
    );

main 通过 Worker 级别的 messageHandler 发送 GetDocRequest 消息。

Worker 侧的处理器注册在 setup 里:

Worker: on GetDocRequest

handler.on('GetDocRequest', data => this.createDocumentHandler(data, port));

它会调用 createDocumentHandler:

Worker: on GetDocRequest → createDocumentHandler

const workerHandlerName = docId + '_worker';
let handler = new MessageHandler(workerHandlerName, docId, port);

校验参数之后,它为这份文档创建一个独立的 MessageHandler,在上面注册一长串消息处理器(GetPage、GetDestinations、GetMetadata、GetOutline、Terminate 等),然后把 workerHandlerName 返回给 main。

回到 getDocument 中:

} else if (url) {
  if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
    throw new Error("Not implemented: NetworkStream");
  }
  const NetworkStream = getNetworkStream(url);
 
  networkStream = new NetworkStream({
    url,
    httpHeaders,
    withCredentials,
    rangeChunkSize,
    disableRange,
    disableStream,
  });
} else {

由于我们传了 url,PDF.js 实例化一个 NetworkStream。然后等待 workerIdPromise resolve:

return workerIdPromise.then(workerId => {
  if (worker.destroyed) {
    throw new Error('Worker was destroyed');
  }
 
  const messageHandler = new MessageHandler(docId, workerId, worker.port);
  const transport = new WorkerTransport(
    messageHandler,
    task,
    networkStream,
    transportParams,
    transportFactory,
    pagesMapper,
  );
  task._transport = transport;
 
  if (task.destroyed) {
    // `destroy()` was called during the worker handshake; the orderly
    // shutdown (including the "Terminate" message) will be issued
    // through the transport once destroy resumes.
    throw new Error('Loading aborted');
  }
  messageHandler.send('Ready', null);
});

main 创建了文档级别的 MessageHandler 和一个 WorkerTransport——这是管理文档整个生命周期的核心对象。最后发送 Ready 消息,正式启动加载流程。

阶段小结 ✅ GetDocRequest 完成:Worker 为该文档注册独立的消息处理链路,返回 workerHandlerName;main 创建 WorkerTransport 并发送 Ready 启动加载。

main → Worker:Ready

进入 Worker: on Ready → setupDoc

// Source: src/core/worker.js
handler.on('Ready', function () {
  setupDoc(docParams);
  docParams = null; // we don't need docParams anymore -- saving memory.
});

setupDoc 是核心的设置函数。它定义了三个辅助函数——onSuccess、onFailure 和 pdfManagerReady——然后调用 getPdfManager。

进入 Worker: on Ready → setupDoc → getPdfManager

const pdfManagerArgs = {
  source: null,
  disableAutoFetch,
  docBaseUrl,
  docId,
  enableXfa,
  evaluatorOptions,
  handler,
  length: 0,
  password,
  rangeChunkSize,
};
 
if (data) {
  pdfManagerArgs.source = data;
 
  return new LocalPdfManager(pdfManagerArgs);
}
const pdfStream = new PDFWorkerStream({ msgHandler: handler }),
  fullReader = pdfStream.getFullReader();

如果直接传了原始字节 data,PDF.js 用 LocalPdfManager。对于 URL 加载,它会创建 PDFWorkerStream 来增量读取数据。

进入 Worker: on Ready → setupDoc → getPdfManager → PDFWorkerStream

class PDFWorkerStream extends BasePDFStream {
  constructor(source) {
    super(source, PDFWorkerStreamReader, PDFWorkerStreamRangeReader);
  }
}

PDFWorkerStream 继承自 BasePDFStream,并注入了两种 reader 实现。

回到 Worker: on Ready → setupDoc → getPdfManager

fullReader = pdfStream.getFullReader();

getFullReader 继承自 BasePDFStream:

getFullReader() {
  assert(
    !this._fullReader,
    "BasePDFStream.getFullReader can only be called once."
  );
  return (this._fullReader = new this.#PDFStreamReader(this));
}

它会实例化 PDFWorkerStreamReader——关键在于,它会通过 msgHandler 给 main 发一条 GetReader 消息:

class PDFWorkerStreamReader extends BasePDFStreamReader {
  _reader = null;
 
  constructor(stream) {
    super(stream);
    const { msgHandler } = stream._source;
 
    const readableStream = msgHandler.sendWithStream('GetReader');
    this._reader = readableStream.getReader();
 
    msgHandler.sendWithPromise('ReaderHeadersReady').then(data => {
      this._contentLength = data.contentLength;
      this._isStreamingSupported = data.isStreamingSupported;
      this._isRangeSupported = data.isRangeSupported;
 
      this._headersCapability.resolve();
    }, this._headersCapability.reject);
  }
  // ...
}

main 侧的 WorkerTransport 处理 GetReader 消息,调用 networkStream.getFullReader(),最终发出第一条真正的 HTTP 请求——对 PDF 文件本身的 fetch 调用:

constructor(stream) {
  super(stream);
  const {
    disableRange,
    disableStream,
    rangeChunkSize,
    url,
    withCredentials,
  } = stream._source;
 
  this._isStreamingSupported = !disableStream;
  const headers = new Headers(stream.headers);
 
  fetchUrl(url, headers, withCredentials, this._abortController)
    .then(response => {
      stream._responseOrigin = getResponseOrigin(response.url);
      ensureResponseStatus(response.status, url);
      this._reader = response.body.getReader();
 
      const responseHeaders = response.headers;
      const { contentLength, isRangeSupported } =
        validateRangeRequestCapabilities({
          responseHeaders,
          isHttp: true,
          rangeChunkSize,
          disableRange,
        });
      this._contentLength = contentLength;
      this._isRangeSupported = isRangeSupported;
      this._filename = extractFilenameFromHeader(responseHeaders);
 
      // We need to stop reading when range is supported and streaming is
      // disabled.
      if (!this._isStreamingSupported && this._isRangeSupported) {
        this.cancel(new AbortException("Streaming is disabled."));
      }
 
      this._headersCapability.resolve();
    })
    .catch(this._headersCapability.reject);
}

这是第一次网络请求。PDF.js 拿到响应后检查 content-length、Accept-Ranges、Content-Disposition 等响应头。

有一个值得注意的细节:

// We need to stop reading when range is supported and streaming is
// disabled.
if (!this._isStreamingSupported && this._isRangeSupported) {
  this.cancel(new AbortException('Streaming is disabled.'));
}

如果服务器支持 range 请求但我们关闭了 streaming,PDF.js 会主动 abort 掉这次初始 fetch。你可能会看到请求发出后立刻被取消——这不是 bug,而是有意为之:探测请求的唯一目的是判断服务器是否支持 range。一旦决定了,PDF.js 要么切到 NetworkPdfManager(range 可用,按需读取),要么 LocalPdfManager(一次性下载全部)。

这是 StreamPDF 的一个优化点:我们的后端保证 OSS 始终支持 range 请求,从而省去了这次探测请求,直接进入下一阶段。

回到 getPdfManager,一旦 headers 确定:

fullReader.headersReady.then(() => {
  if (!fullReader.isRangeSupported) {
    return;
  }
  pdfManagerArgs.source = pdfStream;
  pdfManagerArgs.length = fullReader.contentLength;
  pdfManagerArgs.disableAutoFetch ||= fullReader.isStreamingSupported;
 
  newPdfManager = new NetworkPdfManager(pdfManagerArgs);
  // ...
  resolve(newPdfManager);
});

如果 range 可用 → NetworkPdfManager(增量读取网络数据)。不可用 → 探测请求跑完后全部字节会被缓冲,改用 LocalPdfManager。

async function readData() {
  let loaded = 0;
  while (true) {
    const { value, done } = await fullReader.read();
    ensureNotTerminated();
    if (done) break;
    // ...
    if (!fullReader.isStreamingSupported) {
      handler.send('DocProgress', { loaded, total: fullReader.contentLength });
    }
    if (newPdfManager) {
      newPdfManager.sendProgressiveData(value);
    } else {
      cachedChunks.push(value);
    }
  }
  if (!newPdfManager) {
    pdfManagerArgs.source = arrayBuffersToBytes(cachedChunks);
    newPdfManager = new LocalPdfManager(pdfManagerArgs);
    resolve(newPdfManager);
  }
}
readData();

Q: 为什么叫 LocalPdfManager? 因为一旦所有字节都读到了内存里,代码逻辑就和处理本地 PDF 完全一致了——"local" 指的是数据已完全可用,而不是来自磁盘。

getPdfManager resolve 之后,setupDoc 继续:

getPdfManager(data)
  .then(function (newPdfManager) {
    // ...
    pdfManager = newPdfManager;
 
    pdfManager.requestLoadedStream(/* noFetch = */ true).then(
      stream => {
        handler.send('DataLoaded', { length: stream.bytes.byteLength });
      },
      () => {
        /* avoid errors if terminated */
      },
    );
  })
  .then(pdfManagerReady, onFailure);

然后 pdfManagerReady → loadDocument:

进入 Worker: on Ready → pdfManagerReady → loadDocument

// Source: src/core/worker.js (createDocumentHandler 内的闭包)
async function loadDocument(recoveryMode) {
  await pdfManager.initDocument(recoveryMode);
 
  const isPureXfa = await pdfManager.ensureDoc('isPureXfa');
  if (isPureXfa) {
    const task = new WorkerTask('loadXfaResources');
    startWorkerTask(task);
    await pdfManager.ensureDoc('loadXfaResources', [handler, task]);
    finishWorkerTask(task);
  }
 
  const [numPages, fingerprints] = await Promise.all([
    pdfManager.ensureDoc('numPages'),
    pdfManager.ensureDoc('fingerprints'),
  ]);
 
  const htmlForXfa = isPureXfa ? await pdfManager.ensureDoc('htmlForXfa') : null;
 
  return { numPages, fingerprints, htmlForXfa };
}

在 initDocument(继承自 BasePdfManager)内部,PDF.js 会做一系列完整性校验——解析 header、解析 XRef 表、解析文档、验证第一页和最后一页能否正常加载。这里不展开,留待后续文章。

校验通过后调用 onSuccess,把文档元信息发回 main:

function onSuccess(doc) {
  ensureNotTerminated();
  handler.send('GetDoc', { pdfInfo: doc });
}

阶段小结 ✅ Ready 完成:Worker 初始化 PdfManager,通过 fetch 探测网络能力(range / streaming),创建 NetworkPdfManager 或 LocalPdfManager,完成文档完整性校验,通过 GetDoc 把元信息发回 main。

Worker → main:GetDoc

进入 main: on GetDoc

messageHandler.on('GetDoc', ({ pdfInfo }) => {
  this.pagesMapper.pagesNumber = pdfInfo.numPages;
  this._numPages = pdfInfo.numPages;
  this._htmlForXfa = pdfInfo.htmlForXfa;
  delete pdfInfo.htmlForXfa;
  loadingTask._capability.resolve(new PDFDocumentProxy(pdfInfo, this));
});

main 存储元信息,创建一个 PDFDocumentProxy——Display 层对文档的对外门面——然后 resolve loadingTask.promise。

阶段小结 ✅ GetDoc 完成:main 收到文档元信息,创建 PDFDocumentProxy 并 resolve loadingTask.promise。getDocument 调用链完整结束。

至此 getDocument 返回,调用方拿到了 PDFDocumentProxy 实例。

getPage 方法

从 demo 代码来看:

const loadingTask = pdfjsLib.getDocument({ url: '...' });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);

pdf 就是 PDFDocumentProxy 实例,所以 pdf.getPage(1) 调的是 PDFDocumentProxy.getPage。

进入 main: PDFDocumentProxy.getPage

// Source: src/display/api.js
getPage(pageNumber) {
  return this._transport.getPage(pageNumber);
}

它委托给 WorkerTransport.getPage:

进入 main: PDFDocumentProxy.getPage → WorkerTransport.getPage

// Source: src/display/api.js
getPage(pageNumber) {
  if (
    !Number.isInteger(pageNumber) ||
    pageNumber <= 0 ||
    pageNumber > this.pagesMapper.pagesNumber
  ) {
    return Promise.reject(new Error("Invalid page request."));
  }
  const pageIndex = pageNumber - 1;
  const newPageIndex = this.pagesMapper.getPageId(pageNumber) - 1;
 
  const cachedPromise = this.#pagePromises.get(pageIndex);
  if (cachedPromise) {
    return cachedPromise;
  }
  const promise = this.messageHandler
    .sendWithPromise("GetPage", { pageIndex: newPageIndex })
    .then(pageInfo => {
      if (this.destroyed) { throw new Error("Transport destroyed"); }
      if (pageInfo.refStr) {
        this.#pageRefCache.set(pageInfo.refStr, newPageIndex);
      }
 
      const page = new PDFPageProxy(
        pageIndex, pageInfo, this, this.pagesMapper, this._params.pdfBug
      );
      this.#pageCache.set(pageIndex, page);
      return page;
    });
  this.#pagePromises.set(pageIndex, promise);
  return promise;
}

校验页码、检查 in-flight promise 缓存之后,main 向 Worker 发送 GetPage 消息。

main → Worker:GetPage

进入 Worker: on GetPage

handler.on('GetPage', async function ({ pageIndex }) {
  const page = await pdfManager.getPage(pageIndex);
 
  const [rotate, ref, userUnit, view] = await Promise.all([
    pdfManager.ensure(page, 'rotate'),
    pdfManager.ensure(page, 'ref'),
    pdfManager.ensure(page, 'userUnit'),
    pdfManager.ensure(page, 'view'),
  ]);
  return { rotate, ref, refStr: ref?.toString() ?? null, userUnit, view };
});

它调用 pdfManager.getPage,一路向下:

// BasePdfManager.getPage
getPage(pageIndex) {
  return this.pdfDocument.getPage(pageIndex);
}

最终落到 PDFDocument.getPage:

进入 Worker: on GetPage → pdfManager.getPage → PDFDocument.getPage

// Source: src/core/document.js
getPage(pageIndex) {
  const cachedPromise = this.#pagePromises.get(pageIndex);
  if (cachedPromise) {
    return cachedPromise;
  }
  const { catalog, linearization, xfaFactory } = this;
 
  let promise;
  if (xfaFactory) {
    promise = Promise.resolve([Dict.empty, null]);
  } else if (linearization?.pageFirst === pageIndex) {
    promise = this.#getLinearizationPage(pageIndex);
  } else {
    promise = catalog.getPageDict(pageIndex);
  }
  promise = promise.then(
    ([pageDict, ref]) =>
      new Page({
        pdfManager: this.pdfManager,
        xref: this.xref,
        pageIndex,
        pageDict,
        ref,
        globalIdFactory: this._globalIdFactory,
        fontCache: catalog.fontCache,
        builtInCMapCache: catalog.builtInCMapCache,
        standardFontDataCache: catalog.standardFontDataCache,
        globalColorSpaceCache: catalog.globalColorSpaceCache,
        globalImageCache: catalog.globalImageCache,
        systemFontCache: catalog.systemFontCache,
        nonBlendModesSet: catalog.nonBlendModesSet,
        xfaFactory,
      })
  );
 
  this.#pagePromises.set(pageIndex, promise);
  return promise;
}

这才是 PDF.js 真正去获取并解析页面 PDF 数据、构造 Page 实例的地方。底层 PDF 解析逻辑我们这里不展开,留待后续文章。

回到 main 侧,WorkerTransport.getPage 拿到 pageInfo 后,用它构造 PDFPageProxy,存入页面缓存,返回给调用方。

阶段小结 ✅ GetPage 完成:main 发送 GetPage → Worker 从 PDFDocument 获取 Page 实例并提取 rotate / view / ref / userUnit → main 收到 pageInfo,包装成 PDFPageProxy 返回。

总结

本篇我们完整追踪了 getDocument 和 getPage 的执行路径:

  • getDocument → 创建 PDFDocumentLoadingTask,拉起 Worker 并完成 test / ready / test 握手,发送 GetDocRequest 获取文档级消息通道,发送 Ready 启动加载。Worker 通过 fetch 探测网络能力,创建 NetworkPdfManager 或 LocalPdfManager,验证文档后以 GetDoc 回复。main 收到元信息,resolve 出 PDFDocumentProxy。
  • getPage → 校验页码,发送 GetPage 到 Worker。Worker 解析页面字典、构造 Page 实例,提取 rotate / view / ref / userUnit。main 把返回值包装成 PDFPageProxy 返回。

到这两个调用结束,demo 代码就拿到了全部需要的东西:

const loadingTask = pdfjsLib.getDocument({
  url: 'https://assets.streampdf.com/PDF32000_2008.pdf',
});
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);

最终手里的 PDFPageProxy 就是后续一切的基础——viewport 计算、canvas 渲染、文本提取、注释,都从这里出发。

注意我们始终停留在架构顶层。真正深入 PDF 解析(词法分析、XRef 表、页面字典、操作符)之前,你需要先有这条 main ↔ Worker 数据流动的高层心智模型。跟着这个系列读下去,你会同时拿到 PDF.js 的内部实现和 PDF 规范本身的扎实理解。

下一篇,我们进入视口计算与渲染。

相关文章