StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: ccitt_stream

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: ccitt_stream

Introduction

The previous article, jpx_stream, covered whole-image decoding for JPEG 2000: the codestream is fetched in full, handed to OpenJPEG WASM, and pixels come out in one pass. This article stays in the image decoding layer, and its subject is CCITTFaxStream in src/core/ccitt_stream.js (79 lines).

The name CCITT comes from the Comité Consultatif International Téléphonique et Télégraphique. Its T.4 and T.6 recommendations define the image compression used by fax machines: T.4 is Group 3, T.6 is Group 4. Both handle only one-bit bi-level images — each pixel is either black or white, with no gray levels and no color. That is exactly the most common shape of faxes, scanned contracts and old receipts: a page of A4 at 300 dpi has roughly 8.7 million pixels; stored as 8-bit grayscale that would be over 8 MB, stored as one bit it is just over 1 MB, and after a run-length layer it is often only a few dozen KB.

PDF has supported this encoding since 1.0, with the filter name /CCITTFaxDecode and the abbreviation CCF. It often appears on image XObjects of scans, and often side by side with /JBIG2Decode — both compress bi-level images, one aimed at fax, the other at documents.

The entry point is again Parser.makeFilter:

        case "CCF":
        case "CCITTFaxDecode":
          return new CCITTFaxStream(stream, maybeLength, params);

Unlike JPX, params is passed in here. The encoding mode, row width, byte alignment and color polarity of CCITT are all described by /DecodeParms, and the decoder must have these parameters to interpret the codestream. This class therefore has two jobs: flatten the PDF dictionary into a plain parameter object the decoder can use directly, and hand the codestream to WASM for whole-image decoding.

In one sentence, its job is: normalize the seven fax-encoding parameters, fetch the underlying bytes asynchronously, and hand them together with the image width and height to the shared JBig2/CCITT WASM decoder.

Module Overview

ccitt_stream.js has only 79 lines; without the copyright header and imports the effective code is under sixty lines:

BlockKey MembersResponsibility
ConstructionconstructorStore the upstream stream and dictionary, normalize params into a plain object
BytesbytesLazily fetch the whole codestream and cache it with shadow
Capability declarationsisImageStream, isAsyncDecoderMark this as an image stream whose decoder is asynchronous
DecodingdecodeImageHandle asynchronous byte fetching itself and delegate decoding to WASM
Inherited facilitiesDecodeStreambuffer, bufferLength, eof, getImageData
Underlying kernelJBig2CCITTFaxImage.instanceThe CCITT decoding entry inside the shared JBIG2 module

It overrides neither readBlock nor ensureBuffer. The whole class works only through decodeImage, following the whole-image decoding pattern described in the previous article.

The data path can be written as:

Image dictionary /Filter /CCITTFaxDecode
  → Parser.makeFilter(stream, maybeLength, params)
  → CCITTFaxStream construction (seven parameters normalized)
  → DecodeStream.getImageData
  → decodeImage(null, length, options)
  → asynchronous byte fetch / synchronous fallback
  → JBig2CCITTFaxImage.instance.decode
  → pixel buffer
  → PDFImage

Note that the first argument passed to getImageData is null, not bytes. This detail determines how it divides the work with JpxStream, and is compared specifically later.

Construction: Flattening the Dict Shell into Scalar Parameters

The constructor Method

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"),
    };
  }

The constructor does four things.

First, it calls super(maybeLength). The base class turns the length hint into a power of two not smaller than it, as the initial capacity. For whole-image decoding this capacity is basically unused, because the result overwrites this.buffer directly; but keeping the same initialization as other streams does no harm.

Second, it stores the upstream stream, the length hint and the dictionary:

FieldSourcePurpose
this.streamFirst parameterProvides getBytes and asyncGetBytes
this.maybeLengthSecond parameterDecides how many bytes bytes requests from the upstream
this.dictstr.dictReads the image dimensions W, Width, H, Height

Third, it handles the case where params is not a dictionary:

