PDF.js 深入浅出 2: getDocument & getPage
2026年10月19日 · StreamPDF Team

前言
这是 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(右),时间自上而下流动。
① 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并 resolveloadingTask.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 规范本身的扎实理解。
下一篇,我们进入视口计算与渲染。