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

前言
这是 PDF.js 源码解读系列中关于「主线程对象就绪协调」的一篇,介绍 src/display/pdf_objects.js。当前仓库中的这个文件只有 116 行,导出也只有 PDFObjects 一个类,却处在 worker 解析与 canvas 渲染的交界上。
前面的 api.js、MessageHandler 与网络流文章解决了另一段问题:PDF 字节如何进入 worker,operatorList 如何流式回到主线程,消息如何分发。本篇接着问:worker 已经解码出的字体、图片和字形路径到了主线程以后,渲染循环怎样在对象就绪时同步取用,又在对象尚未就绪时暂停并等待?
答案不是「每次 get 都返回一个 Promise」,也不是「把跨线程 Promise 直接交给 canvas」。PDFObjects 在主线程为每个对象保存一份稳定条目:条目里的 data 表示当前值,条目里的本地 Promise 只负责通知等待者。真正绘制时仍然走同步读取。
一句话概括它的职责:用一个小型三态注册表,统一对象先到与等待者先到两种时序,并让文档级、页级渲染资源拥有明确的就绪与清理边界。
模块总览
文件没有显式构造函数、继承或外部依赖,全部机制由一个哨兵、一个条目工厂和一个私有 Map 组成。
| 部分 | 职责 |
|---|---|
INITIAL_DATA | 私有 Symbol,标记「条目已登记,但数据尚未解决」 |
dataObj | 创建含 data 与 Promise.withResolvers() 结果的条目 |
#objs | 以 objId 为键保存条目;条目的身份在等待期间保持不变 |
get | 无回调时同步取数据;有回调时登记就绪通知 |
has | 只探测是否已解决,不创建条目 |
resolve | 写入最终数据并唤醒本地等待者;同一条目只能解决一次 |
delete | 删除已解决条目,拒绝删除未登记或未解决条目 |
clear | 关闭条目中的 ImageBitmap,再清空整个注册表 |
Symbol.iterator | 只迭代已解决的 [objId, data],跳过 pending 项 |
这里的「对象」是主线程渲染要消费的产物,不是 worker 的 XRef 间接对象缓存。当前消息链路会把字体、FontPath、图片和图案放进来;operatorList 则仍由 PDFPageProxy 的渲染状态单独管理。
条目模型:数据与就绪通知分离
INITIAL_DATA 与 dataObj
const INITIAL_DATA = Symbol('INITIAL_DATA');
const dataObj = () => ({
...Promise.withResolvers(),
data: INITIAL_DATA,
});Promise.withResolvers() 返回 { promise, resolve, reject },展开后再补一个 data 字段,就得到注册表中的一个条目。这个工厂只创建本地 JavaScript 对象,不发送消息,也不启动解码任务。
| 字段 | 在 PDFObjects 中的作用 |
|---|---|
promise | 供一个或多个 get(objId, callback) 注册 .then |
resolve | 在数据写好后履行 promise,唤醒回调 |
reject | 由 withResolvers 一并提供,但当前文件没有使用 |
data | 初始为 INITIAL_DATA,解决后保存真正的 payload |
关键是 data 与 Promise 不承载同一份信息。Promise 的履行值并不是渲染数据;真正的数据始终从条目的 data 读取。这样无论回调是在解决前还是解决后注册,读取的都是同一个条目对象。
INITIAL_DATA 使用模块内私有 Symbol,而不是 null、undefined 或空字符串。于是「有一个值,而且这个值恰好是 null」与「还没有值」不会混淆,任何正常 payload 也不可能意外等于这个哨兵。
三态,而不是简单的有/无
| 状态 | Map 中是否有条目 | data | 本地 Promise | 无回调 get |
|---|---|---|---|---|
| 未登记 | 否 | 不存在 | 不存在 | 抛错 |
| 已登记、未解决 | 是 | INITIAL_DATA | pending | 抛错 |
| 已解决 | 是 | 实际数据,包括 null | 已履行 | 同步返回 data |
未登记与未解决对同步调用方都意味着「不能现在取」,但对异步调用方不同:带回调的 get 会把未登记变成已登记,而未解决条目已经可能有等待者。resolve 则既可以把未登记直接变成已解决,也可以完成已有的 pending 条目。
类的方法从不读取 Promise 的内部状态;它们只用「条目是否存在」与 data === INITIAL_DATA 判断状态。Promise 是通知装置,哨兵才是同步状态机的事实来源。
get:同步读取与回调等待是两种契约
get(objId, callback = null) {
// If there is a callback, then the get can be async and the object is
// not required to be resolved right now.
if (callback) {
const obj = this.#objs.getOrInsertComputed(objId, dataObj);
obj.promise.then(() => callback(obj.data));
return null;
}
// If there isn't a callback, the user expects to get the resolved data
// directly.
const obj = this.#objs.get(objId);
// If there isn't an object yet or the object isn't resolved, then the
// data isn't ready yet!
if (!obj || obj.data === INITIAL_DATA) {
throw new Error(`Requesting object that isn't resolved yet ${objId}.`);
}
return obj.data;
}有回调:必要时登记,但不返回 Promise
当前版本直接使用 Map.prototype.getOrInsertComputed:键已存在时返回原条目,不存在时调用 dataObj 创建并插入,再返回新条目。它不是旧代码记忆中的手写「先 get、再 set」展开;这个文件本身也没有提供旧式 Map 的回退实现。
无论条目原本处于 pending 还是已解决,下一行都是 obj.promise.then(...)。因此同一个对象可以登记多个回调,所有回调共享一个就绪信号,而不是各自创建跨线程请求。
回调体写成 () => callback(obj.data),没有使用 .then 的参数。原因在 resolve 中会看到:条目 Promise 是以空参数履行的,回调执行时再从闭包里的 obj 取最新数据。
即使对象已经解决,Promise 回调也只会被排入微任务,不会在 get 的调用栈内同步执行。源码注释里的「right away」表示不必等待新的解决事件,不等于同步调用。
带回调分支固定返回 null,不是 Promise,也不是将来的数据。调用方不能用 await objs.get(id, cb) 等对象就绪;它交出的是回调注册权。
无回调:未就绪就抛错
无回调分支只调用普通 Map.get,绝不偷偷创建条目。未登记与 data === INITIAL_DATA 都抛出同一个 Requesting object that isn't resolved yet 错误;它不会退化成异步等待。
已解决时直接返回 obj.data。因为状态由哨兵判断,resolve(id) 得到的 null 也是合法的同步返回值,不会被误判为 pending。
这就是渲染热路径需要的态度:真正执行绘画算子时,对象必须先就绪;若未就绪,应该由依赖调度器提前暂停,而不是让每个 fill、drawImage 或字体操作自己变成异步函数。
has:无副作用的就绪探测
has(objId) {
const obj = this.#objs.get(objId);
return !!obj && obj.data !== INITIAL_DATA;
}has 与无回调 get 使用同一套状态判定,但把「未就绪抛错」改成 false。未登记和 pending 都是 false,已解决则为 true,即使 data 是 null。
它不会登记对象,也不会注册回调。canvas 的依赖算子先用它决定能否继续;commonobj / obj 消息处理器也用它跳过已经解决的重复对象。
不要把 has(id) === false 理解成「Map 里一定没有 id」:它把未登记和已登记未解决刻意折叠成同一个消费语义——现在还不能同步读取。
resolve:先写数据,再发就绪信号
resolve(objId, data = null) {
const obj = this.#objs.getOrInsertComputed(objId, dataObj);
if (obj.data !== INITIAL_DATA) {
throw new Error(`Object already resolved ${objId}.`);
}
obj.data = data;
obj.resolve();
}getOrInsertComputed 同时覆盖两条入口:对象先到时新建条目并立即解决;回调先注册时取得原 pending 条目,不替换它。等待者持有的 Promise 因而始终是最终被唤醒的那个。
重复解决检查发生在写入之前。只要 data !== INITIAL_DATA,就抛出 Object already resolved,第二次调用的数据不会覆盖第一次,条目 Promise 也不会被再次利用。
这里的「单次」是单次登记生命周期,不是某个 id 永远不能再用。delete 或 clear 移除条目后,同一 id 可以建立新条目;但在原条目上,resolve 不是更新缓存值的 set。
默认参数是当前版本很容易被忽略的细节:resolve(objId) 会把 data 写成 null。由于 pending 由 INITIAL_DATA 表示,这个对象仍然是已解决状态,has 返回 true,无回调 get 返回 null。
赋值顺序也不能颠倒。obj.data = data 先同步完成,obj.resolve() 再履行 Promise;后续微任务里的 callback(obj.data) 因而一定能看到最终值。
obj.resolve() 没有传参,所以 Promise 的履行值是 undefined。这再次说明它不是数据通道。reject 在当前实现中同样没有参与:对象处理失败时,是否把某种错误值作为 data 解决、是否走备用渲染,是上层消息处理器决定的。
delete:为什么拒绝删除未解决项
delete(objId) {
const obj = this.#objs.get(objId);
if (!obj || obj.data === INITIAL_DATA) {
// Only allow removing the object *after* it's been resolved.
return false;
}
this.#objs.delete(objId);
return true;
}未登记返回 false 很直观;pending 也返回 false,则是在保护已经注册的等待者。设想允许删除 pending 条目 A:回调仍挂在 A 的 Promise 上,之后的 resolve 却会通过 getOrInsertComputed 新建条目 B 并唤醒 B,A 上的回调便永远没有下文。
拒绝删除让「等待中」这个生命周期保持完整:要么原条目被解决,要么由更高层的整体销毁终止语义;不能一边留下等待者,一边悄悄换掉它们等待的注册项。
这个方法在字体路径上有实际用途。FontFaceObject.getPathGenerator 把原始绘制命令编译成可复用路径后,会删除不再需要的原始对象:
const objId = this.loadedName + '_path_' + character;
let cmds;
try {
cmds = objs.get(objId);
} catch (ex) {
warn(`getPathGenerator - ignoring character: "${ex}".`);
}
const path = makePathFromDrawOPS(cmds?.path);
if (!this.fontExtraProperties) {
// Remove the raw path-string, since we don't need it anymore.
objs.delete(objId);
}
return (this.compiledGlyphs[character] = path);同步 get 保证正常走到删除时对象已经解决;若未就绪,前面的异常分支不会把 pending 项删掉。编译结果进入 compiledGlyphs,原始 FontPath 条目则可以退出注册表。
注意 delete 本身只删 Map 项,不调用 bitmap.close()。批量释放位图资源是 clear 的职责,不能把两者当成同一个清理 API。
clear:先释放位图,再整体移除
clear() {
for (const { data } of this.#objs.values()) {
data?.bitmap?.close(); // Release any `ImageBitmap` data.
}
this.#objs.clear();
}循环遍历所有条目,对已解决且带 bitmap 的图像数据调用 ImageBitmap.close(),主动释放其图形资源;null 或 pending 的 INITIAL_DATA 会被可选链安全跳过。随后 Map.clear() 去掉所有注册项,其余普通数据交给垃圾回收。
这里释放的是 data.bitmap,不是假定每个对象本身都有 close。字体字节、编译路径、普通像素数组各有自己的生命周期,PDFObjects.clear 只对这一条共用的位图资源约定做特殊处理。
clear 不是逐对象取消协议。 它不 resolve、也不 reject pending 条目的 Promise,已挂在旧条目上的回调不会仅因为 clear 就收到通知。正确性依赖上层生命周期先停止消费者,而不是把这个小类当成通用任务取消器。
页面清理时,PDFPageProxy.#tryCleanup 先确认没有活动 renderTask 且 operatorList 已到最后一块,再清 _intentStates 并调用 objs.clear()。此后迟到的 obj 消息看见 _intentStates.size === 0,会直接关闭随消息到来的 imageData.bitmap,不重新写入已经清理的页缓存。
文档销毁时,WorkerTransport.destroy 先标记 transport 已销毁,让后续对象消息被忽略,再等待页面任务与 worker Terminate 收尾,最后执行 commonObjs.clear()。显式文档 cleanup() 也会在各页清理成功后清文档级对象。
因此,clear 的简单实现与 api.js 的严格清理顺序是配套设计:前者负责释放与移除,后者保证此时不再有人合法等待或继续写入。
Symbol.iterator:只遍历可以消费的数据
*[Symbol.iterator]() {
for (const [objId, { data }] of this.#objs) {
if (data !== INITIAL_DATA) {
yield [objId, data];
}
}
}这是一个生成器方法,所以 PDFObjects 实例可以直接放进 for...of。它产出 [objId, data],不是内部条目,也不产出 Promise。
pending 项被逐项跳过,不会阻塞整个迭代,也不会触发等待;已解决的 null 则仍会被产出。调用方看到的永远是可以同步检查的 payload。
当前 commonobj 的 CopyLocalImage 分支正依赖这个接口。worker 希望复用主线程某页已经解码的图片时,处理器扫描各页对象:
for (const pageProxy of this.#pageCache.values()) {
for (const [, data] of pageProxy.objs) {
if (data?.ref !== imageRef) {
continue;
}
if (!data.dataLen) {
return null;
}
const copy = structuredClone(data);
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('TESTING')) {
copy.CopyLocalImage = true;
}
this.commonObjs.resolve(id, copy);
return data.dataLen;
}
}只有已解决的页级数据才参与 ref 匹配。找到后,主线程 structuredClone 一份数据,把副本解决到文档级 commonObjs,再把 dataLen 作为这个 commonobj 请求的答复返回给 worker,避免重复解码。
这里存在两类 Promise,但不能混为一谈:CopyLocalImage 的请求—应答由 MessageHandler.sendWithPromise 完成;commonObjs.resolve 唤醒的则是主线程本地对象等待者。它们服务的对象与方向都不同。
两级注册表:commonObjs 与 objs
PDFObjects 本身不决定作用域,作用域由 api.js 创建实例的位置决定。每个 WorkerTransport 创建一份文档级注册表:
this.commonObjs = new PDFObjects();每个 PDFPageProxy 共享 transport 的那一份,同时创建自己的页级注册表:
this.commonObjs = transport.commonObjs;
this.objs = new PDFObjects();| 注册表 | 拥有者与共享范围 | 入口消息 | 当前主要数据 | 清理边界 |
|---|---|---|---|---|
commonObjs | 一个 WorkerTransport 一份,所有页面共享 | commonobj | 字体、字形路径、全局图片、部分图案、本地图片副本 | 文档 cleanup / destroy |
objs | 每个 PDFPageProxy 一份 | obj | 本页图片与图案 | 页面 cleanup / destroy |
PDFPageProxy.render 把两者一起交给 InternalRenderTask,后者再交给 CanvasGraphics。canvas 按 id 约定选池:字符串以 g_ 开头时走 commonObjs,否则走页级 objs。
getObject(opIdx, data, fallback = null) {
if (typeof data === "string") {
this.dependencyTracker?.recordNamedDependency(opIdx, data);
return data.startsWith("g_")
? this.commonObjs.get(data)
: this.objs.get(data);
}
return fallback;
}真正的图片、图案读取会经过这个同步 getObject。字体的 setFont 更直接,调用 this.commonObjs.get(fontRefName);需要内置字形路径时,paintChar 再把同一个 commonObjs 交给 font.getPathGenerator。
worker 侧也按资源范围选择消息:普通页图片发送 obj,Type3 字体内的图片或需要全局缓存的图片发送 commonobj。所以两级缓存不是主线程事后猜测出来的分类,而是生产端与消费端共同遵守的路由协议。
对象消息到达,不等于对象已经就绪
当前 commonobj 处理器按类型决定何时、以什么值调用 resolve:
| 类型 | 主线程处理 | 最终注册位置 |
|---|---|---|
Font | 构造 FontFaceObject,等待字体绑定或 fallback 链完成 | commonObjs |
FontPath | 包装成 FontPathInfo | commonObjs |
Image | 使用传回的 exportedData | commonObjs |
Pattern | 经 PatternInfo.getIR() 转换 | commonObjs |
CopyLocalImage | 扫描页缓存并克隆可复用图片 | commonObjs |
obj 的 Image / Pattern | 校验页面仍有效后直接解决 | 对应页的 objs |
字体路径最能说明「收到数据」与「可以渲染」之间的距离:
this.fontLoader
.bind(font)
.catch(() => messageHandler.sendWithPromise('FontFallback', { id }))
.finally(() => {
if (!font.fontExtraProperties) {
// Immediately release the `font.data` property once the font
// has been attached to the DOM, since it's no longer needed,
// rather than waiting for a `PDFDocumentProxy.cleanup` call.
// Since `font.data` could be very large, e.g. in some cases
// multiple megabytes, this will help reduce memory usage.
font.clearData();
}
this.commonObjs.resolve(id, font);
});bind(font) 可能要等待原生 FontFace.loaded,也可能走 DOM 字体加载探测。绑定失败时,catch 返回 FontFallback 请求 Promise;无论直接成功还是进入 fallback,链条最后都在 finally 中解决对象。
所以 PDFObjects 没有替字体加载制造 Promise,也没有监听 worker 的某个 Promise。是 api.js 在主线程完成必要的包装、绑定与降级后,主动宣布「这个 id 现在可供同步渲染使用」。
finally 里的 font.clearData() 又是另一层资源管理:不需要保留额外字体属性时,字体字节挂入 DOM 后就立即释放,不必等整个 commonObjs.clear()。
另外,commonobj 的字体错误分支会 resolve(id, exportedError),而不是 reject 条目 Promise。这并不矛盾:PDFObjects 只统一「已有一个最终值」的就绪语义,不定义上层如何处理错误值。
条目 Promise 从不跨线程。 worker 通过 send("commonobj", ...) 或 send("obj", ...) 发送可结构化克隆/可转移的数据;主线程消息处理器收到后才调用本地 resolve。跨线程请求—应答 Promise 属于 MessageHandler,条目 Promise 属于主线程 PDFObjects,两者不要拼接成一条虚构的 Promise 链。
渲染循环怎样使用两种 get
依赖等待集中在 CanvasGraphics.executeOperatorList 的 OPS.dependency 分支,而不是散落在每个绘画算子里:
for (const depObjId of fnArgs) {
this.dependencyTracker?.recordNamedData(depObjId, i);
const objsPool = depObjId.startsWith('g_') ? commonObjs : objs;
// If the promise isn't resolved yet, add the continueCallback
// to the promise and bail out.
if (!objsPool.has(depObjId)) {
objsPool.get(depObjId, continueCallback);
return i;
}
}has 为真时,循环直接越过依赖算子,后续绘画算子通过无回调 get 同步取数据。has 为假时,才用带回调 get 登记 continueCallback,并从当前索引 i 返回。
返回值是 i 而不是 i + 1 很重要。InternalRenderTask 把它保存为 operatorListIdx;对象就绪后的下一轮会重新检查同一个依赖算子,确认所有依赖都能同步读取,再真正执行后续绘画。
continueCallback 在这里是 InternalRenderTask._continueBound。条目回调虽然会传入 obj.data,_continue() 并不使用这个参数;它只把通知当成重新调度的信号。数据仍由后续算子从注册表同步取出。
时序一:对象先解决,渲染后登记依赖
以一张已经由 obj 消息解决到页缓存的图片为例:
| 顺序 | 发生的动作 | 结果 |
|---|---|---|
| 1 | 消息处理器执行 pageProxy.objs.resolve(id, imageData) | 条目直接从未登记变为已解决 |
| 2 | 渲染循环后来遇到 OPS.dependency,has(id) 为 true | 不注册等待,不暂停 |
| 3 | 图片算子调用 getObject,进入无回调 get | 同一调用栈内返回 imageData |
这条路径的主线程绘制热路径没有等待成本。条目 Promise 虽已履行,但渲染循环甚至不需要碰它,只用 has 与同步 get。
如果其他代码此时显式调用 get(id, callback),getOrInsertComputed 会返回这个已解决条目,callback 仍通过已履行 Promise 的 .then 排入微任务;这不会影响无回调读取的同步性。
时序二:渲染先登记等待,对象后解决
worker 在解码图片前就会向 operatorList 添加依赖,因此下面这条路径同样正常:
| 顺序 | 发生的动作 | 结果 |
|---|---|---|
| 1 | canvas 遇到依赖,has(id) 为 false | 对象可能未登记,也可能已经 pending |
| 2 | get(id, continueCallback) 复用或创建条目,并注册 .then | get 返回 null,executeOperatorList 返回当前 i |
| 3 | 图片消息稍后到达,api.js 调用 resolve(id, imageData) | 先写 data,再履行本地 Promise |
| 4 | Promise 微任务调用 _continueBound | 渲染任务安排下一轮,而不是在消息回调里直接画 |
| 5 | 从索引 i 重试,has(id) 为 true | 越过依赖,绘画算子同步取得图片 |
字体的「消息先到但尚未就绪」也归到第二条路径:commonobj 已到达不代表 has 为真,只有字体绑定/fallback 链走到 finally 并调用 resolve 后,等待渲染才会继续。
两条路径能共用同一份代码,靠的就是 getOrInsertComputed 保留条目身份、data 保存同步状态、Promise 只负责通知。谁先谁后只改变状态迁移顺序,不改变最终读取契约。
设计要点
1. 哨兵表达三态,胜过真值判断
如果只用 Map.get(id) 的返回值判断,就无法区分未登记、pending、null 和其他假值。私有 Symbol 把「未解决」从所有合法 payload 中隔离出来,resolve 的默认 null 也因而安全。
2. 稳定条目同时服务生产者和等待者
get 与 resolve 都通过 getOrInsertComputed 获得同一个条目。先注册回调不会挡住后来的数据,先解决数据也不会让后注册回调丢失通知;不需要另外维护等待者数组或消息重放缓存。
3. 异步只出现在依赖边界,绘制保持同步
PDFObjects 没有把 canvas 的每个算子改造成 async。依赖算子负责暂停与恢复,真正绘画时统一同步读取,这让渲染状态机、异常路径与绘制顺序都保持简单。
4. 「就绪」由消费者定义,而不是由消息到达定义
图片可以收到后立即解决;字体必须等主线程绑定或 fallback 完成。PDFObjects 提供的是中立的通知机制,api.js 才把不同资源各自的准备工作纳入同一个 id 就绪契约。
5. 单次解决与 pending 删除保护的是等待关系
禁止重复解决防止最终值被覆盖;禁止删除 pending 防止旧等待者与新条目失联。这两条约束共同保证一个登记生命周期里的通知关系闭合。
6. 缓存作用域与资源释放必须一起设计
文档级和页级两个池避免跨页资源被过早清理,也让页资源可以独立回收。clear 主动关闭 ImageBitmap,迭代器只暴露已解决数据,api.js 再补上清理后的迟到消息防线,才构成完整的资源所有权链。
总结
PDFObjects 不是通用缓存,也不是跨线程 Promise 封装。它是一个只关心渲染对象生命周期的三态注册表:未登记、已登记未解决、已解决;以 INITIAL_DATA 做同步判定,以本地 Promise 做异步通知。
读懂这 116 行,渲染链路里两个看似矛盾的行为就统一了:CanvasGraphics 可以在依赖未就绪时通过回调停等,又能在真正绘制时用 get 同步取对象。commonObjs 与 objs 决定数据能共享多久,resolve 的决定时机决定数据何时真的可用,delete / clear / iterator 则守住等待关系与资源释放边界。
从消息到像素,PDF.js 没有把异步扩散到整个绘制器,而是把它压缩在「依赖未就绪」这一条边上。这正是这个小文件在展示层中的价值。
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 源码解读: 网络流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日