StreamPDFStreamPDF
返回博客

PDF.js 源码解读: ccitt_stream

2026年10月27日 · StreamPDF Team

PDF.js 源码解读: ccitt_stream

前言

上一篇 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 解码
继承设施DecodeStreambuffer、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.dictstr.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 对象」与「解码器选项」两个世界隔开。字段与默认值如下:

字段表达式默认值含义
Kparams.get("K") || 00编码组别与二维行周期,负值表示 Group 4
EndOfLine!!params.get("EndOfLine")false每行是否带 EOL 标记
EncodedByteAlign!!params.get("EncodedByteAlign")false每行是否对齐到字节边界
Columnsparams.get("Columns") || 17281728编码行的像素宽度
Rowsparams.get("Rows") || 00编码行数,0 表示不预设上限
EndOfBlock!!(params.get("EndOfBlock") ?? true)true码流是否以 EOB 标记结束
BlackIs1!!params.get("BlackIs1")false1 位是否为黑;默认 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,也不是一个配置类,而是一个形状固定、只有七个标量字段的对象。这样做有三个直接好处:

  1. WASM 边界只认标量。module._ccitt_decode 的参数是一串数字,字典对象过不去。先在 JS 侧拍平,调用处就不需要反复 get 和类型判断。
  2. 布尔值提前变成 0/1。PDF 字典里的 true、false、null、缺失,在调用处还要再判断一次;在构造时统一成布尔,解码路径就不再关心 PDF 对象模型。
  3. 默认值只写一次。七个字段的默认语义集中在构造函数里,读代码的人一眼能看到「什么都没写时会怎样」,不用追到 WASM 里猜。

设计要点:把「PDF 对象」翻译成「解码器选项」的动作放在构造函数,而不是 decodeImage。解码路径越短越好,参数解释越靠边界越好;这样即使将来换一个解码后端,也只需要改这一处映射。

K 值:一个整数表达三种传真模式

K 是 CCITT 参数里信息量最大的一个。PDF 规范用它同时表达「Group 3 还是 Group 4」与「二维行如何穿插」:

K 的取值编码模式规范每行结构典型来源
负值Group 4T.6全部为二维编码,行间无 EOL大多数现代扫描件、传真存档
0Group 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 3RTC(Return To Control)连续六个 EOL
Group 4EOFB(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 数据
widthdict.get("W", "Width")输出图像宽度,兼容内联图像与图像 XObject
heightdict.get("H", "Height")输出图像高度,同上
globalsnullCCITT 不使用 JBIG2 的全局段
CCITTOptionsthis.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 描述「输出缓冲有多宽」。两者在正常文件里相等,但在参数缺失、图像字典被改动或码流损坏时可能不一致,解码器需要同时知道。

与其它图像解码器的对照

把四个图像流放在一起,差异一目了然:

维度CCITTFaxStreamJbig2StreamJpxStreamJpegStream
过滤器/CCITTFaxDecode/JBIG2Decode/JPXDecode/DCTDecode
解码后端jbig2.wasm 的 CCITT 分支jbig2.wasm 的 JBIG2 分支openjpeg.wasmjpg.js 或 ImageDecoder
解码器参数来源/DecodeParms 七个键/DecodeParms 的 JBIG2GlobalsPDFImage 生成的 decoderOptions/ColorTransform
宽高来源W 或 Width、H 或 HeightWidth、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 密集:

情况行为
第二次调用 decodeImageeof 已置位,直接返回 buffer
/DecodeParms 缺失或不是字典使用 Dict.empty,七个字段全取默认值
K 缺失按 0 处理,即 Group 3 一维
EndOfBlock 缺失按 true 处理
EndOfBlock 显式为 false保留 false,但当前版本不把它传给 WASM
Columns 缺失按 1728 处理
Rows 缺失按 0 处理,不预设行数上限
BlackIs1 缺失按 false 处理,0 位为黑
异步取字节返回空回退到 this.bytes 同步读取
图像字典缺少 W、Widthwidth 为 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 中的 CCITTFaxDecodeCCITTFaxStream
参数归一化CCITTFaxStream 构造/DecodeParms 字典七个字段的平面对象
取字节bytes、asyncGetBytes上游流、maybeLength完整码流
解码decodeImage码流、W 或 Width、H 或 Height、参数像素缓冲
底层内核JBig2CCITTFaxImage.instance.decode码流、宽高、CCITTOptionsimageData
消费PDFImage像素缓冲ImageData 或位图

用一句话串起来:Parser 创建流,构造函数把 PDF 参数翻译成解码参数,decodeImage 取齐字节并等待 WASM,渲染层只拿到像素。

五条设计原则

  1. 参数在边界拍平。Dict 在构造函数里就被转成七个标量的普通对象,布尔值提前归一化,默认值集中在一处;解码路径不再处理 PDF 对象模型。
  2. 一个整数表达模式。K 的符号与大小同时编码 Group 3、Group 4 与二维行周期,不拆子类、不加枚举,解码模式的分派交给 WASM。
  3. 异步取字节就地解决。不覆盖 canAsyncDecodeImageFromBuffer,而是在 decodeImage 里先 asyncGetBytes、失败再回退 this.bytes;调用方只需要一个入口。
  4. 编码参数与画布尺寸分离。Columns、Rows 描述码流怎么排布,W、H 描述输出缓冲多大;两组值都传给解码器,不互相替代。
  5. 共享内核、独立策略。CCITT 与 JBIG2 共用 jbig2.wasm 与单例,只在参数分支上分开;谁取字节、何时预取由各自的流类决定。

与上一篇 jpx_stream 并排看,这两个类构成了图像解码层里最短的一对:一个把异步预取交给基类,一个把异步预取收进自己;一个的参数来自上层 PDFImage,一个的参数来自过滤器字典。它们都没有 readBlock,都用一次 decodeImage 完成整幅图,这正是图像编解码与通用过滤器的分水岭。

StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.

相关文章