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

前言
这是「图像解码」层的第二篇文章,对象是 Worker 端 src/core/jbig2_stream.js(104 行)中的 Jbig2Stream。
上一篇 jpeg_stream 处理的是连续色调的 DCT 码流;本篇处理的是二值图像。JBIG2 由 ITU-T T.88 定义,可以看作传真编码(CCITT G3/G4)的后继:它面向黑白扫描页,用符号字典、算术编码与区域组合把 1 位像素压到很小的体积。扫描版 PDF 里的文字层、传真归档、部分电子书,背后往往就是一段 JBIG2 码流。
它在 PDF 里的过滤器名是 /JBIG2Decode,入口同样来自 Parser.makeFilter:
case "JBIG2Decode":
return new Jbig2Stream(stream, maybeLength, params);PDF.js 没有用 JavaScript 从头实现 JBIG2,而是把 PDFium 的解码器编译成 WASM,并提供纯 JS 回退。这个选择很实际:JBIG2 的算术编码与符号字典组合远比 JPEG 的 Huffman 加 IDCT 复杂,纯 JS 实现既慢又难维护。代价是解码变成异步,整个流类因此要围绕 Promise 重新设计。
与 JPEG 相比,这里多了三件麻烦事:
- JBIG2 有一个独立的文件头,而 PDF 里嵌入的是不带文件头的段流;有些生成器仍然把文件头写进去,解码前必须剥掉。
- 一份图像的符号字典可以放在另一条流里,通过
/DecodeParms的/JBIG2Globals引用。主码流与全局段要分别取回、分别剥头,再一起交给解码器。 - 真正的解码器是一个异步的 WASM 单例,首次调用还要加载
jbig2.wasm(或纯 JS 回退模块)。同步getBytes那条路在这里走不通。
一句话概括它的职责:取回主码流与全局段,剥掉可能存在的 JBIG2 文件头,把字节、字典尺寸与全局段交给 WASM 解码器,异步得到一幅 1 位像素缓冲。
模块总览
jbig2_stream.js 只有 104 行,主体是一类、五个方法与两个 getter:
| 区块 | 关键成员 | 职责 |
|---|---|---|
| 构造 | constructor | 保存上游流、图像字典、长度与 DecodeParms |
| 字节入口 | bytes、ensureBuffer | 惰性读取压缩字节,声明不做增量缓冲 |
| 类型标记 | isAsyncDecoder、isImageStream | 声明异步解码与图像身份 |
| 预处理 | stripFileHeader | 去掉 JBIG2 文件头,主码流与全局段共用 |
| 解码 | decodeImage | 取全局段,调用 WASM 解码器,异步写入缓冲 |
| 异步能力 | canAsyncDecodeImageFromBuffer | 把「能否先异步取字节」委托给上游流 |
它和 JpegStream 共享同一套「把整图解码器嵌入流接口」的模板:惰性 bytes、空实现的 ensureBuffer、委托上游的 canAsyncDecodeImageFromBuffer、恒为 true 的 isImageStream。差别在于解码函数返回 Promise,因此这个类不能走同步 getBytes。
三个布尔标记回答的是三个不同的问题,读源码时要分开看:
| 标记 | 回答的问题 | 本类实现 |
|---|---|---|
canAsyncDecodeImageFromBuffer | 压缩字节能否先异步取齐 | 委托 this.stream.isAsync |
isAsyncDecoder | 解码函数本身是否返回 Promise | true |
isImageStream | 这是图像流还是页面内容流 | true |
三者组合出 DecodeStream.getImageData 的两条分支:上游可异步时先 asyncGetBytes 再解码;上游同步时因为 isAsyncDecoder 为真,直接调用 decodeImage 并等待 Promise。同步 getBytes 永远不会被图像链路选中。
stripFileHeader 是静态方法,因为它不依赖实例状态:主码流与全局段只是两个字节数组,切头规则完全相同。静态化让它可以被独立测试,也避免为全局段临时构造一个流对象。
继承与构造
类的声明与 JpegStream 几乎逐行对应:
class Jbig2Stream extends DecodeStream {
constructor(stream, maybeLength, params) {
super(maybeLength);
this.stream = stream;
this.dict = stream.dict;
this.maybeLength = maybeLength;
this.params = params;
}constructor 方法
四个字段的语义与上一篇相同,但用途各有侧重:
stream是上游流,可能是内存Stream,也可能是网络分块流。bytes与asyncGetBytes都从它取数据。dict是图像字典。JBIG2 的/Width与/Height不来自码流,而是从这里读,后文会展开。maybeLength来自/Length,为null时表示一直读到上游 EOF。JBIG2 段流没有 JPEG 那样的 EOI,读到末尾就是完整数据。params是/DecodeParms。/JBIG2Globals就挂在这里,它是本类区别于 JPEG 的关键输入。
super(maybeLength) 同样会计算 minBufferLength,但 ensureBuffer 是空实现,这套扩容逻辑不会执行。
bytes 惰性 getter
压缩字节仍然通过 getter 惰性读取并缓存:
get bytes() {
// If `this.maybeLength` is null, we'll get the entire stream.
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
}shadow 第一次访问时执行 getBytes,之后把 getter 替换成同名的自有数据属性,后续读取不再触达上游。与 JPEG 一样,这里有一个同步副作用:网络分块流如果还没下载完,getBytes 会抛 MissingDataException。DecodeStream.getImageData 在 canAsyncDecodeImageFromBuffer 为真时会先 asyncGetBytes,正是为了绕开这次同步读取。
ensureBuffer、isAsyncDecoder 与 isImageStream
接下来三个成员都很短,却决定了这个类能不能被消费:
ensureBuffer(requested) {
// No-op, since `this.readBlock` will always parse the entire image and
// directly insert all of its data into `this.buffer`.
}
get isAsyncDecoder() {
return true;
}
get isImageStream() {
return true;
}ensureBuffer 空实现沿用了图像流的写法:不为「增量写入」预留空间,反正整幅图一次解码后直接替换 buffer。
但这里有一个细节值得指出:注释说 readBlock 会解析整幅图,而 Jbig2Stream 并没有覆盖 readBlock。基类的 readBlock 实现是 unreachable("Abstract method ...")。也就是说,这个类从设计上就不支持同步 getBytes;如果哪条调用链真的走到 readBlock,会立刻抛错。真正让图像链路绕开同步读取的,是 isAsyncDecoder。
isAsyncDecoder 在 BaseStream 里默认是 false。它告诉 DecodeStream.getImageData:即使上游字节已经就绪,也要走 decodeImage 并 await 它,而不能走 getBytes。原因很直接:WASM 模块加载与 decode 都是 Promise,没有任何办法把它们伪装成同步字节流。
isImageStream 则把这条流与页面内容流区分开。StreamsSequenceStream 拼接内容时会过滤掉它,PDFPage.getContentStream 也不会把像素当操作符解析。JBIG2 流尤其需要这个标记,因为它连同步读取都不支持,一旦被当成普通流处理就会直接抛错。
设计要点:JPEG 用
readBlock把异步世界伪装成同步世界;JBIG2 不伪装,直接声明isAsyncDecoder,让上层走异步入口。同一个模板在「解码器是否可同步驱动」这一点上分叉。承认限制比制造一个假的同步接口更安全:前者会在类型层面暴露,后者只会在运行时炸开。
stripFileHeader:剥掉独立文件头
stripFileHeader 方法
JBIG2 的独立文件格式与 PDF 内嵌的段流不是一回事。规范 T.88 的 Annex D.4 定义了文件头,PDF 里通常存的是「embedded」组织形式,不带这层头:
// The JBIG2 file header is defined in ITU-T T.88, Annex D.4:
// https://www.itu.int/rec/T-REC-T.88
static stripFileHeader(bytes) {
if (
bytes.length >= 9 &&
bytes[0] === 0x97 &&
bytes[1] === 0x4a &&
bytes[2] === 0x42 &&
bytes[3] === 0x32 &&
bytes[4] === 0x0d &&
bytes[5] === 0x0a &&
bytes[6] === 0x1a &&
bytes[7] === 0x0a
) {
const headerLength = (bytes[8] & 2) === 0 ? 13 : 9;
return bytes.subarray(headerLength);
}
return bytes;
}文件头的布局可以写成一张表:
| 偏移 | 长度 | 内容 |
|---|---|---|
| 0 到 7 | 8 | 魔数 0x97 0x4A 0x42 0x32 0x0D 0x0A 0x1A 0x0A |
| 8 | 1 | flags,其中掩码 2 的位表示页数是否未知 |
| 9 到 12 | 4 | 页数,仅当掩码 2 的位为 0 时存在 |
三个判断:
- 至少 9 字节。魔数加 flags 就是 9 字节;更短的输入不可能带合法文件头,直接原样返回。
- 逐字节比对魔数。八个常量分别对应 JBIG2 的签名,任何一位不符就认为这是纯段流,不做切割。这里没有用
TextDecoder或字符串比较,避免在字节层引入编码开销。 - 由 flags 决定 9 还是 13。
bytes[8]中掩码为2的那一位如果为 0,说明文件头里记录了页数,后面还有 4 字节,总长 13;如果为 1,页数未知,头只有 9 字节。源码写成(bytes[8] & 2) === 0 ? 13 : 9,位运算和三元表达式的顺序很容易读反。
返回值用 subarray,是视图而不是副本。这一点与 JPEG 的 #skipUselessBytes 一致:剥头只改变读取起点,不搬运数据。主码流与全局段都调用这个静态方法,避免两份实现分叉。
之所以要剥头,是因为 WASM 侧的 _jbig2_decode 接收的是段流:它自己解析段头、区域信息与符号字典,不期待前面再套一层文件头。保留文件头会让第一个段头解析错位,后续整段数据都读歪。
段流的组织形式
文件头之下是连续的 segment(段)。每个段由段头与段数据组成,段头记录段号、段类型、页关联、被引用段列表与数据长度。类型决定段体是页信息、符号字典、中间区域还是文字区域;页关联决定它属于哪一页;被引用段列表让符号字典可以跨段复用。
| 字段 | 作用 | 与 PDF 的关系 |
|---|---|---|
| 段号 | 唯一标识一个段 | 全局段里的字典靠段号被引用 |
| flags | 段类型、页关联长度、引用计数 | 决定段头后续字段如何解析 |
| 页关联 | 该段属于哪一页 | 多页码流据此拆分 |
| 数据长度 | 段体的字节数 | 长度错误会让整段解析失败 |
stripFileHeader 不碰这些字段,只把文件头切掉;段头的解析全部交给 WASM。流层不重复实现规范,这是边界清晰的地方:PDF 的兼容问题在 JavaScript 里解决,JBIG2 的语法问题在解码器里解决。
decodeImage:把三个输入交给 WASM
decodeImage 方法
真正的解码入口是异步的:
async decodeImage(bytes, length, _decoderOptions) {
if (this.eof) {
return this.buffer;
}
bytes = Jbig2Stream.stripFileHeader(bytes || this.bytes);
let globals = null;
if (this.params instanceof Dict) {
const globalsStream = this.params.get("JBIG2Globals");
if (globalsStream instanceof BaseStream) {
globals = Jbig2Stream.stripFileHeader(globalsStream.getBytes());
}
}
this.buffer = await JBig2CCITTFaxImage.instance.decode(
bytes,
this.dict.get("Width"),
this.dict.get("Height"),
globals
);
this.bufferLength = this.buffer.length;
this.eof = true;
return this.buffer;
}逐段拆解:
eof守卫。与 JPEG 相同,解完直接返回已有缓冲。区别是这里的缓冲来自 WASM,类型与普通Uint8Array不完全一样,但上层只按字节长度与位深解释。- 主码流剥头。
bytes || this.bytes允许异步路径传入已经取好的字节;不传时走惰性 getter。无论来源,先过stripFileHeader。 - 全局段可选。
params必须是Dict才有/JBIG2Globals;取到的值必须是BaseStream,否则当作没有全局段。全局段自己也可能带文件头,所以同样剥一次。 - 字典尺寸。
Width与Height从this.dict读取,不是从params,也不是从 JBIG2 段头猜。PDF 的图像字典是最终尺寸的权威来源,码流里的区域信息只描述如何拼出这幅图。 - WASM 解码。
JBig2CCITTFaxImage.instance是一个shadow缓存的单例,decode接收主码流、宽、高与全局段,返回 Promise。 - 提交结果。
buffer直接替换,bufferLength取长度,eof置真。length与_decoderOptions都被忽略:JBIG2 没有按请求长度解一半的能力,也没有可调的解码选项。
注意 eof = true 是在 await 之后才执行的。它不是并发锁:如果两个调用方在解码完成前同时进入,两边都会看到 eof 为假。实际链路里同一张图通常由一次 getImageData 驱动,但这个细节说明「幂等」只保证重复读取不重复解码,不保证并发只解一次。
JBIG2Globals 与全局段
JBIG2 的码流由一个个段组成:页信息段、符号字典段、中间区域段、文字区域段等。全局段存放可以跨页复用的符号字典与通用区域。PDF 把它们拆成两条流:
- 图像流本身只放当前页的段;
/DecodeParms里的/JBIG2Globals指向另一条流,存放共享段。
解码器需要同时看到两者,才能把符号编号映射回真实位图。源码的取法很直接:
if (this.params instanceof Dict) {
const globalsStream = this.params.get('JBIG2Globals');
if (globalsStream instanceof BaseStream) {
globals = Jbig2Stream.stripFileHeader(globalsStream.getBytes());
}
}三个约束藏在短短四行里:
- 只认
BaseStream。/JBIG2Globals可能缺失、可能是null、也可能被损坏文件写成别的类型;这些情况都退化成「没有全局段」,而不是抛错。 - 同步取字节。
globalsStream.getBytes()不带长度,读到该流 EOF。全局段通常不大,且与图像字典一起被取回,这里没有再走一次asyncGetBytes。 - 全局段也剥头。有些生成器把全局段单独存成完整 JBIG2 文件,文件头同样不属于段流。主码流与全局段共用
stripFileHeader,保证两条输入的组织形式一致。
如果漏掉全局段,带符号字典的 JBIG2 图像会解出一堆空白或直接失败:解码器在文字区域里读到符号编号,却找不到对应的位图。这个字段在 PDF 规范里是 /DecodeParms 的可选项,但在扫描件里出现频率很高。
Width 与 Height 的权威来源
decode 的第二个、第三个参数是 this.dict.get("Width") 与 this.dict.get("Height")。这一点与 JPEG 不同:JPEG 的 SOF 段自带尺寸,JpegImage 可以从码流里读;JBIG2 的段流描述的是区域与组合方式,最终位图的尺寸由 PDF 图像字典规定。
因此会出现三种情况:
| 字典尺寸 | 码流内容 | 结果 |
|---|---|---|
| 与页信息段一致 | 正常单页 | 按字典尺寸输出,最常见 |
| 大于码流实际区域 | 多区域拼接或空白填充 | 解码器按给定尺寸布置区域,空白处留白 |
| 缺失或非正整数 | 任意 | WASM 收到非法尺寸,解码失败,由图像链路兜底 |
源码没有在这里做类型校验,直接把 dict.get 的结果传下去。校验责任在 Parser 与 PDFImage:图像字典的 Width、Height 是必填项,损坏到缺失时,失败会发生在解码器内部,而不是流层。
WASM 单例与输出格式
JBig2CCITTFaxImage 继承自 WasmImage,负责加载 jbig2.wasm;环境不支持 WASM 时回退到 jbig2_nowasm_fallback.js。instance 是 shadow 缓存的单例,整个 Worker 共用一份模块实例:
- 首次调用时加载并初始化模块;
- 每次解码用
_malloc把主码流与全局段复制进 WASM 堆; - 调用
_jbig2_decode,由 C 侧完成算术解码、符号组合与位图合成; - 结果写到
module.imageData,再复制成 JavaScript 的Uint8ClampedArray; finally里释放两块堆内存,避免跨文档累积。
输出是1 位打包的像素行,而不是已经展开的 RGBA。这与 PDF 的 /BitsPerComponent 1、/ColorSpace /DeviceGray 对齐:Worker 里保持紧凑格式,等 PDFImage 决定绘制方式时再展开。
WasmImage.cleanup 在文档清理时可以丢弃模块 Promise,但单例对象本身仍然存在;下一次解码会重新初始化。这个生命周期由 Catalog.cleanup 等入口调用的 clearGlobalCaches 驱动,不在本文件的职责范围内。
1 位像素如何被消费
PDFImage.createImageData 拿到这条流时,因为 image instanceof DecodeStream,会直接复用 buffer,不在 Worker 里展开成 RGBA;需要反相时只在 1 位缓冲上做一次异或。位图种类被标记为 ImageKind.GRAYSCALE_1BPP,一个字节承载八个像素。
若 Worker 支持 OffscreenCanvas,createBitmap 用 convertToRGBA 展开成 RGBA 后 transferToImageBitmap;否则把 1 位缓冲与 kind 一起传回主线程,由显示端展开。两条路都把「解压」与「上色」分开:WASM 只产出位平面,颜色空间与反相留到绘制前。对整页扫描图来说,这一步省下的是一整幅 RGBA 的中间内存。
设计要点:
decodeImage把「格式预处理」「可选输入」「尺寸权威」三件事全部放在流层完成,WASM 侧只接收干净的段流、宽高与全局段。边界越窄,解码器越不需要理解 PDF 的损坏方式;反过来,PDF 的兼容逻辑也不会渗进 C 代码。
异步取字节
canAsyncDecodeImageFromBuffer 方法
最后一个方法与 JPEG 完全一致:
get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}它只回答「上游字节能否异步取齐」,不回答「解码是否异步」。两者组合起来,DecodeStream.getImageData 有两条路径:
上游 isAsync | isAsyncDecoder | getImageData 的行为 |
|---|---|---|
true | true | 先 await asyncGetBytes,再 await decodeImage |
false | true | 直接 await decodeImage(null, ...) |
false | false | 同步 getBytes,循环 readBlock |
对本类来说,第三行永远不会出现,因为 isAsyncDecoder 恒为真;如果强行调用 getBytes,会落到基类未实现的 readBlock。第二行是本地文件场景:字节已经就绪,但解码仍然是 Promise,必须等待。第一行是网络场景:先把压缩字节取齐,避免解码中途缺数据。
这里没有 MissingDataException 重试逻辑,因为 decodeImage 本身不是可恢复的解析器。WASM 一旦开始读堆内存,就没有「暂停再续」的接口;把字节取齐是唯一正确的做法。
这也是为什么本类不提供 readBlock:同步接口要求一个能立即返回的块解码函数,而 WASM 调用必须等待模块加载与 Promise 结算。与其写一个会抛错的伪实现,不如让 isAsyncDecoder 把调用方引到正确入口。
isImageStream 与消费者
isImageStream 的三行实现与 JPEG 相同,但对 JBIG2 更重要:
get isImageStream() {
return true;
}全仓库只有两处消费者:
StreamsSequenceStream构造时过滤图像流,避免把像素拼进内容流;PDFPage.getContentStream排除图像流,避免把 JBIG2 字节当成页面操作符。
JBIG2 流没有可用的 readBlock,一旦被误当普通流读取,会立刻抛「抽象方法被调用」。isImageStream 把这种误用挡在类型判断阶段,而不是等运行时爆炸。
四个常见误读
- 把
/JBIG2Globals当成主码流的一部分。它是/DecodeParms引用的独立流,必须单独取回、单独剥头,再作为第四个参数传给解码器。 - 认为
stripFileHeader只处理主码流。全局段同样可能带完整的 JBIG2 文件头,源码对两条输入一视同仁。 - 看到
ensureBuffer空实现就以为同步getBytes可用。readBlock并没有被覆盖,同步读取会落到基类的unreachable;正确入口是getImageData,由isAsyncDecoder分流。 - 把
Width、Height当成/DecodeParms的字段。它们在图像字典里,源码从this.dict读取,params只负责/JBIG2Globals。
与 jpeg_stream 的对照
| 维度 | JpegStream | Jbig2Stream |
|---|---|---|
| 过滤器 | /DCTDecode、/DCT | /JBIG2Decode |
| 解码器 | 纯 JS JpegImage | WASM 单例 JBig2CCITTFaxImage |
| 解码同步性 | 同步 | 异步 |
| 同步读取 | 可用,readBlock 触发整图解码 | 不可用,未覆盖 readBlock |
| 文件头 | 只需跳过 SOI 之前的垃圾字节 | 需按 flags 剥 9 或 13 字节文件头 |
| 额外输入 | /ColorTransform | /JBIG2Globals 全局段 |
| 尺寸来源 | drawWidth、drawHeight 可缩放 | 字典 Width、Height,不可缩放 |
| 输出 | 1、3 或 4 通道像素 | 1 位打包像素 |
| 原生加速 | 可选 ImageDecoder 路径 | 无,统一走 WASM |
两篇文章放在一起看,会发现「惰性字节 + 空 ensureBuffer + 异步能力委托 + isImageStream」是图像流的标准模板;真正不同的只有解码器接口与格式预处理。把模板与差异分开,读新过滤器时只需要回答三个问题:字节怎么来、解码器是同步还是异步、码流前面有没有格式头。
总结
Jbig2Stream 是二值图像解码层的适配器:它把 PDF 的两条流(主码流与全局段)整理成一份干净的段流输入,剥掉不属于段流的文件头,再把宽高与全局段一起交给异步 WASM 解码器。
它在整条链路中的位置
| 阶段 | 模块 | 输入 | 输出 |
|---|---|---|---|
| 过滤器 | Parser.makeFilter | /JBIG2Decode、流字典 | Jbig2Stream |
| 取字节 | bytes、canAsyncDecodeImageFromBuffer | 上游 Stream 或分块流 | 完整主码流字节 |
| 预处理 | stripFileHeader | 主码流、全局段 | 去头的段流 |
| 解码 | decodeImage → JBig2CCITTFaxImage.instance | 段流、Width、Height、全局段 | 1 位像素缓冲 |
| 图像封装 | PDFImage.createImageData | 1 位缓冲 | ImageBitmap |
| 绘制 | CanvasGraphics | ImageBitmap | canvas 上的图像 |
用一句话串起来:Parser 把 /JBIG2Decode 包装成 Jbig2Stream,流层剥头并汇总全局段,WASM 把段流解成 1 位位图,PDFImage 再决定如何展开与绘制。
五条设计原则
- 复用图像流模板,只替换解码器。惰性
bytes、空ensureBuffer、委托上游的canAsyncDecodeImageFromBuffer、恒真的isImageStream,四项与 JPEG 完全一致;新格式只需要补自己的预处理与解码调用。 - 格式头在进入解码器前解决。
stripFileHeader同时服务主码流与全局段,按 flags 精确切 9 或 13 字节,并用subarray避免复制。解码器因此只需要理解段流这一种输入。 - 可选输入显式降级。
/JBIG2Globals缺失、类型不对时把globals保持为null,而不是抛错。带符号字典的图会因此失败,但不带字典的图仍能正常解码,容错范围被控制在最小。 - 异步能力必须显式声明。
isAsyncDecoder让上层走getImageData而不是getBytes,避免调用未实现的readBlock。不伪造同步接口,是把限制暴露在类型层而不是运行时。 - 中间结果保持紧凑。WASM 输出 1 位打包像素,不在 Worker 里提前展开成 RGBA;真正的展开推迟到
PDFImage决定绘制路径时。像素越大,这条原则省下的内存越多。
至此,「JBIG2 怎么变成二值位图」这一环补齐了。图像解码层还剩 JPX(JPEG 2000)与 CCITT 传真:前者同样是异步 WASM,后者是纯 JS 的位流解码,但都沿用本篇文章里的模板。读完这三类,再看 PDFImage 如何根据 ColorSpace、掩码与 BitsPerComponent 解释像素,链路就闭合了。
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- 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 源码解读: 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 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日