if (!(params instanceof Dict)) {
  params = Dict.empty;
}

/DecodeParms may be missing, may be written as PDF null, or may be a corrupt entry parsed into a non-dictionary object. Dict.empty is a singleton cached through shadow, whose set is replaced with unreachable, so it is read-only:

  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);
  }

Using a singleton instead of new Dict() avoids allocating an empty object for every parameterless CCITT stream. Every get on the empty dictionary returns undefined, so all seven fields fall back to their defaults.

Fourth, it builds the plain object this.params. This is the most important action of the whole class, separating the world of PDF objects from the world of decoder options. The fields and defaults are:

FieldExpressionDefaultMeaning
Kparams.get("K") || 00Encoding group and two-dimensional row period; a negative value means Group 4
EndOfLine!!params.get("EndOfLine")falseWhether each row carries an EOL marker
EncodedByteAlign!!params.get("EncodedByteAlign")falseWhether each row is padded to a byte boundary
Columnsparams.get("Columns") || 17281728Pixel width of a coded row
Rowsparams.get("Rows") || 00Number of coded rows; 0 means no preset limit
EndOfBlock!!(params.get("EndOfBlock") ?? true)trueWhether the codestream ends with an EOB marker
BlackIs1!!params.get("BlackIs1")falseWhether a 1 bit is black; by default a 0 bit is black

Several of these expressions have details that are easy to misread.

EndOfBlock uses nullish coalescing:

      EndOfBlock: !!(params.get("EndOfBlock") ?? true),

?? takes the right operand only for null or undefined. That distinguishes three cases: missing gives true, PDF false gives false, PDF true gives true. If it were mistakenly written as || true, an explicit false would be swallowed and every stream would be treated as ending with an EOB marker, which on truncated files leads directly to reading out of bounds or stopping early.

Columns defaults to 1728, the standard page width of T.4: 8.5 inches at 200 dpi is about 1700 pixels, standardized to 1728. It is not a substitute for "image width" but the default for "coded row width"; the real output width is still determined by W or Width in the image dictionary.

Why Normalize at the Boundary

this.params is not a Dict and not a configuration class, but an object with a fixed shape and only seven scalar fields. That brings three direct benefits:

  1. The WASM boundary only understands scalars. The arguments of module._ccitt_decode are a sequence of numbers, and a dictionary object cannot cross. Flattening on the JS side first means the call site needs no repeated get and type checks.
  2. Booleans become 0/1 early. true, false, null and missing values in a PDF dictionary would each need another check at the call site; normalizing them to booleans in the constructor means the decoding path no longer cares about the PDF object model.
  3. Defaults are written once. The default semantics of the seven fields are concentrated in the constructor, so a reader can see at a glance what happens when nothing is written, instead of digging into WASM to guess.

Design note: The translation from "PDF object" to "decoder option" belongs in the constructor, not in decodeImage. The decoding path should be as short as possible, and parameter interpretation should happen as close to the boundary as possible; that way, even if the decoding backend is replaced later, only this one mapping has to change.

K: One Integer Expressing Three Fax Modes

K carries the most information among the CCITT parameters. The PDF specification uses it to express both "Group 3 or Group 4" and "how two-dimensional rows are interleaved":

Value of KEncoding modeSpecificationRow structureTypical source
NegativeGroup 4T.6Every row is two-dimensional, no EOL between rowsMost modern scans, fax archives
0Group 3 one-dimensionalT.4Every row is one-dimensional run-length coding, usually with EOL between rowsEarly faxes, simple bi-level images
PositiveGroup 3 two-dimensionalT.4One one-dimensional row inserted every K rows as a synchronization referenceMixed-mode fax streams

