PDF.js Source Walkthrough: ccitt_stream
October 27, 2026 · StreamPDF Team

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:
| Block | Key Members | Responsibility |
|---|---|---|
| Construction | constructor | Store the upstream stream and dictionary, normalize params into a plain object |
| Bytes | bytes | Lazily fetch the whole codestream and cache it with shadow |
| Capability declarations | isImageStream, isAsyncDecoder | Mark this as an image stream whose decoder is asynchronous |
| Decoding | decodeImage | Handle asynchronous byte fetching itself and delegate decoding to WASM |
| Inherited facilities | DecodeStream | buffer, bufferLength, eof, getImageData |
| Underlying kernel | JBig2CCITTFaxImage.instance | The 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
→ PDFImageNote 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:
| Field | Source | Purpose |
|---|---|---|
this.stream | First parameter | Provides getBytes and asyncGetBytes |
this.maybeLength | Second parameter | Decides how many bytes bytes requests from the upstream |
this.dict | str.dict | Reads 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:
| Field | Expression | Default | Meaning |
|---|---|---|---|
K | params.get("K") || 0 | 0 | Encoding group and two-dimensional row period; a negative value means Group 4 |
EndOfLine | !!params.get("EndOfLine") | false | Whether each row carries an EOL marker |
EncodedByteAlign | !!params.get("EncodedByteAlign") | false | Whether each row is padded to a byte boundary |
Columns | params.get("Columns") || 1728 | 1728 | Pixel width of a coded row |
Rows | params.get("Rows") || 0 | 0 | Number of coded rows; 0 means no preset limit |
EndOfBlock | !!(params.get("EndOfBlock") ?? true) | true | Whether the codestream ends with an EOB marker |
BlackIs1 | !!params.get("BlackIs1") | false | Whether 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:
- The WASM boundary only understands scalars. The arguments of
module._ccitt_decodeare a sequence of numbers, and a dictionary object cannot cross. Flattening on the JS side first means the call site needs no repeatedgetand type checks. - Booleans become 0/1 early.
true,false,nulland 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. - 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 K | Encoding mode | Specification | Row structure | Typical source |
|---|---|---|---|---|
| Negative | Group 4 | T.6 | Every row is two-dimensional, no EOL between rows | Most modern scans, fax archives |
| 0 | Group 3 one-dimensional | T.4 | Every row is one-dimensional run-length coding, usually with EOL between rows | Early faxes, simple bi-level images |
| Positive | Group 3 two-dimensional | T.4 | One one-dimensional row inserted every K rows as a synchronization reference | Mixed-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
Krows, which amounts to periodic resynchronization. A largerKcompresses 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
Kalready encode the mode information completely; splitting it intoGroup3OneDimStream,Group3TwoDimStreamandGroup4Streamwould 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 mode | End marker | Composition |
|---|---|---|
| Group 3 | RTC (Return To Control) | Six consecutive EOL |
| Group 4 | EOFB (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:
| Switch | Effect | Typical value |
|---|---|---|
EndOfLine | Whether each row carries the EOL bit pattern | true commonly for Group 3, false for Group 4 |
EncodedByteAlign | Whether each row is padded to a byte boundary | true for some Group 3 fax streams |
Columns | Number of pixels in a coded row | Default 1728; larger for wide scans |
Rows | Number of coded rows | 0 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.
EndOfBlockuses?? truerather than|| true, andColumnsuses|| 1728rather 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:
| Case | Behaviour |
|---|---|
bytes already has a value | Skip the whole expression and decode directly |
bytes is empty and the upstream is asynchronous | await this.stream.asyncGetBytes() |
| The asynchronous result is empty | Fall back to this.bytes, the synchronous read |
bytes is empty and the upstream is not asynchronous | this.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:
| Dimension | JpxStream | CCITTFaxStream |
|---|---|---|
canAsyncDecodeImageFromBuffer | Overridden to this.stream.isAsync | Inherits the default false |
| Asynchronous prefetch location | Base class getImageData | This class's decodeImage |
First argument of decodeImage | The prefetched bytes, or null | Usually null |
| Synchronous fallback | bytes ||= this.bytes | || this.bytes inside the ternary |
| Result | Both paths can decode | Both 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:
canAsyncDecodeImageFromBufferis the switch for "who prefetches", not the switch for "can it be asynchronous".JpxStreamleaves it to the base class andCCITTFaxStreamkeeps it for itself; as long asdecodeImagehandles everything inside, either choice is valid.
Delegating to the Shared WASM Decoder
The second half of decodeImage passes five arguments to JBig2CCITTFaxImage.instance.decode:
| Argument | Expression | Meaning |
|---|---|---|
bytes | The codestream fetched above | Complete CCITT data |
width | dict.get("W", "Width") | Output image width, compatible with inline images and image XObjects |
height | dict.get("H", "Height") | Output image height, as above |
globals | null | CCITT does not use JBIG2 global segments |
CCITTOptions | this.params | The 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 nameJBig2CCITTFaxImageputs the two together. - Every decode allocates its own memory.
_mallocandwriteArrayToMemorycopy the codestream, andfinallycalls_free, so nothing leaks even when an error is thrown. - The output is detached from the module.
module.imageData = nullprevents the next decode from reading an old buffer; if the module produced noimageData, aJbig2Erroris thrown directly. - Width and height come from the image dictionary, not from
DecodeParms.ColumnsandRowsonly describe the encoding side; the final canvas size followsWandH.
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:
| Dimension | CCITTFaxStream | Jbig2Stream | JpxStream | JpegStream |
|---|---|---|---|---|
| Filter | /CCITTFaxDecode | /JBIG2Decode | /JPXDecode | /DCTDecode |
| Decoding backend | The CCITT branch of jbig2.wasm | The JBIG2 branch of jbig2.wasm | openjpeg.wasm | jpg.js or ImageDecoder |
| Decoder parameter source | Seven keys of /DecodeParms | JBIG2Globals of /DecodeParms | decoderOptions built by PDFImage | /ColorTransform |
| Width and height source | W or Width, H or Height | Width, Height | The SIZ marker segment of the codestream | drawWidth and drawHeight set by PDFImage |
canAsyncDecodeImageFromBuffer | Inherits the default false | Overridden to this.stream.isAsync | Overridden to this.stream.isAsync | Overridden to this.stream.isAsync |
| Asynchronous byte fetch location | Inside decodeImage | Base class getImageData | Base class getImageData | Base class getImageData |
Implements readBlock | No | No | No | Yes, it calls the synchronous decodeImage internally |
| Typical content | Faxes, bi-level scans | Document scans, black and white text | High-quality photos, maps, medical imaging | Ordinary 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.
JBig2CCITTFaxImageonly 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:
| Situation | Behaviour |
|---|---|
Second call to decodeImage | eof is already set, buffer is returned directly |
/DecodeParms missing or not a dictionary | Dict.empty is used, all seven fields take their defaults |
K missing | Treated as 0, that is Group 3 one-dimensional |
EndOfBlock missing | Treated as true |
EndOfBlock explicitly false | false is kept, but the current version does not pass it to WASM |
Columns missing | Treated as 1728 |
Rows missing | Treated as 0, no preset row limit |
BlackIs1 missing | Treated as false, a 0 bit is black |
| Asynchronous byte fetch returns empty | Fall back to this.bytes, the synchronous read |
Image dictionary lacks W, Width | width is undefined, the low-level layer cannot create an output buffer, usually throwing Jbig2Error |
| Codestream truncated or markers missing | The decoder may throw, eof stays false, the next call retries |
| Parameters do not match the codestream | No 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
| Stage | Module | Input | Output |
|---|---|---|---|
| Filter selection | Parser.makeFilter | CCITTFaxDecode in /Filter | CCITTFaxStream |
| Parameter normalization | CCITTFaxStream constructor | /DecodeParms dictionary | Plain object with seven fields |
| Byte fetching | bytes, asyncGetBytes | Upstream stream, maybeLength | Complete codestream |
| Decoding | decodeImage | Codestream, W or Width, H or Height, parameters | Pixel buffer |
| Underlying kernel | JBig2CCITTFaxImage.instance.decode | Codestream, width and height, CCITTOptions | imageData |
| Consumption | PDFImage | Pixel buffer | ImageData 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
- Parameters are flattened at the boundary. The
Dictis 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. - One integer expresses the mode. The sign and magnitude of
Kencode Group 3, Group 4 and the two-dimensional row period together, with no subclasses and no enum; mode dispatch is left to WASM. - Asynchronous byte fetching is solved in place.
canAsyncDecodeImageFromBufferis not overridden; insteaddecodeImagecallsasyncGetBytesfirst and falls back tothis.byteson failure, so the caller needs only one entry point. - Encoding parameters and canvas size are separate.
ColumnsandRowsdescribe how the codestream is laid out, whileWandHdescribe how large the output buffer is; both are passed to the decoder and neither substitutes for the other. - Shared kernel, independent policy. CCITT and JBIG2 share
jbig2.wasmand 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
- PDF.js Source Walkthrough: pdf_managerOctober 27, 2026
- PDF.js Source Walkthrough: object_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_coreOctober 27, 2026
- PDF.js Source Walkthrough: cryptoOctober 27, 2026
- PDF.js Source Walkthrough: core_utilsOctober 27, 2026
- PDF.js Source Walkthrough: primitivesOctober 27, 2026
- PDF.js Source Walkthrough: worker_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_streamOctober 27, 2026
- PDF.js Source Walkthrough: jbig2_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpeg_streamOctober 27, 2026
- PDF.js Source Walkthrough: decrypt_streamOctober 27, 2026
- PDF.js Source Walkthrough: brotli_streamOctober 27, 2026
- PDF.js Source Walkthrough: run_length_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_hex_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_85_streamOctober 27, 2026
- PDF.js Source Walkthrough: lzw_streamOctober 27, 2026
- PDF.js Source Walkthrough: predictor_streamOctober 27, 2026
- PDF.js Source Walkthrough: flate_streamOctober 27, 2026
- PDF.js Source Walkthrough: decode_streamOctober 27, 2026
- PDF.js Source Walkthrough: chunked_streamOctober 27, 2026
- PDF.js Source Walkthrough: streamOctober 27, 2026
- PDF.js Source Walkthrough: base_streamOctober 27, 2026
- PDF.js Deep Dive 11: ParserOctober 26, 2026
- PDF.js Deep Dive 10: XRefOctober 26, 2026
- PDF.js Deep Dive 9: CanvasGraphicsOctober 26, 2026
- PDF.js Deep Dive 8: PDFDocumentOctober 26, 2026
- The AI Era: What Should We Do to Survive? (Reflections on Founding StreamPDF)October 26, 2026
- PDF.js Deep Dive 7: PartialEvaluatorOctober 25, 2026
- PDF.js Deep Dive 6: PDFManager.initDocumentOctober 25, 2026
- PDF.js Deep Dive 5: PDFFetchStream and PDFWorkerStreamOctober 25, 2026
- PDF.js Deep Dive 4: MessageHandlerOctober 22, 2026
- PDF.js Deep Dive 3: getViewport & renderOctober 22, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026