PDF.js 源码解读: MessageHandler
2026年10月27日 · StreamPDF Team

前言
这是 PDF.js 源码解读系列中关于「跨线程消息枢纽」的一篇,介绍 src/shared/message_handler.js(538 行)。
worker 线程解析 PDF、主线程渲染 canvas——两者之间所有通讯(除了 Transferable 的零拷贝所有权转移)都走这一个类:主线程侧一个 MessageHandler 实例,worker 侧另一个,两个实例共享同一个 comObj(MessagePort 或 Worker 对象)。
此前 api.js 文章四次提到它是「另文展开」:
sendWithPromise("GetDocRequest", docParams)—— 第 2 篇 getDocument 的握手通道;send("Ready", null)—— 握手尾声的宣告;sendWithStream("GetOperatorList", { ... })—— 第 5 篇 render 的流式 operatorList;sendWithStream("GetTextContent", { ... })—— 文本层的流式文本块。
这些调用方视角只展示了"API 怎么用"。本篇翻过来——站在协议实现层,看消息是怎么被包裹、分发、回调、取消、流控的。
一句话概括 MessageHandler 的职责:在 comObj.postMessage 这个最小原语之上,建立三种语义不同的通讯原语——fire-and-forget(send)、请求-应答(sendWithPromise)、流式(sendWithStream)。
文件结尾只有 2 个导出:MessageHandler 类本身、以及在跨线程 Error 身份保持上必须复用的 wrapReason 辅助函数。
模块总览
message_handler.js 可以切成七块:
| 区块 | 行号 | 职责 |
|---|---|---|
CallbackKind / StreamKind 常量 | 26-40 | 两类消息体的类型枚举 |
wrapReason | 44-74 | 跨线程 Error 类型保持 |
MessageHandler 构造函数 | 76-93 | 四个 Map 容器 + 消息监听 |
#onMessage 分发主循环 | 95-156 | callback / action / stream 三路分派 |
| 三个 send API | 158-294 | send / sendWithPromise / sendWithStream |
流的两端:#createStreamSink + #processStreamMessage | 296-519 | sink(消费端)/ controller(生产端)的双向背压 |
#deleteStreamController + destroy | 521-535 | 流清理与生命周期终结 |
消息信封:postMessage 的三层包裹
postMessage 的载荷只能是 JSON-可序列化对象。MessageHandler 规定了自己的协议——所有穿过 comObj 的消息都带这几个字段:
// 信封通用部分
{
sourceName: "main" | "worker" | "d1" | "d1_worker", // 消息来源
targetName: "main" | "worker" | "d1" | "d1_worker", // 消息目的
// ... 具体形态的字段
}信封之上再套三形态:
| 形态 | 关键字段 | 对端动作 | 对应 API |
|---|---|---|---|
| Action(fire-and-forget) | action, data | 执行后不回包 | send(action, data) |
| Action + Callback(请求-应答) | action, data, callbackId | 执行后按 callbackId 回一个 callback | sendWithPromise(action, data) |
| Callback(纯回包) | callback, callbackId, data/reason | 这是对请求的答复,不再被路由到 actionHandler | — |
| Stream Start | action, data, streamId, desiredSize | 创建 sink,执行 action(同步),回 START_COMPLETE | sendWithStream(action, data) |
| Stream Control | stream, streamId | PULL / CANCEL / CANCEL_COMPLETE 等 8 种流控制 | 由 ReadableStream 内部自动发 |
| Stream Data | stream, streamId, chunk | ENQUEUE / CLOSE / ERROR,由 sink 发 | 由 sink 侧代码主动调 |
#onMessage 按字段存在性分三路处理,互斥、有严格的先后顺序:
if (data.stream) { /* 流式控制或数据 */ }
if (data.callback) { /* callbackId 的回包 */ }
// 剩下的才是 actionsourceName / targetName 两个字段构成软路由:#onMessage 里只做一次精确匹配——data.targetName !== this.sourceName 就丢弃。多文档并存时(两个 getDocument 并行调用),docId 编进名字(d1 / d1_worker / d2 / d2_worker),MessageHandler 就天然隔离,互不串线。
MessageHandler 类:四个表 + 一个监听
constructor(sourceName, targetName, comObj) {
this.sourceName = sourceName;
this.targetName = targetName;
this.comObj = comObj;
this.callbackId = 1;
this.streamId = 1;
this.streamSinks = Object.create(null); // streamId → sink(我这边是数据接收端)
this.streamControllers = Object.create(null); // streamId → controller(我这边是数据生产端)
this.callbackCapabilities = Object.create(null); // callbackId → Promise.withResolvers()
this.actionHandler = Object.create(null); // actionName → handler
comObj.addEventListener("message", this.#onMessage.bind(this), {
signal: this.#messageAC.signal,
});
}四张表的分工一目了然:
actionHandler:我能响应的远端动作。MessageHandler.on("GetPage", handler)注册,远端sendWithPromise("GetPage", ...)时触发;callbackCapabilities:我发请求、等远端回 callbackId。key 是回调 id,value 是Promise.withResolvers()。id 递增分配,用完即删;streamControllers:我发sendWithStream、远端是数据生产端。我这边持有 ReadableStream 的 controller,远端往这个 sink 里 enqueue 数据;streamSinks:远端发sendWithStream、我是数据生产端。我这边持有一个 sink 对象(有enqueue / close / error方法),远端 ReadableStream 的 pull/cancel 控制我。
设计要点:四张表的"我/远端"关系是相对的。主线程发
sendWithStream("GetOperatorList")时,主线程持有streamControllers[streamId],worker 持有streamSinks[streamId];worker 发sendWithStream("GetReader")(把全量 PDF 数据流式推给主线程)时,关系恰好反过来。每个 MessageHandler 实例同时是某些流的生产端、某些流的消费端,所以两个 Map 都需要。
callbackId / streamId 都是自增整数,每个 MessageHandler 独立分配。worker 与主线程各自从 1 开始,不会撞——因为它们的 MessageHandler 是两个不同实例。
监听用 AbortController signal——destroy() 时 abort() 移除事件,之后 comObj.postMessage 收到的消息不会再触发任何回调。
on() / send() / sendWithPromise():前两种形态
on():注册一个 action handler
on(actionName, handler) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(typeof handler === "function",
'MessageHandler.on: Expected "handler" to be a function.');
}
const ah = this.actionHandler;
if (ah[actionName]) {
throw new Error(`There is already an actionName called "${actionName}"`);
}
ah[actionName] = handler;
}只允许每个 action 名注册一个 handler——第二条就抛错。这让远端的"我要发一个 GetPage"在接收方只有一处实现,排查时不会陷入"到底是哪个 handler 响应"的困境。
api.js 里 WorkerTransport.setupMessageHandler() 连续调 20+ 次 .on(...) 注册:GetDoc、DocException、PasswordRequest、commonobj、obj、StartRenderPage、DocProgress、FetchBinaryData、SaveDocument、Terminate 等。一个 doc 的所有消息类型都集中在这一批 .on 里,是 WorkerTransport 这个类"把 worker↔主线程通讯翻译成语义"的核心。
send():fire-and-forget
send(actionName, data, transfers) {
this.comObj.postMessage({
sourceName: this.sourceName,
targetName: this.targetName,
action: actionName,
data,
}, transfers);
}最简单的一条路。没有 callbackId、没有 streamId,消息出去就完了。api.js 里两处使用:
messageHandler.send("Ready", null)—— 握手尾声的宣告;messageHandler.send("configure", { verbosity })—— PDFWorker 把日志级别传给 worker。
sendWithPromise():请求-应答
sendWithPromise(actionName, data, transfers) {
const callbackId = this.callbackId++;
const capability = Promise.withResolvers();
this.callbackCapabilities[callbackId] = capability;
try {
this.comObj.postMessage({
sourceName: this.sourceName,
targetName: this.targetName,
action: actionName,
callbackId,
data,
}, transfers);
} catch (ex) {
capability.reject(ex);
}
return capability.promise;
}跟 send 的唯一区别:带上 callbackId,并把 promise resolver 记在表里。远端处理 action 时,如果看到 callbackId 字段存在,就会把结果(或异常)作为一个 callback 形态的消息回过来,#onMessage 的 callback 分支再把 resolver 调起来。
注意两个细节:
- 同步 try-catch 只兜
postMessage自身的抛错(transfer 所有权已释放后的二次 transfer、transfer 里的对象不可序列化等)。postMessage 返回后,远端处理 action 的任何异常都在另一边的事件循环里,必须走 callback ERROR 通道才能回来; - callback 消息必须最终送达——否则 caller 拿到的 promise 永远 pending。这就是为什么
.finally里总要.catch(() => {})认领 rejection:销毁竞态时 MessageHandler 可能被提前destroy(),callback 永远收不到。
api.js 里的四个重角色调用(GetDocRequest / GetPage / GetMetadata / SaveDocument)全走这条通道。
sendWithStream():第三种形态,最复杂
流式接口用 Web 标准 ReadableStream 封装。调用方拿到的是一个 ReadableStream,另一端拿到的是一个 sink 对象(有 enqueue / close / error),双方通过 #processStreamMessage 里的 8 种 StreamKind 消息协调背压。
发起方:ReadableStream 的 start / pull / cancel 回调
sendWithStream(actionName, data, queueingStrategy, transfers) {
const streamId = this.streamId++,
sourceName = this.sourceName,
targetName = this.targetName,
comObj = this.comObj;
return new ReadableStream({
start: controller => {
const startCapability = Promise.withResolvers();
this.streamControllers[streamId] = {
controller, startCall: startCapability,
pullCall: null, cancelCall: null, isClosed: false,
};
comObj.postMessage({
sourceName, targetName,
action: actionName, streamId, data,
desiredSize: controller.desiredSize, // 首次推送的背压信息
}, transfers);
return startCapability.promise; // 告诉 ReadableStream:等我这个 promise 才开始 pull
},
pull: controller => {
const pullCapability = Promise.withResolvers();
this.streamControllers[streamId].pullCall = pullCapability;
comObj.postMessage({ sourceName, targetName,
stream: StreamKind.PULL, streamId,
desiredSize: controller.desiredSize });
return pullCapability.promise; // 告诉 ReadableStream:等我回 PULL_COMPLETE 再 pull 下一次
},
cancel: reason => {
assert(reason instanceof Error, "cancel must have a valid reason");
const cancelCapability = Promise.withResolvers();
this.streamControllers[streamId].cancelCall = cancelCapability;
this.streamControllers[streamId].isClosed = true;
comObj.postMessage({ sourceName, targetName,
stream: StreamKind.CANCEL, streamId,
reason: wrapReason(reason) });
return cancelCapability.promise; // 告诉 ReadableStream:等我 CANCEL_COMPLETE
},
}, queueingStrategy);
}start 回调不直接 resolve——它先把 action + streamId + desiredSize 发给对端,再把 startCapability.promise 返回给 ReadableStream。对端回 START_COMPLETE 后(见下文 #processStreamMessage),startCall.resolve() 被调用,ReadableStream 才开始向消费端推数据。
这就是为什么 api.js _pumpOperatorList 里拿到 readableStream 后立刻调 reader.read() 能正常工作——start 期间 ReadableStream 内部是 await 住的,直到远端 sink 创建完毕、START_COMPLETE 回来,才开始接受 pull 请求。
pull 每次 ReadableStream 内部缓冲区满到需要更多数据时触发。只在返回的 promise resolve 后才会再触发下一次 pull,所以每一轮 pull 都必须等对端回 PULL_COMPLETE——这是天然的背压通道。
接收方(对端):#createStreamSink
对端的 MessageHandler 在 #onMessage 里发现 data.streamId 存在(不是 stream 字段,是 action 消息带 streamId),就走 #createStreamSink(data):
#createStreamSink(data) {
const streamSink = {
enqueue(chunk, size = 1, transfers) { /* 见下文 */ },
close() { /* 发 CLOSE */ },
error(reason) { /* 发 ERROR */ },
sinkCapability: Promise.withResolvers(),
onPull: null, // 被 PULL 消息触发
onCancel: null, // 被 CANCEL 消息触发
isCancelled: false,
desiredSize: data.desiredSize, // 初始值来自 start 消息
ready: null,
};
streamSink.sinkCapability.resolve();
streamSink.ready = streamSink.sinkCapability.promise;
this.streamSinks[streamId] = streamSink;
Promise.try(action, data.data, streamSink).then(
() => comObj.postMessage({ stream: StreamKind.START_COMPLETE, streamId, success: true }),
reason => comObj.postMessage({ stream: StreamKind.START_COMPLETE, streamId, reason: wrapReason(reason) })
);
}streamSink 是主动生产者——sink 的 owner(如 worker 的 GetOperatorList handler)通过 enqueue(chunk) 把数据推给对端;被动响应的是 onPull / onCancel——当 ReadableStream 端需要更多数据(或取消)时,MessageHandler 会回调这两个函数。
#processStreamMessage:8 种 StreamKind 的双向状态机
所有流控制与数据消息都走这一个 switch:
case StreamKind.PULL: // ReadableStream 端需要更多数据 → 触发 sink.onPull
case StreamKind.PULL_COMPLETE: // sink.onPull 执行完毕 → resolve ReadableStream 的 pull promise
case StreamKind.ENQUEUE: // sink 发了数据 → controller.enqueue
case StreamKind.CLOSE: // sink 结束 → controller.close
case StreamKind.ERROR: // sink 报错 → controller.error
case StreamKind.CANCEL: // ReadableStream 被取消 → 触发 sink.onCancel
case StreamKind.CANCEL_COMPLETE: // sink.onCancel 执行完毕 → resolve ReadableStream 的 cancel promise
case StreamKind.START_COMPLETE: // sink 创建 + action 执行完毕 → resolve ReadableStream 的 start promise把它理解为两对 promise 握手:
| ReadableStream 端动作 | 往对端发 | 对端回 | 本端 resolve |
|---|---|---|---|
| start() | start message(带 desiredSize) | START_COMPLETE | startCall |
| pull() | PULL | PULL_COMPLETE | pullCall |
| cancel() | CANCEL | CANCEL_COMPLETE | cancelCall |
| — | — | ENQUEUE / CLOSE / ERROR | controller 被直接操作,不经过 promise |
enqueue 的背压:desiredSize 双向流动
sink.enqueue 里有一段不直观的逻辑:
const lastDesiredSize = this.desiredSize;
this.desiredSize -= size;
if (lastDesiredSize > 0 && this.desiredSize <= 0) {
this.sinkCapability = Promise.withResolvers();
this.ready = this.sinkCapability.promise;
}desiredSize 是 ReadableStream 主动侧告诉你的「我还能接多少」。sink 每 enqueue 一块就减去对应 size;减到负数或零时,sink 侧把自己的 ready 挂起。这是"生产者等不及消费者拉"的信号——sink 的 owner(如 worker 的 operatorList 解析循环)在每次 enqueue 前应该 await sink.ready,否则就是无脑推送直到对端 ReadableStream 内部缓冲区撑爆。
对应的触发端在 PULL 消息里:
if (streamSink.desiredSize <= 0 && data.desiredSize > 0) {
streamSink.sinkCapability.resolve();
}
streamSink.desiredSize = data.desiredSize;对端 pull 到来时重置 desiredSize 为新值(ReadableStream 此时应该已经消耗掉一些),并在值由负转正时 resolve 掉 sink 挂起的 ready。desiredSize 双向流动——从 sink.enqueue 扣减到 PULL 到达时重置,构成完整的背压环。
#deleteStreamController:等三条 promise 全部 settle 再删
async #deleteStreamController(streamController, streamId) {
await Promise.allSettled([
streamController.startCall?.promise,
streamController.pullCall?.promise,
streamController.cancelCall?.promise,
]);
delete this.streamControllers[streamId];
}controller 被 CLOSE / ERROR / CANCEL_COMPLETE 任何一条触发删除时,不能立刻 delete——因为 ReadableStream 内部可能还在等 pullCall.promise(它还在排队执行、尚未 resolve)。立即删会让 pullCall.resolve(...) 抛 TypeError: Cannot read properties of undefined。Promise.allSettled 保证三条 promise 都 settle 后再清理,让 ReadableStream 内部所有悬挂回调安全结束。
#onMessage:分发主循环的三种形态
#onMessage({ data }) {
if (data.targetName !== this.sourceName) return; // 软路由
if (data.stream) {
this.#processStreamMessage(data); // StreamKind 控制或数据
return;
}
if (data.callback) {
// 纯回包:从 callbackCapabilities 取出 capability,按 DATA/ERROR resolve 或 reject
return;
}
// 剩下的是 action
const action = this.actionHandler[data.action];
if (!action) throw new Error(`Unknown action from worker: ${data.action}`);
if (data.callbackId) {
// action + callbackId:执行后把结果/异常回成 callback
Promise.try(action, data.data).then(
result => comObj.postMessage({ callback: DATA, callbackId, data: result }),
reason => comObj.postMessage({ callback: ERROR, callbackId, reason: wrapReason(reason) })
);
return;
}
if (data.streamId) {
// action + streamId:创建 sink,不回 action 形态的答复
this.#createStreamSink(data);
return;
}
action(data.data); // 纯 action,fire-and-forget
}几个值得展开的点:
Promise.try 的作用:把可能抛同步异常的 action 包进一个微任务里,让后续 .then/.catch 能捕获。没有它的话,action(data.data) 抛同步异常会直接炸在当前事件循环里,而不是走 ERROR callback。
callbackId 回包有两条:DATA 与 ERROR。两端都用 wrapReason 做 Error 类型保持。
data.callbackId 存在但 data.streamId 不存在 → 请求-应答;data.streamId 存在 → 流式;data.callback 存在 → 纯回包。 三者互斥,#onMessage 靠字段存在性区分,不靠"先到先试"的 try/catch——这是刻意的设计选择,避免消息语义歧义。
wrapReason:跨线程的 Error 身份保持
postMessage 不会把 Error 实例当作 Error 传——它会序列化成 { name, message, ... } 的普通对象。收到后要根据 name 字段重新构造成 PDF.js 自定义的异常类型:
function wrapReason(ex) {
// 已经是正确类型 → 直接返回(避免二次构造)
if (ex instanceof AbortException || ex instanceof InvalidPDFException ||
ex instanceof PasswordException || ex instanceof ResponseException ||
ex instanceof UnknownErrorException) {
return ex;
}
if (!(ex instanceof Error || (typeof ex === "object" && ex !== null))) {
unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');
}
switch (ex.name) {
case "AbortException": return new AbortException(ex.message);
case "InvalidPDFException": return new InvalidPDFException(ex.message);
case "PasswordException": return new PasswordException(ex.message, ex.code);
case "ResponseException": return new ResponseException(ex.message, ex.status, ex.missing);
case "UnknownErrorException": return new UnknownErrorException(ex.message, ex.details);
}
return new UnknownErrorException(ex.message, ex.toString());
}五种自定义异常,各自有专属额外字段(PasswordException.code / ResponseException.status + missing / UnknownErrorException.details),wrapReason 都原样保留。worker 端 wrapReason 了一次(发消息时),主线程端再 wrapReason 一次(收消息时)——但因为已经是正确的 instanceof 类型,二次 wrap 会短路返回原值,不做二次构造。
destroy():生命周期终结
destroy() {
this.#messageAC?.abort();
this.#messageAC = null;
}AbortController.abort() 移除 comObj 上的 message 监听器,之后无论 comObj 来多少消息,都不会再触发 #onMessage。四张表(callbackCapabilities / streamControllers / streamSinks / actionHandler)不会被显式清空,但它们持有的 capability 等对象如果被外部引用,会收到"永远不 resolve/reject"的悬挂状态——这就是 api.js 里 destroy() 时先去认领 _capability.promise.catch(() => {}) 的原因。
LoopbackPort:假 worker 回退下的同构通讯
pdf.WorkerTransport 在 _setupFakeWorker 里用 LoopbackPort 代替真实 MessagePort:
const port = new LoopbackPort();
const id = `fake${PDFWorker.#fakeWorkerId++}`;
const workerHandler = new MessageHandler(id + "_worker", id, port);
WorkerMessageHandler.setup(workerHandler, port);
this.#messageHandler = new MessageHandler(id, id + "_worker", port);两个 MessageHandler 实例 共享同一个 LoopbackPort,谁 postMessage 谁的监听就立即被调用(同步派发)。对 MessageHandler 来说 LoopbackPort.addEventListener("message", cb) + postMessage(obj) 的接口与真实 MessagePort 完全一致——所以 MessageHandler 层对"我面对的是同线程还是跨线程"完全无感知。
这正是 api.js 文章提到的「能力回退不打扰上层」模式:LoopbackPort 只让通讯从"异步 postMessage"变成"同步事件触发",MessageHandler 的三张 API(send / sendWithPromise / sendWithStream)以及三张表的用法完全不变。
总结
MessageHandler 是 PDF.js 主线程↔worker 通讯的最小原语封装——它没有"读文件""取页面"这些业务语义,只建立三种消息形态和一套信封协议。
三种通讯形态的落地场景
| 形态 | 调用方 | 业务含义 |
|---|---|---|
send | WorkerTransport.on("Ready", ...) | 宣告"我这边好了" |
sendWithPromise | WorkerTransport._setupCapability | "请给我 GetDocRequest 的回执" |
sendWithStream | PDFPageProxy._pumpOperatorList | "把 operatorList 分块推过来" |
| streamSink.enqueue | worker 端 GetOperatorList handler | "我刚解析完一块绘制指令" |
信封协议的设计原则
- 软路由不删消息:
sourceName/targetName匹配失败时只return,不抛错。多文档并存场景下大量消息会自然被过滤掉。 - 请求必回 callback:带
callbackId的 action 处理完必须回 callback,否则 caller 悬挂。这是契约,不是建议。 - 流控天然内嵌:ReadableStream 的
desiredSize通过两次 postMessage(start 时、每轮 pull 时)在两端流动,sink 侧据此挂起ready,构成完整背压环。 - 错误身份不丢失:
wrapReason让跨线程的 PDF.js 自定义异常在另一侧仍是正确的instanceof类型。 - 同构接口即回退能力:LoopbackPort 与 MessagePort 接口一致,让假 worker 回退对 MessageHandler 透明。
读懂 MessageHandler 之后,再看 api.js 文章里的四个重角色调用(GetDocRequest / Ready / GetOperatorList / GetTextContent),它们就不再是"一句笼统的 worker 通讯"——你能精确知道 callbackId 走哪张表、streamId 走哪两张表、desiredSize 怎么在两端流动。
下一篇应该接 网络流(fetch_stream.js + network_stream.js)——GetReader / GetRangeReader 消息就在 MessageHandler 的 stream 通道上跑,MessageHandler 是它们的底层。
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- 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 深入浅出 5: render 方法全貌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 源码解读: 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日