The technical difference between the three modes can be understood in terms of a reference line:

  • One-dimensional coding compresses only the black and white runs inside the current row, using the MH (Modified Huffman) tables. The decoder does not need the previous row, so it is self-contained, but the compression ratio is average.
  • Two-dimensional coding uses the relation between the current row and the previous row, with MMR (Modified Modified READ) codes. It splits the current row into segments and encodes each segment by its change position relative to the previous row. The compression ratio is clearly higher, but once a row is decoded incorrectly the error propagates forward.
  • Group 4 makes every row two-dimensional: the first row starts from a virtual all-white reference line, and every later row uses the previous one as reference. It therefore has the best compression ratio and depends most on codestream integrity. Fax machines and scanners use it by default most of the time.
  • Group 3 two-dimensional inserts a one-dimensional row every K rows, which amounts to periodic resynchronization. A larger K compresses better and resists errors worse; common values are 2 or 4.

These modes are not branched on in decodeImage. K is passed to WASM as an ordinary integer, and _ccitt_decode selects the corresponding code tables and state machine internally. PDF.js makes no mode decision in the JS layer and does not validate whether K and EndOfLine go together; when the parameters are wrong, the result is a decoding error or corrupted output rather than a friendly exception.

Design note: Polymorphism through a single parameter replaces three subclasses. The sign and magnitude of K already encode the mode information completely; splitting it into Group3OneDimStream, Group3TwoDimStream and Group4Stream would only increase the number of types without adding expressiveness.

EndOfBlock and the Other Switches

EndOfBlock answers the question: does the codestream have an explicit end marker?

Encoding modeEnd markerComposition
Group 3RTC (Return To Control)Six consecutive EOL
Group 4EOFB (End Of Facsimile Block)Two consecutive EOL

The PDF specification sets the default of EndOfBlock to true, meaning the decoder may expect an EOB marker. When it is false, the producer promises not to write an EOB, and the decoder must determine the end another way, for example by the number of rows given by Rows, or by running out of input data.

This switch is easy to get wrong: some producers write an EOB yet set EndOfBlock to false, while some truncated files lack an EOB yet keep the default true. PDF.js handles it by normalizing it into this.params, although this source does not pass it to WASM. The underlying call makes that clear:

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,
  );
}

The six business parameters are K, EndOfLine, EncodedByteAlign, BlackIs1, Columns and Rows, in that order, with no EndOfBlock. In other words, end detection is left to the low-level decoder and the input length; this.params.EndOfBlock is currently only stored, and the decoding path never reads it.

The other four switches each control one thing:

SwitchEffectTypical value
EndOfLineWhether each row carries the EOL bit patterntrue commonly for Group 3, false for Group 4
EncodedByteAlignWhether each row is padded to a byte boundarytrue for some Group 3 fax streams
ColumnsNumber of pixels in a coded rowDefault 1728; larger for wide scans
RowsNumber of coded rows0 means unlimited; equals the image height when the height is explicit

BlackIs1 determines the color polarity: the default false means a 0 bit is black and a 1 bit is white, consistent with the DeviceGray convention in PDF; setting it to true reverses that. It and the /Decode array in the PDF image dictionary are two independent flipping mechanisms — the former describes what a bit means at encoding time, the latter describes how values map to colors at render time. They act at different stages, and changing either one changes the final color; when debugging inverted black and white, it matters whether the encoding polarity is wrong or the rendering mapping is wrong.

Design note: The default of a switch must match the "semantics when omitted" of the specification. EndOfBlock uses ?? true rather than || true, and Columns uses || 1728 rather than ?? 1728 (a zero width is meaningless and should fall back to the default); the choice of each operator expresses a domain rule.

bytes and Asynchronous Byte Fetching

The bytes Method

  get bytes() {
    // If `this.maybeLength` is null, we'll get the entire stream.
    return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
  }

Exactly the same as JpxStream: on the first access it requests maybeLength bytes from the upstream, where null means read to the end of the stream; shadow caches the result as an instance property so later accesses do not fetch again.

The difference lies in how decodeImage uses it.

The decodeImage Method

  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;
  }

The first line is still the idempotence guard: when eof is true, the previous result is returned directly.

The second line is the important one:

bytes ??= this.stream.isAsync ? (await this.stream.asyncGetBytes()) || this.bytes : this.bytes;

It handles four cases:

