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

前言
上一篇 jpx_stream 讲的是 JPEG 2000 的整图解码:码流完整取到手,交给 OpenJPEG WASM,一次产出像素。本篇继续停留在「图像解码」层,对象是 src/core/ccitt_stream.js(79 行)里的 CCITTFaxStream。
CCITT 这个名字来自国际电报电话咨询委员会(Comité Consultatif International Téléphonique et Télégraphique)。它制定的 T.4 与 T.6 建议,定义了传真机的图像压缩方式:T.4 是 Group 3,T.6 是 Group 4。两者都只处理一位像素的二值图像——每个像素非黑即白,没有灰度,也没有颜色。这正是传真、扫描合同、老式票据最常见的形态:一页 A4 在 300 dpi 下约有 870 万个像素,如果用 8 位灰度存要 8 MB 多,用一位存只要 1 MB 出头,再叠一层游程编码,往往只剩几十 KB。
PDF 从 1.0 起就支持这套编码,过滤器名是 /CCITTFaxDecode,缩写 CCF。它常出现在扫描件的图像 XObject 上,也常与 /JBIG2Decode 并列出现——两者都在压二值图像,只是一个面向传真,一个面向文档。
入口同样在 Parser.makeFilter:
case "CCF":
case "CCITTFaxDecode":
return new CCITTFaxStream(stream, maybeLength, params);与 JPX 不同,这里把 params 一起传了进来。CCITT 的编码方式、行宽、字节对齐、颜色极性全部由 /DecodeParms 描述,解码器必须拿到这些参数才能解释码流。于是这个类有两个职责:把 PDF 字典压成解码器能直接使用的平面参数对象,以及把码流交给 WASM 完成整图解码。
一句话概括它的职责:归一化七个传真编码参数,异步取齐底层字节,连同图像宽高一起交给共享的 JBig2/CCITT WASM 解码器。
模块总览
ccitt_stream.js 只有 79 行,去掉版权头与 import,有效代码不到六十行:
| 区块 | 关键成员 | 职责 |
|---|---|---|
| 构造 | constructor | 保存上游流与字典,把 params 归一化成平面对象 |
| 字节 | bytes | 惰性取出完整码流,并用 shadow 缓存 |
| 能力声明 | isImageStream、isAsyncDecoder | 标记这是图像流,且解码器是异步的 |
| 解码 | decodeImage | 自行处理异步取字节,委派 WASM 解码 |
| 继承设施 | DecodeStream | buffer、bufferLength、eof、getImageData |
| 底层内核 | JBig2CCITTFaxImage.instance | 共用 JBIG2 模块里的 CCITT 解码入口 |
它没有覆盖 readBlock,也没有覆盖 ensureBuffer。整个类只通过 decodeImage 工作,属于上一篇描述过的「整图解码」模式。
数据路径可以写成:
图像字典 /Filter /CCITTFaxDecode
→ Parser.makeFilter(stream, maybeLength, params)
→ CCITTFaxStream 构造(七个参数归一化)
→ DecodeStream.getImageData
→ decodeImage(null, length, options)
→ 异步取字节 / 同步回退
→ JBig2CCITTFaxImage.instance.decode
→ 像素缓冲
→ PDFImage注意 getImageData 传入的第一个参数是 null,不是字节。这个细节决定了它与 JpxStream 的分工方式,后面会专门对比。
构造:把 Dict 外壳压成平面参数
constructor 方法
class CCITTFaxStream extends DecodeStream {
constructor(str, maybeLength, params) {
super(maybeLength);
this.stream = str;
this.maybeLength = maybeLength;
this.dict = str.dict;
if (!(params instanceof Dict)) {
params = Dict.empty;
}
this.params = {
K: params.get("K") || 0,
EndOfLine: !!params.get("EndOfLine"),
EncodedByteAlign: !!params.get("EncodedByteAlign"),
Columns: params.get("Columns") || 1728,
Rows: params.get("Rows") || 0,
EndOfBlock: !!(params.get("EndOfBlock") ?? true),
BlackIs1: !!params.get("BlackIs1"),
};
}构造函数做四件事。
第一,调用 super(maybeLength)。基类把长度提示换算成不小于它的 2 的幂,作为初始容量。对整图解码来说,这个容量基本用不上,因为结果会直接覆盖 this.buffer;但保持与其它流一致的初始化没有坏处。
第二,保存上游流、长度提示与字典:
| 字段 | 来源 | 用途 |
|---|---|---|
this.stream | 第一个参数 | 提供 getBytes 与 asyncGetBytes |
this.maybeLength | 第二个参数 | 决定 bytes 向上游要多少字节 |
this.dict | str.dict | 读取图像宽高 W、Width、H、Height |
第三,处理 params 不是字典的情况:
if (!(params instanceof Dict)) {
params = Dict.empty;
}/DecodeParms 可能缺失,可能写成 PDF 的 null,也可能是一个被解析成非字典对象的损坏条目。Dict.empty 是一个通过 shadow 缓存的单例,它的 set 被替换成 unreachable,只读不写:
static get empty() {
const emptyDict = new Dict(null);
emptyDict.set = (key, value) => {
unreachable("Should not call `set` on the empty dictionary.");
};
return shadow(this, "empty", emptyDict);
}用单例而不是 new Dict(),是为了避免每个没有参数的 CCITT 流都分配一个空对象。空字典上的所有 get 都返回 undefined,于是七个字段全部落到默认值。
第四,构造平面对象 this.params。这是整个类最重要的动作,把「PDF 对象」与「解码器选项」两个世界隔开。字段与默认值如下:
| 字段 | 表达式 | 默认值 | 含义 |
|---|---|---|---|
K | params.get("K") || 0 | 0 | 编码组别与二维行周期,负值表示 Group 4 |
EndOfLine | !!params.get("EndOfLine") | false | 每行是否带 EOL 标记 |
EncodedByteAlign | !!params.get("EncodedByteAlign") | false | 每行是否对齐到字节边界 |
Columns | params.get("Columns") || 1728 | 1728 | 编码行的像素宽度 |
Rows | params.get("Rows") || 0 | 0 | 编码行数,0 表示不预设上限 |
EndOfBlock | !!(params.get("EndOfBlock") ?? true) | true | 码流是否以 EOB 标记结束 |
BlackIs1 | !!params.get("BlackIs1") | false | 1 位是否为黑;默认 0 位为黑 |
这些表达式有几个容易读错的细节。
EndOfBlock 用的是空值合并:
EndOfBlock: !!(params.get("EndOfBlock") ?? true),?? 只在 null 或 undefined 时取右侧。于是三种情况被区分开:缺失取 true,PDF false 取 false,PDF true 取 true。如果这里误写成 || true,显式的 false 会被吞掉,所有流都被当成带 EOB 结束标记,这在截断文件上会直接读越界或提前停止。
Columns 的 1728 是 T.4 的标准页宽:8.5 英寸乘 200 dpi 约为 1700 像素,标准化后取 1728。它不是「图像宽度」的替代品,而是「编码行宽度」的默认值;真正的输出宽度仍由图像字典的 W 或 Width 决定。
为什么在边界上完成归一化
this.params 不是一个 Dict,也不是一个配置类,而是一个形状固定、只有七个标量字段的对象。这样做有三个直接好处:
- WASM 边界只认标量。
module._ccitt_decode的参数是一串数字,字典对象过不去。先在 JS 侧拍平,调用处就不需要反复get和类型判断。 - 布尔值提前变成 0/1。PDF 字典里的
true、false、null、缺失,在调用处还要再判断一次;在构造时统一成布尔,解码路径就不再关心 PDF 对象模型。 - 默认值只写一次。七个字段的默认语义集中在构造函数里,读代码的人一眼能看到「什么都没写时会怎样」,不用追到 WASM 里猜。
设计要点:把「PDF 对象」翻译成「解码器选项」的动作放在构造函数,而不是
decodeImage。解码路径越短越好,参数解释越靠边界越好;这样即使将来换一个解码后端,也只需要改这一处映射。
K 值:一个整数表达三种传真模式
K 是 CCITT 参数里信息量最大的一个。PDF 规范用它同时表达「Group 3 还是 Group 4」与「二维行如何穿插」:
K 的取值 | 编码模式 | 规范 | 每行结构 | 典型来源 |
|---|---|---|---|---|
| 负值 | Group 4 | T.6 | 全部为二维编码,行间无 EOL | 大多数现代扫描件、传真存档 |
| 0 | Group 3 一维 | T.4 | 每行都是一维游程编码,行间通常有 EOL | 早期传真、简单二值图 |
| 正值 | Group 3 二维 | T.4 | 每 K 行二维后插入一行一维,作为同步基准 | 混合模式的传真流 |
三种模式的技术差异可以用「参照行」来理解:
- 一维编码只压当前行内部的黑白游程,用 MH(Modified Huffman)码表。解码器不需要知道上一行,独立性强,但压缩率一般。
- 二维编码同时利用当前行与上一行的关系,用 MMR(Modified Modified READ)码。它把当前行切成若干段,每段相对上一行的变化位置来编码,压缩率明显更高,但一旦某行解错,错误会向后传播。
- Group 4 把所有行都做成二维,第一行以虚拟全白参照行开始,之后每行都用上一行作为参照,因此压缩率最高,也最依赖码流的完整性。传真机与扫描仪默认多用它。
- Group 3 二维每隔
K行插入一行一维,相当于定期「重新同步」。K越大压缩越好,抗错越差;常见取值是 2 或 4。
这些模式在 decodeImage 里没有分支。K 只是作为普通整数传给 WASM,由 _ccitt_decode 内部选择对应的码表与状态机。PDF.js 不在 JS 层做任何模式判断,也不校验 K 与 EndOfLine 是否搭配;参数写错时,得到的是解码错误或花屏,而不是一个友好的异常。
设计要点:用一个参数的多态代替三个子类。
K的符号与大小已经完整编码了模式信息,再拆成Group3OneDimStream、Group3TwoDimStream、Group4Stream只会增加类型数量,不增加表达力。
EndOfBlock 与其它开关
EndOfBlock 回答的问题是:码流有没有明确的结束标记?
| 编码模式 | 结束标记 | 组成 |
|---|---|---|
| Group 3 | RTC(Return To Control) | 连续六个 EOL |
| Group 4 | EOFB(End Of Facsimile Block) | 连续两个 EOL |
PDF 规范把 EndOfBlock 的默认值定为 true,意思是「解码器可以期待一个 EOB 标记」。当它为 false 时,生产者承诺不写 EOB,解码器必须用别的方式确定结束,例如 Rows 指定的行数,或者输入数据耗尽。
这个开关容易被写错:有的生成器写了 EOB 却把 EndOfBlock 设成 false,有的截断文件缺少 EOB 却保持默认 true。PDF.js 的处理方式是——归一化后放进 this.params,但这份源码并没有把它传给 WASM。看底层调用就清楚:
if (CCITTOptions) {
module._ccitt_decode(
ptr,
size,
width,
height,
CCITTOptions.K,
CCITTOptions.EndOfLine ? 1 : 0,
CCITTOptions.EncodedByteAlign ? 1 : 0,
CCITTOptions.BlackIs1 ? 1 : 0,
CCITTOptions.Columns,
CCITTOptions.Rows,
);
}六个业务参数依次是 K、EndOfLine、EncodedByteAlign、BlackIs1、Columns、Rows,没有 EndOfBlock。也就是说,结束判断被交给底层解码器与输入长度;this.params.EndOfBlock 当前只是被保存下来,解码路径并没有读取它。
其余四个开关各自控制一件事:
| 开关 | 作用 | 典型取值 |
|---|---|---|
EndOfLine | 每行是否带 EOL 位型 | Group 3 常见 true,Group 4 常见 false |
EncodedByteAlign | 每行是否补齐到字节边界 | 某些 Group 3 传真流为 true |
Columns | 编码行的像素数 | 默认 1728;宽幅扫描件更大 |
Rows | 编码行数 | 0 表示不限制;明确高度时等于图像高度 |
BlackIs1 则决定颜色极性:默认 false,表示 0 位是黑、1 位是白,与 PDF 的 DeviceGray 约定一致;设为 true 时反过来。它与 PDF 图像字典里的 /Decode 数组是两套独立的翻转机制——前者描述编码时的位含义,后者描述渲染时如何映射到颜色。两者作用在不同阶段,修改任何一层都会改变最终颜色;排查黑白颠倒时,要分清是编码极性写错,还是渲染映射写错。
设计要点:开关的默认值必须与规范的「省略时语义」一致。
EndOfBlock用?? true而不是|| true,Columns用|| 1728而不是?? 1728(因为 0 宽没有意义,应回落到默认),每个运算符的选择都在表达一个领域规则。
bytes 与异步取字节
bytes 方法
get bytes() {
// If `this.maybeLength` is null, we'll get the entire stream.
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
}与 JpxStream 完全一致:第一次访问时向上游要 maybeLength 个字节,null 表示读到流结束;shadow 把结果缓存成实例属性,后续不再重复取。
不同之处在 decodeImage 怎么使用它。
decodeImage 方法
async decodeImage(bytes, length, _decoderOptions) {
if (this.eof) {
return this.buffer;
}
bytes ??= this.stream.isAsync
? (await this.stream.asyncGetBytes()) || this.bytes
: this.bytes;
this.buffer = await JBig2CCITTFaxImage.instance.decode(
bytes,
this.dict.get("W", "Width"),
this.dict.get("H", "Height"),
null,
this.params
);
this.bufferLength = this.buffer.length;
this.eof = true;
return this.buffer;
}第一行仍是幂等保护:eof 为 true 时直接返回上次结果。
第二行是重点:
bytes ??= this.stream.isAsync ? (await this.stream.asyncGetBytes()) || this.bytes : this.bytes;它处理四种情况:
| 情况 | 行为 |
|---|---|
bytes 已有值 | 跳过整个表达式,直接解码 |
bytes 为空,上游是异步流 | await this.stream.asyncGetBytes() |
| 异步取回的结果为空 | 回退到 this.bytes,走同步读取 |
bytes 为空,上游不是异步流 | 直接 this.bytes |
为什么这里要自己 await,而不是像 JpxStream 那样交给基类的 getImageData?因为 CCITTFaxStream 没有覆盖 canAsyncDecodeImageFromBuffer。基类的 getter 默认返回 false,于是 DecodeStream.getImageData 走的是这一支:
return this.isAsyncDecoder
? this.decodeImage(null, length, decoderOptions)
: this.getBytes(length, decoderOptions);CCITTFaxStream 的 isAsyncDecoder 是 true,所以 getImageData 把 null 作为第一参数传进来,接着 decodeImage 自己完成「先异步、再同步回退」的取字节逻辑。
这两种写法没有优劣,只有分工差异:
| 维度 | JpxStream | CCITTFaxStream |
| ------------------------------- | ---------------------------- | ------------------ | ------------- | -------------- | --- | ----------- |
| canAsyncDecodeImageFromBuffer | 覆盖为 this.stream.isAsync | 继承默认 false |
| 异步预取位置 | 基类 getImageData | 本类 decodeImage |
| decodeImage 首参 | 预取到的字节,或 null | 通常是 null |
| 同步回退 | bytes | | = this.bytes | 三元表达式内 | | this.bytes |
| 结果 | 两种路径都能解码 | 两种路径都能解码 |
第二种写法把「取字节」和「解码」放在同一个 async 方法里,调用方只需要调用 decodeImage,不用理解基类的预取策略。代价是 getImageData 的异步分支在这里永远不会命中,基类那段代码对本类来说是死路径。
设计要点:
canAsyncDecodeImageFromBuffer是「谁负责预取」的开关,不是「能不能异步」的开关。JpxStream交给基类,CCITTFaxStream留给自己;只要decodeImage内部处理完整,两种选择都成立。
委派给共享的 WASM 解码器
decodeImage 的后半段把五个实参交给 JBig2CCITTFaxImage.instance.decode:
| 实参 | 表达式 | 含义 |
|---|---|---|
bytes | 上面取到的码流 | 完整 CCITT 数据 |
width | dict.get("W", "Width") | 输出图像宽度,兼容内联图像与图像 XObject |
height | dict.get("H", "Height") | 输出图像高度,同上 |
globals | null | CCITT 不使用 JBIG2 的全局段 |
CCITTOptions | this.params | 七个归一化后的编码参数 |
dict.get("W", "Width") 的双键写法来自 PDF 的两种图像形态:内联图像(BI/ID/EI)用缩写 W、H,图像 XObject 用完整名 Width、Height。Dict.get 先查第一个键,缺失再查第二个,因此一行就能同时兼容两者。
第四参数固定为 null 很关键。同一个 WASM 模块既支持 JBIG2 也支持 CCITT,两者的入口是两套:传了 CCITTOptions 就走 _ccitt_decode,否则走 _jbig2_decode,后者才会用到 globals。CCITT 没有「全局段」概念,所以这里显式传 null,确保分支不会误入 JBIG2。
JBig2CCITTFaxImage 的单例由 shadow 缓存,解码方法的核心分支如下:
async decode(bytes, width, height, globals, CCITTOptions) {
const module = await this._getModule(JBig2);
if (!module) {
throw new Jbig2Error("JBig2 failed to initialize");
}
let ptr, globalsPtr;
try {
const size = bytes.length;
ptr = module._malloc(size);
module.writeArrayToMemory(bytes, ptr);
if (CCITTOptions) {
module._ccitt_decode(
ptr,
size,
width,
height,
CCITTOptions.K,
CCITTOptions.EndOfLine ? 1 : 0,
CCITTOptions.EncodedByteAlign ? 1 : 0,
CCITTOptions.BlackIs1 ? 1 : 0,
CCITTOptions.Columns,
CCITTOptions.Rows
);
}CCITTOptions 为空时源码会走 _jbig2_decode 分支,并把 globals 复制进模块;分支结束后检查 module.imageData,摘走结果并置空,最后在 finally 中释放码流内存。
几个事实值得记下来:
- 同一个 WASM 文件名是
jbig2.wasm。CCITT 没有独立的二进制,它复用 JBIG2 模块里的传真解码器。这也是类名JBig2CCITTFaxImage把两者拼在一起的原因。 - 每次解码独立申请内存。
_malloc与writeArrayToMemory复制码流,finally里_free,即使抛错也不泄漏。 - 输出从模块上摘走。
module.imageData = null防止下一次解码读到旧缓冲;如果模块没有产出imageData,直接抛Jbig2Error。 - 宽度与高度来自图像字典,不来自
DecodeParms。Columns与Rows只是编码侧的描述,最终画布尺寸以W、H为准。
这也解释了一个常见困惑:为什么 Columns 明明也是宽度,解码器还要接收 width?因为 Columns 描述「每一行码流代表多少个像素」,而 width 描述「输出缓冲有多宽」。两者在正常文件里相等,但在参数缺失、图像字典被改动或码流损坏时可能不一致,解码器需要同时知道。
与其它图像解码器的对照
把四个图像流放在一起,差异一目了然:
| 维度 | CCITTFaxStream | Jbig2Stream | JpxStream | JpegStream |
|---|---|---|---|---|
| 过滤器 | /CCITTFaxDecode | /JBIG2Decode | /JPXDecode | /DCTDecode |
| 解码后端 | jbig2.wasm 的 CCITT 分支 | jbig2.wasm 的 JBIG2 分支 | openjpeg.wasm | jpg.js 或 ImageDecoder |
| 解码器参数来源 | /DecodeParms 七个键 | /DecodeParms 的 JBIG2Globals | PDFImage 生成的 decoderOptions | /ColorTransform |
| 宽高来源 | W 或 Width、H 或 Height | Width、Height | 码流 SIZ 标记段 | 由 PDFImage 设置的 drawWidth、drawHeight |
canAsyncDecodeImageFromBuffer | 继承默认 false | 覆盖为 this.stream.isAsync | 覆盖为 this.stream.isAsync | 覆盖为 this.stream.isAsync |
| 异步取字节位置 | decodeImage 内部 | 基类 getImageData | 基类 getImageData | 基类 getImageData |
实现 readBlock | 否 | 否 | 否 | 是,内部调用同步 decodeImage |
| 典型内容 | 传真、二值扫描件 | 文档扫描、黑白文字 | 高质量照片、地图、医学影像 | 普通照片、缩略图 |
最值得对照的是 Jbig2Stream:它和 CCITTFaxStream 共用同一个 WASM 类、同一个 jbig2.wasm、同一个 WASM 解码出口,却在两处走了相反的选择。
第一处是参数。Jbig2Stream 需要从 /DecodeParms 里取 JBIG2Globals,那是一个流对象,必须先取字节、剥掉文件头,再作为 globals 传进去;CCITTFaxStream 则把整个字典拍平成七个标量,globals 固定为 null。
第二处是异步策略。Jbig2Stream 覆盖了 canAsyncDecodeImageFromBuffer,让基类先预取;CCITTFaxStream 不覆盖,自己在 decodeImage 里等。两种做法都能工作,但前者把「取字节」留在基类,后者把「取字节」收进子类。读源码时如果只记「图像流都是异步的」,就会漏掉这个分工差异。
设计要点:共享内核不等于共享策略。
JBig2CCITTFaxImage只负责「把字节变成像素」,谁取字节、何时取、要不要预取,由各自的流类决定。内核与策略分离,才能让两个格式在同一模块上保持独立的生命周期。
边界、容错与常见错误
CCITT 的参数多、默认值多,边界情况也比 JPX 密集:
| 情况 | 行为 |
|---|---|
第二次调用 decodeImage | eof 已置位,直接返回 buffer |
/DecodeParms 缺失或不是字典 | 使用 Dict.empty,七个字段全取默认值 |
K 缺失 | 按 0 处理,即 Group 3 一维 |
EndOfBlock 缺失 | 按 true 处理 |
EndOfBlock 显式为 false | 保留 false,但当前版本不把它传给 WASM |
Columns 缺失 | 按 1728 处理 |
Rows 缺失 | 按 0 处理,不预设行数上限 |
BlackIs1 缺失 | 按 false 处理,0 位为黑 |
| 异步取字节返回空 | 回退到 this.bytes 同步读取 |
图像字典缺少 W、Width | width 为 undefined,底层无法建立输出缓冲,通常抛 Jbig2Error |
| 码流截断或标记缺失 | 解码器可能抛错,eof 保持 false,下一次调用重试 |
| 参数与码流不匹配 | 不校验,直接得到错误像素或解码异常 |
三个常见错误值得展开。
第一,K 与 EndOfLine 搭配错误。 Group 4 的码流里没有 EOL,如果误把 EndOfLine 设为 true,解码器会去每行开头找 000000000001,找不到就当成数据位,整幅图从第一行开始错位。反过来,带 EOL 的 Group 3 码流却把 EndOfLine 设为 false,解码器会把 EOL 位型当成普通游程,同样得到花屏。PDF.js 不在 JS 层做一致性校验,而是把参数原样交给解码器,让错误以图像异常的形式暴露。
第二,BlackIs1 与 /Decode 数组叠加。 BlackIs1 描述编码侧:false 时 0 位代表黑。/Decode 数组描述渲染侧:[1 0] 会把灰度反过来。两者作用在不同阶段,修改任何一层都会改变最终颜色;排查黑白颠倒时,要分清是编码极性写错,还是渲染映射写错。PDF.js 在 PDFImage 里根据 /Decode 计算 needsDecode,在像素阶段统一处理;BlackIs1 则在 WASM 解码时就生效。
第三,Rows 与图像高度的关系。 Rows 是编码侧的声明,图像字典的 H、Height 是输出侧的事实。正常文件两者相等;当 Rows 为 0 时,解码器只能依靠 EOB 或输入结束;当 Rows 大于实际行数时,多出来的行可能是空白或重复,取决于底层实现。PDF.js 把两者同时传给 WASM,让解码器自行取舍,这是「不替格式做决定」的典型做法。
设计要点:容错不是把所有异常都吞掉,而是只归一化自己能确定的输入,剩下的交给解码器。参数缺失、类型不对、字典为空,这些在 JS 层可以确定;码流是否合法、行数是否匹配,只有解码器知道。分工清楚,错误才不会在两层之间来回甩锅。
总结
CCITTFaxStream 是 PDF.js 的 CCITT 传真图像解码适配器:把 /DecodeParms 的七个键归一化成平面对象,异步取齐底层字节,再连同图像宽高一起交给共享的 JBig2/CCITT WASM 内核。它不实现 readBlock,解码路径也不依赖增量扩容,属于与 JpxStream 同层的整图解码器。
它在整条链路中的位置
| 阶段 | 模块 | 输入 | 输出 |
|---|---|---|---|
| 过滤器选择 | Parser.makeFilter | /Filter 中的 CCITTFaxDecode | CCITTFaxStream |
| 参数归一化 | CCITTFaxStream 构造 | /DecodeParms 字典 | 七个字段的平面对象 |
| 取字节 | bytes、asyncGetBytes | 上游流、maybeLength | 完整码流 |
| 解码 | decodeImage | 码流、W 或 Width、H 或 Height、参数 | 像素缓冲 |
| 底层内核 | JBig2CCITTFaxImage.instance.decode | 码流、宽高、CCITTOptions | imageData |
| 消费 | PDFImage | 像素缓冲 | ImageData 或位图 |
用一句话串起来:Parser 创建流,构造函数把 PDF 参数翻译成解码参数,decodeImage 取齐字节并等待 WASM,渲染层只拿到像素。
五条设计原则
- 参数在边界拍平。
Dict在构造函数里就被转成七个标量的普通对象,布尔值提前归一化,默认值集中在一处;解码路径不再处理 PDF 对象模型。 - 一个整数表达模式。
K的符号与大小同时编码 Group 3、Group 4 与二维行周期,不拆子类、不加枚举,解码模式的分派交给 WASM。 - 异步取字节就地解决。不覆盖
canAsyncDecodeImageFromBuffer,而是在decodeImage里先asyncGetBytes、失败再回退this.bytes;调用方只需要一个入口。 - 编码参数与画布尺寸分离。
Columns、Rows描述码流怎么排布,W、H描述输出缓冲多大;两组值都传给解码器,不互相替代。 - 共享内核、独立策略。CCITT 与 JBIG2 共用
jbig2.wasm与单例,只在参数分支上分开;谁取字节、何时预取由各自的流类决定。
与上一篇 jpx_stream 并排看,这两个类构成了图像解码层里最短的一对:一个把异步预取交给基类,一个把异步预取收进自己;一个的参数来自上层 PDFImage,一个的参数来自过滤器字典。它们都没有 readBlock,都用一次 decodeImage 完成整幅图,这正是图像编解码与通用过滤器的分水岭。
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 源码解读: 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 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日