CaseBehaviour
bytes already has a valueSkip the whole expression and decode directly
bytes is empty and the upstream is asynchronousawait this.stream.asyncGetBytes()
The asynchronous result is emptyFall back to this.bytes, the synchronous read
bytes is empty and the upstream is not asynchronousthis.bytes directly

Why does it await here itself instead of leaving it to the base class's getImageData like JpxStream? Because CCITTFaxStream does not override canAsyncDecodeImageFromBuffer. The base class getter returns false by default, so DecodeStream.getImageData takes this branch:

return this.isAsyncDecoder
  ? this.decodeImage(null, length, decoderOptions)
  : this.getBytes(length, decoderOptions);

isAsyncDecoder of CCITTFaxStream is true, so getImageData passes null as the first argument, and then decodeImage itself performs the "asynchronous first, synchronous fallback" byte fetching.

The two styles are not better or worse, only a division of labour:

DimensionJpxStreamCCITTFaxStream
canAsyncDecodeImageFromBufferOverridden to this.stream.isAsyncInherits the default false
Asynchronous prefetch locationBase class getImageDataThis class's decodeImage
First argument of decodeImageThe prefetched bytes, or nullUsually null
Synchronous fallbackbytes ||= this.bytes|| this.bytes inside the ternary
ResultBoth paths can decodeBoth paths can decode

The second style keeps byte fetching and decoding in one async method, so the caller only has to call decodeImage and does not need to understand the prefetch strategy of the base class. The cost is that the asynchronous branch of getImageData is never hit here, and that part of the base class is a dead path for this class.

Design note: canAsyncDecodeImageFromBuffer is the switch for "who prefetches", not the switch for "can it be asynchronous". JpxStream leaves it to the base class and CCITTFaxStream keeps it for itself; as long as decodeImage handles everything inside, either choice is valid.

Delegating to the Shared WASM Decoder

The second half of decodeImage passes five arguments to JBig2CCITTFaxImage.instance.decode:

ArgumentExpressionMeaning
bytesThe codestream fetched aboveComplete CCITT data
widthdict.get("W", "Width")Output image width, compatible with inline images and image XObjects
heightdict.get("H", "Height")Output image height, as above
globalsnullCCITT does not use JBIG2 global segments
CCITTOptionsthis.paramsThe seven normalized encoding parameters

The double-key form dict.get("W", "Width") comes from the two image forms in PDF: inline images (BI/ID/EI) use the abbreviated W and H, while image XObjects use the full names Width and Height. Dict.get checks the first key and then the second if it is missing, so one line covers both.

The fourth argument being fixed to null matters. The same WASM module supports both JBIG2 and CCITT through two entry points: with CCITTOptions it goes to _ccitt_decode, otherwise it goes to _jbig2_decode, and only the latter uses globals. CCITT has no concept of global segments, so null is passed explicitly to ensure the branch does not drift into JBIG2.

The singleton of JBig2CCITTFaxImage is cached through shadow, and the core branch of its decoding method is:

  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
        );
      }

When CCITTOptions is empty the source takes the _jbig2_decode branch and copies globals into the module; after the branch it checks module.imageData, detaches the result and empties it, and finally releases the codestream memory in finally.

Several facts are worth recording:

  • The shared WASM file is named jbig2.wasm. CCITT has no separate binary; it reuses the fax decoder inside the JBIG2 module. That is why the class name JBig2CCITTFaxImage puts the two together.
  • Every decode allocates its own memory. _malloc and writeArrayToMemory copy the codestream, and finally calls _free, so nothing leaks even when an error is thrown.
  • The output is detached from the module. module.imageData = null prevents the next decode from reading an old buffer; if the module produced no imageData, a Jbig2Error is thrown directly.
  • Width and height come from the image dictionary, not from DecodeParms. Columns and Rows only describe the encoding side; the final canvas size follows W and H.

This also explains a common confusion: Columns is a width too, so why does the decoder also need width? Because Columns describes how many pixels each coded row represents, while width describes how wide the output buffer is. They are equal in normal files, but may disagree when parameters are missing, the image dictionary has been modified, or the codestream is damaged, and the decoder needs to know both.

Comparison with the Other Image Decoders

Putting the four image streams side by side makes the differences obvious:

DimensionCCITTFaxStreamJbig2StreamJpxStreamJpegStream
Filter/CCITTFaxDecode/JBIG2Decode/JPXDecode/DCTDecode
Decoding backendThe CCITT branch of jbig2.wasmThe JBIG2 branch of jbig2.wasmopenjpeg.wasmjpg.js or ImageDecoder
Decoder parameter sourceSeven keys of /DecodeParmsJBIG2Globals of /DecodeParmsdecoderOptions built by PDFImage/ColorTransform
Width and height sourceW or Width, H or HeightWidth, HeightThe SIZ marker segment of the codestreamdrawWidth and drawHeight set by PDFImage
canAsyncDecodeImageFromBufferInherits the default falseOverridden to this.stream.isAsyncOverridden to this.stream.isAsyncOverridden to this.stream.isAsync
Asynchronous byte fetch locationInside decodeImageBase class getImageDataBase class getImageDataBase class getImageData
Implements readBlockNoNoNoYes, it calls the synchronous decodeImage internally
Typical contentFaxes, bi-level scansDocument scans, black and white textHigh-quality photos, maps, medical imagingOrdinary photos, thumbnails

The most instructive comparison is Jbig2Stream: it shares the same WASM class, the same jbig2.wasm and the same WASM decoding exit with CCITTFaxStream, yet makes the opposite choice in two places.

The first is parameters. Jbig2Stream needs JBIG2Globals from /DecodeParms, which is a stream object that must first be fetched as bytes and stripped of its file header before being passed in as globals; CCITTFaxStream flattens the entire dictionary into seven scalars and fixes globals to null.

The second is the asynchronous strategy. Jbig2Stream overrides canAsyncDecodeImageFromBuffer so the base class prefetches first; CCITTFaxStream does not override it and waits inside decodeImage itself. Both work, but the former leaves byte fetching to the base class while the latter pulls it into the subclass. A reader who only remembers that "image streams are all asynchronous" will miss this difference.

Design note: Sharing a kernel is not the same as sharing a strategy. JBig2CCITTFaxImage only turns bytes into pixels; who fetches the bytes, when, and whether to prefetch is decided by each stream class. Kernel and policy are separated so that the two formats can keep independent lifecycles on the same module.

Boundaries, Fault Tolerance and Common Mistakes

CCITT has many parameters and many defaults, so its edge cases are denser than those of JPX:

SituationBehaviour
Second call to decodeImageeof is already set, buffer is returned directly
/DecodeParms missing or not a dictionaryDict.empty is used, all seven fields take their defaults
K missingTreated as 0, that is Group 3 one-dimensional
EndOfBlock missingTreated as true
EndOfBlock explicitly falsefalse is kept, but the current version does not pass it to WASM
Columns missingTreated as 1728
Rows missingTreated as 0, no preset row limit
BlackIs1 missingTreated as false, a 0 bit is black
Asynchronous byte fetch returns emptyFall back to this.bytes, the synchronous read
Image dictionary lacks W, Widthwidth is undefined, the low-level layer cannot create an output buffer, usually throwing Jbig2Error
Codestream truncated or markers missingThe decoder may throw, eof stays false, the next call retries
Parameters do not match the codestreamNo validation; wrong pixels or a decoding exception follow directly

Three common mistakes deserve elaboration.

First, a bad K and EndOfLine combination. Group 4 codestreams have no EOL, so if EndOfLine is wrongly set to true, the decoder looks for 000000000001 at the start of every row, fails to find it, treats those bits as data, and the whole image is misaligned from the first row. Conversely, a Group 3 codestream that has EOL but sets EndOfLine to false makes the decoder treat the EOL pattern as ordinary runs, with the same garbage result. PDF.js performs no consistency validation in the JS layer; it hands the parameters to the decoder as they are and lets errors surface as image anomalies.

Second, BlackIs1 stacking with the /Decode array. BlackIs1 describes the encoding side: when false, a 0 bit means black. The /Decode array describes the rendering side: [1 0] reverses the gray levels. They act at different stages, and changing either one changes the final color; when debugging inverted black and white, it matters whether the encoding polarity is wrong or the rendering mapping is wrong. PDF.js computes needsDecode from /Decode inside PDFImage and handles it uniformly at the pixel stage, while BlackIs1 takes effect during WASM decoding.

Third, the relationship between Rows and the image height. Rows is a declaration on the encoding side; H and Height in the image dictionary are the facts on the output side. In normal files they are equal; when Rows is 0, the decoder can only rely on EOB or the end of input; when Rows is larger than the actual number of rows, the extra rows may be blank or repeated depending on the low-level implementation. PDF.js passes both to WASM and lets the decoder decide, which is a typical example of not making decisions on behalf of the format.

Design note: Fault tolerance does not mean swallowing every exception; it means normalizing only the input you can be sure about and leaving the rest to the decoder. Missing parameters, wrong types and an empty dictionary are determinable in the JS layer; whether the codestream is valid and whether the row count matches is known only to the decoder. With that division clear, errors are not passed back and forth between two layers.

Conclusion

CCITTFaxStream is the CCITT fax image decoding adapter of PDF.js: it normalizes the seven keys of /DecodeParms into a plain object, fetches the underlying bytes asynchronously, and hands them together with the image width and height to the shared JBig2/CCITT WASM kernel. It does not implement readBlock, the decoding path does not rely on incremental buffer growth, and it is a whole-image decoder on the same layer as JpxStream.

Where It Sits in the Chain

StageModuleInputOutput
Filter selectionParser.makeFilterCCITTFaxDecode in /FilterCCITTFaxStream
Parameter normalizationCCITTFaxStream constructor/DecodeParms dictionaryPlain object with seven fields
Byte fetchingbytes, asyncGetBytesUpstream stream, maybeLengthComplete codestream
DecodingdecodeImageCodestream, W or Width, H or Height, parametersPixel buffer
Underlying kernelJBig2CCITTFaxImage.instance.decodeCodestream, width and height, CCITTOptionsimageData
ConsumptionPDFImagePixel bufferImageData or a bitmap

In one sentence: Parser creates the stream, the constructor translates PDF parameters into decoding parameters, decodeImage fetches the bytes and waits for WASM, and the rendering layer only receives pixels.

Five Design Principles

  1. Parameters are flattened at the boundary. The Dict is turned into a plain object of seven scalars inside the constructor, booleans are normalized early, and defaults are collected in one place; the decoding path no longer touches the PDF object model.
  2. One integer expresses the mode. The sign and magnitude of K encode Group 3, Group 4 and the two-dimensional row period together, with no subclasses and no enum; mode dispatch is left to WASM.
  3. Asynchronous byte fetching is solved in place. canAsyncDecodeImageFromBuffer is not overridden; instead decodeImage calls asyncGetBytes first and falls back to this.bytes on failure, so the caller needs only one entry point.
  4. Encoding parameters and canvas size are separate. Columns and Rows describe how the codestream is laid out, while W and H describe how large the output buffer is; both are passed to the decoder and neither substitutes for the other.
  5. Shared kernel, independent policy. CCITT and JBIG2 share jbig2.wasm and the singleton and split only on the parameter branch; who fetches bytes and when to prefetch is decided by each stream class.

Seen next to the previous article on jpx_stream, these two classes form the shortest pair in the image decoding layer: one leaves asynchronous prefetching to the base class, the other pulls it inside itself; one takes its parameters from PDFImage above, the other from the filter dictionary. Neither has readBlock, and both complete the whole image in one decodeImage — that is exactly the watershed between image codecs and general filters.

StreamPDF design principle: anything that can be handled ahead of time is absolutely not deferred to parse time; the server solves all performance problems once. The browser only fetches and renders the data it must.

Related articles