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

Introduction
This is the second article in the "Image Decoding" layer, and its subject is Jbig2Stream in the worker-side src/core/jbig2_stream.js (104 lines).
The previous article, jpeg_stream, handled continuous-tone DCT codestreams; this one handles bi-level images. JBIG2 is defined by ITU-T T.88 and can be seen as the successor to fax coding (CCITT G3/G4): it targets black-and-white scanned pages and uses symbol dictionaries, arithmetic coding and region composition to squeeze 1-bit pixels into a very small volume. The text layer of a scanned PDF, fax archives and some ebooks often sit on top of a JBIG2 codestream.
Its PDF filter name is /JBIG2Decode, and the entry point again comes from Parser.makeFilter:
case "JBIG2Decode":
return new Jbig2Stream(stream, maybeLength, params);PDF.js does not implement JBIG2 from scratch in JavaScript. It compiles PDFium's decoder to WASM and provides a pure-JS fallback. That choice is pragmatic: JBIG2's arithmetic coding and symbol-dictionary composition are far more complex than JPEG's Huffman plus IDCT, and a pure-JS implementation would be both slow and hard to maintain. The price is that decoding becomes asynchronous, so the whole stream class has to be redesigned around Promises.
Compared with JPEG, three extra complications appear:
- JBIG2 has a standalone file header, while PDF embeds a headerless segment stream; some generators still write the header, and it must be stripped before decoding.
- An image's symbol dictionary may live in another stream, referenced through
/JBIG2Globalsin/DecodeParms. The main codestream and the global segment must be fetched separately, stripped separately, and handed to the decoder together. - The real decoder is an asynchronous WASM singleton, and the first call also has to load
jbig2.wasm(or the pure-JS fallback module). The synchronousgetBytespath does not work here.
In one sentence, its job is: fetch the main codestream and the global segments, strip any JBIG2 file header, hand the bytes, dictionary dimensions and global segments to the WASM decoder, and asynchronously obtain a 1-bit pixel buffer.
Module Overview
jbig2_stream.js is only 104 lines; the body is one class with five methods and two getters:
| Block | Key members | Responsibility |
|---|---|---|
| Construction | constructor | Stores the upstream stream, image dictionary, length and DecodeParms |
| Byte entry | bytes, ensureBuffer | Reads compressed bytes lazily and declares that no incremental buffer is used |
| Type markers | isAsyncDecoder, isImageStream | Declares asynchronous decoding and the image identity |
| Preprocessing | stripFileHeader | Strips the JBIG2 file header, shared by the main codestream and globals |
| Decoding | decodeImage | Fetches globals, calls the WASM decoder and writes the buffer asynchronously |
| Async capability | canAsyncDecodeImageFromBuffer | Delegates "can bytes be fetched asynchronously first" to the upstream stream |
It shares the same template with JpegStream for embedding a whole-image decoder into the stream interface: a lazy bytes, a no-op ensureBuffer, canAsyncDecodeImageFromBuffer delegated to upstream, and isImageStream always true. The difference is that the decoding function returns a Promise, so this class cannot use synchronous getBytes.
Three boolean markers answer three different questions and must be read separately:
| Marker | Question it answers | Implementation in this class |
|---|---|---|
canAsyncDecodeImageFromBuffer | Can the compressed bytes be fetched asynchronously first | Delegates to this.stream.isAsync |
isAsyncDecoder | Does the decoding function itself return a Promise | true |
isImageStream | Is this an image stream or a page content stream | true |
Together they produce the two branches of DecodeStream.getImageData: when upstream is asynchronous, asyncGetBytes first and then decode; when upstream is synchronous, isAsyncDecoder is true, so decodeImage is called directly and the Promise awaited. Synchronous getBytes is never selected by the image pipeline.
stripFileHeader is static because it depends on no instance state: the main codestream and the global segment are just two byte arrays with identical header rules. Being static also makes it independently testable and avoids constructing a temporary stream object for the global segment.
Inheritance and Construction
The class declaration corresponds almost line for line with JpegStream:
class Jbig2Stream extends DecodeStream {
constructor(stream, maybeLength, params) {
super(maybeLength);
this.stream = stream;
this.dict = stream.dict;
this.maybeLength = maybeLength;
this.params = params;
}The constructor Method
The four fields mean the same as in the previous article, but each is used differently:
streamis the upstream stream, possibly an in-memoryStreamor a network chunked stream. BothbytesandasyncGetBytestake data from it.dictis the image dictionary. JBIG2's/Widthand/Heightdo not come from the codestream but are read from here; more on that below.maybeLengthcomes from/Length;nullmeans read to upstream EOF. A JBIG2 segment stream has no EOI like JPEG, so reaching the end means the data is complete.paramsis/DecodeParms./JBIG2Globalshangs off it, and it is the input that distinguishes this class from JPEG.
super(maybeLength) likewise computes minBufferLength, but ensureBuffer is a no-op, so that growth logic never runs.
The Lazy bytes Getter
Compressed bytes are still read lazily through a getter and cached:
get bytes() {
// If `this.maybeLength` is null, we'll get the entire stream.
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
}shadow runs getBytes on first access and then replaces the getter with an own data property of the same name, so later reads never touch upstream. As with JPEG there is a synchronous side effect: if a chunked network stream has not finished downloading, getBytes throws MissingDataException. DecodeStream.getImageData calls asyncGetBytes first when canAsyncDecodeImageFromBuffer is true, precisely to bypass that synchronous read.
ensureBuffer, isAsyncDecoder and isImageStream
The next three members are short, yet they decide whether this class can be consumed at all:
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;
}The no-op ensureBuffer follows the image-stream convention: reserve no space for incremental writes, since the whole image is decoded once and then replaces buffer.
One detail is worth calling out: the comment says readBlock parses the whole image, but Jbig2Stream does not override readBlock. The base class implementation is unreachable("Abstract method ..."). In other words, this class is by design incompatible with synchronous getBytes; if any call chain really reaches readBlock, it throws immediately. What actually steers the image pipeline away from synchronous reads is isAsyncDecoder.
isAsyncDecoder defaults to false in BaseStream. It tells DecodeStream.getImageData: even if upstream bytes are ready, go through decodeImage and await it instead of getBytes. The reason is direct: WASM module loading and decode are both Promises, and there is no way to disguise them as a synchronous byte stream.
isImageStream separates this stream from page content streams. StreamsSequenceStream filters it out when concatenating content, and PDFPage.getContentStream will not parse pixels as operators. JBIG2 streams need this marker especially, because they do not even support synchronous reads; treating one as an ordinary stream throws immediately.
Design note: JPEG disguises the asynchronous world as a synchronous one through
readBlock; JBIG2 does not disguise anything and declaresisAsyncDecoderso the upper layer takes the asynchronous entry. The same template forks on one question: whether the decoder can be driven synchronously. Admitting a limitation is safer than inventing a fake synchronous interface: the former shows up at the type level, the latter blows up only at runtime.
stripFileHeader: Stripping the Standalone File Header
The stripFileHeader Method
JBIG2's standalone file format is not the same as the segment stream embedded in PDF. Annex D.4 of T.88 defines the file header, and PDF normally stores the "embedded" organisation without it:
// 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;
}The header layout can be written as a table:
| Offset | Length | Content |
|---|---|---|
| 0 to 7 | 8 | Magic bytes 0x97 0x4A 0x42 0x32 0x0D 0x0A 0x1A 0x0A |
| 8 | 1 | Flags, where the bit selected by mask 2 marks an unknown page count |
| 9 to 12 | 4 | Page count, present only when the mask 2 bit is 0 |
Three checks:
- At least 9 bytes. Magic plus flags is 9 bytes; a shorter input cannot carry a legal header and is returned unchanged.
- Byte-by-byte magic comparison. Eight constants spell the JBIG2 signature; any mismatch means a plain segment stream and no slicing.
TextDecoderor string comparison is avoided here to keep the byte layer free of encoding overhead. - Flags decide 9 or 13. If the bit selected by mask
2inbytes[8]is 0, the header records the page count and four more bytes follow, for 13 in total; if it is 1, the page count is unknown and the header is only 9 bytes. The source writes(bytes[8] & 2) === 0 ? 13 : 9, and the order of the bit test and the ternary is easy to read backwards.
The return value uses subarray, a view rather than a copy. This matches JPEG's #skipUselessBytes: stripping only moves the read start, it does not move data. Both the main codestream and the global segment call this static method, so the two implementations cannot diverge.
The header must be stripped because the WASM side's _jbig2_decode expects a segment stream: it parses segment headers, region information and symbol dictionaries itself and does not expect another header in front. Leaving the header in misaligns the first segment header and corrupts every segment after it.
Segment Stream Organization
Below the file header come consecutive segments. Each segment consists of a segment header and segment data; the header records the segment number, segment type, page association, referred-to segment list and data length. The type decides whether the body is page information, a symbol dictionary, an intermediate region or a text region; the page association says which page it belongs to; the referred-to list lets symbol dictionaries be reused across segments.
| Field | Role | Relationship to PDF |
|---|---|---|
| Segment number | Uniquely identifies a segment | Dictionaries in globals are referenced by segment number |
| Flags | Segment type, page association length, reference count | Determines how the rest of the segment header is parsed |
| Page association | Which page the segment belongs to | Multi-page codestreams are split by it |
| Data length | Byte count of the segment body | A wrong length makes the whole segment fail to parse |
stripFileHeader touches none of these fields; it only cuts the header off, and all segment-header parsing is left to WASM. The stream layer does not reimplement the specification — the boundary is clean: PDF compatibility issues are solved in JavaScript, JBIG2 syntax issues are solved in the decoder.
decodeImage: Three Inputs to WASM
The decodeImage Method
The real decoding entry is asynchronous:
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;
}Piece by piece:
- The
eofguard. As with JPEG, a finished decode returns the existing buffer. The difference is that this buffer comes from WASM and its type is not exactly a plainUint8Array, but the upper layers only interpret it by byte length and bit depth. - Strip the main codestream.
bytes || this.byteslets the asynchronous path pass bytes already fetched; otherwise the lazy getter runs. Whatever the source,stripFileHeaderis applied first. - Globals are optional.
/JBIG2Globalsexists only whenparamsis aDict; the value must be aBaseStreamor it counts as no globals. The global segment may itself carry a file header, so it is stripped once too. - Dictionary dimensions.
WidthandHeightare read fromthis.dict, not fromparams, and are not guessed from JBIG2 segment headers. The PDF image dictionary is the authoritative source for final size; region information in the codestream only describes how the image is composed. - WASM decoding.
JBig2CCITTFaxImage.instanceis ashadow-cached singleton, anddecodetakes the main codestream, width, height and globals and returns a Promise. - Commit the result.
bufferis replaced,bufferLengthtakes its length andeofis set. Bothlengthand_decoderOptionsare ignored: JBIG2 cannot decode half at a requested length, and it has no tunable decoding options.
Note that eof = true runs only after the await. It is not a concurrency lock: if two callers enter before decoding finishes, both see eof as false. In practice one getImageData drives one image, but the detail shows that "idempotent" only guarantees repeated reads do not decode twice, not that concurrent calls decode once.
JBIG2Globals and Global Segments
A JBIG2 codestream is made of segments: page information, symbol dictionaries, intermediate regions, text regions and so on. Global segments hold symbol dictionaries and generic regions reusable across pages. PDF splits them into two streams:
- the image stream itself holds only the segments of the current page;
/JBIG2Globalsin/DecodeParmspoints at another stream holding the shared segments.
The decoder must see both to map symbol numbers back to real bitmaps. The source fetches them directly:
if (this.params instanceof Dict) {
const globalsStream = this.params.get('JBIG2Globals');
if (globalsStream instanceof BaseStream) {
globals = Jbig2Stream.stripFileHeader(globalsStream.getBytes());
}
}Three constraints hide in four short lines:
- Only
BaseStreamis accepted./JBIG2Globalsmay be missing, may benull, or may be written as some other type in a damaged file; all these degrade to "no global segments" instead of throwing. - Synchronous fetch.
globalsStream.getBytes()takes no length and reads to that stream's EOF. The global segment is usually small and is fetched together with the image dictionary, so no secondasyncGetBytesis made here. - Globals are stripped too. Some generators store the global segment as a complete JBIG2 file, and that file header also does not belong to a segment stream. Main codestream and global segment share
stripFileHeader, keeping both inputs in the same organisation.
If the globals are missed, a JBIG2 image with symbol dictionaries decodes to a pile of blanks or fails outright: the decoder reads symbol numbers inside text regions but cannot find the corresponding bitmaps. The field is optional in the PDF specification, but it is very common in scanned documents.
The Authoritative Source of Width and Height
The second and third arguments to decode are this.dict.get("Width") and this.dict.get("Height"). This differs from JPEG: a JPEG SOF segment carries its own dimensions, which JpegImage can read from the codestream; a JBIG2 segment stream describes regions and composition, and the final bitmap size is specified by the PDF image dictionary.
Three situations follow:
| Dictionary dimensions | Codestream content | Result |
|---|---|---|
| Match the page information segment | Normal single page | Output at dictionary size, the most common case |
| Larger than the actual regions | Multiple regions or blank padding | The decoder lays out regions at the given size and leaves the remainder blank |
| Missing or not a positive integer | Anything | WASM receives an invalid size, decoding fails, and the image pipeline handles it |
The source performs no type validation here and passes the result of dict.get straight through. Validation belongs to Parser and PDFImage: Width and Height are required in an image dictionary, and when damage removes them the failure happens inside the decoder rather than in the stream layer.
The WASM Singleton and Output Format
JBig2CCITTFaxImage extends WasmImage and loads jbig2.wasm; when the environment lacks WASM it falls back to jbig2_nowasm_fallback.js. instance is a shadow-cached singleton, and the whole worker shares one module instance:
- the first call loads and initialises the module;
- each decode copies the main codestream and global segments into the WASM heap with
_malloc; _jbig2_decoderuns, and the C side performs arithmetic decoding, symbol composition and bitmap assembly;- the result is written to
module.imageDataand copied into a JavaScriptUint8ClampedArray; finallyreleases both heap blocks, preventing accumulation across documents.
Output is 1-bit packed pixel rows, not expanded RGBA. This aligns with PDF's /BitsPerComponent 1 and /ColorSpace /DeviceGray: the worker keeps the compact format and expands only when PDFImage decides how to draw.
WasmImage.cleanup can discard the module Promise during document cleanup, but the singleton object itself remains; the next decode reinitialises. This lifecycle is driven by clearGlobalCaches, called from entry points such as Catalog.cleanup, and is outside this file's responsibility.
How 1-bit Pixels Are Consumed
When PDFImage.createImageData receives this stream, because image instanceof DecodeStream it reuses buffer directly and does not expand to RGBA in the worker; when inversion is needed it only XORs the 1-bit buffer once. The bitmap kind is marked ImageKind.GRAYSCALE_1BPP, with one byte carrying eight pixels.
If the worker supports OffscreenCanvas, createBitmap expands to RGBA with convertToRGBA and then calls transferToImageBitmap; otherwise the 1-bit buffer and kind are sent to the main thread and expanded there. Both paths separate "decompression" from "colouring": WASM produces only the bit plane, and colour space and inversion wait until drawing. For a full-page scan, this saves the intermediate memory of a whole RGBA image.
Design note:
decodeImagecompletes all three of "format preprocessing", "optional inputs" and "authoritative dimensions" in the stream layer, and the WASM side receives only a clean segment stream, a width and height, and global segments. The narrower the boundary, the less the decoder needs to understand how PDF files break; conversely, PDF compatibility logic never seeps into C code.
Asynchronous Byte Fetching
The canAsyncDecodeImageFromBuffer Method
The last method is identical to JPEG:
get canAsyncDecodeImageFromBuffer() {
return this.stream.isAsync;
}It only answers "can upstream bytes be fetched asynchronously", not "is decoding asynchronous". Combined, DecodeStream.getImageData has two paths:
Upstream isAsync | isAsyncDecoder | Behaviour of getImageData |
|---|---|---|
true | true | await asyncGetBytes first, then await decodeImage |
false | true | await decodeImage(null, ...) directly |
false | false | Synchronous getBytes, looping over readBlock |
For this class the third row never occurs, because isAsyncDecoder is always true; forcing getBytes would land in the base class's unimplemented readBlock. The second row is the local-file case: bytes are ready, but decoding is still a Promise and must be awaited. The first row is the network case: fetch all compressed bytes first, so decoding does not run out of data halfway.
There is no MissingDataException retry logic here, because decodeImage is not a resumable parser. Once WASM starts reading heap memory there is no "pause and continue" interface; fetching all bytes is the only correct approach.
That is also why this class provides no readBlock: the synchronous interface requires a block decoder that returns immediately, while a WASM call must wait for module loading and Promise settlement. Rather than write a fake implementation that throws, let isAsyncDecoder guide callers to the right entry.
isImageStream and Its Consumers
The three-line isImageStream implementation matches JPEG, but it matters more for JBIG2:
get isImageStream() {
return true;
}Only two places in the repository consume it:
StreamsSequenceStreamfilters image streams when constructing, so pixels are never glued into a content stream;PDFPage.getContentStreamexcludes image streams, so JBIG2 bytes are never parsed as page operators.
A JBIG2 stream has no usable readBlock; read as an ordinary stream, it immediately throws "abstract method called". isImageStream blocks that misuse at the type-check stage instead of letting it explode at runtime.
Four Common Misreadings
- Treating
/JBIG2Globalsas part of the main codestream. It is a separate stream referenced by/DecodeParms, and must be fetched alone, stripped alone, and passed as the fourth argument to the decoder. - Assuming
stripFileHeaderonly handles the main codestream. The global segment may also carry a complete JBIG2 file header, and the source treats both inputs the same way. - Seeing a no-op
ensureBufferand assuming synchronousgetBytesworks.readBlockis not overridden, so a synchronous read lands on the base class'sunreachable; the correct entry isgetImageData, branched byisAsyncDecoder. - Treating
WidthandHeightas/DecodeParmsfields. They live in the image dictionary, the source reads them fromthis.dict, andparamsonly handles/JBIG2Globals.
Comparison with jpeg_stream
| Dimension | JpegStream | Jbig2Stream |
|---|---|---|
| Filter | /DCTDecode, /DCT | /JBIG2Decode |
| Decoder | Pure-JS JpegImage | WASM singleton JBig2CCITTFaxImage |
| Decoding mode | Synchronous | Asynchronous |
| Synchronous read | Available, readBlock triggers whole-image decoding | Unavailable, readBlock not overridden |
| File header | Only junk bytes before SOI are skipped | A 9- or 13-byte header is stripped according to the flags |
| Extra inputs | /ColorTransform | /JBIG2Globals global segments |
| Size source | drawWidth, drawHeight, scalable | Dictionary Width, Height, not scalable |
| Output | 1, 3 or 4 channel pixels | 1-bit packed pixels |
| Native acceleration | Optional ImageDecoder path | None, always WASM |
Read together, the two articles show that "lazy bytes + no-op ensureBuffer + delegated async capability + isImageStream" is the standard template for image streams; only the decoder interface and the format preprocessing really differ. Separating the template from the differences means that reading a new filter requires answering just three questions: where the bytes come from, whether the decoder is synchronous or asynchronous, and whether the codestream has a format header in front.
Conclusion
Jbig2Stream is the adapter of the bi-level image decoding layer: it turns PDF's two streams (main codestream and global segments) into one clean segment-stream input, strips file headers that do not belong to a segment stream, and then hands width, height and global segments to the asynchronous WASM decoder.
Where It Sits in the Overall Chain
| Stage | Module | Input | Output |
|---|---|---|---|
| Filter | Parser.makeFilter | /JBIG2Decode, stream dictionary | Jbig2Stream |
| Byte fetch | bytes, canAsyncDecodeImageFromBuffer | Upstream Stream or chunked stream | Complete main codestream bytes |
| Preprocessing | stripFileHeader | Main codestream, global segments | Headerless segment stream |
| Decoding | decodeImage → JBig2CCITTFaxImage.instance | Segment stream, Width, Height, globals | 1-bit pixel buffer |
| Image wrapping | PDFImage.createImageData | 1-bit buffer | ImageBitmap |
| Drawing | CanvasGraphics | ImageBitmap | Image on the canvas |
In one sentence: Parser wraps /JBIG2Decode in a Jbig2Stream, the stream layer strips headers and gathers globals, WASM turns the segment stream into a 1-bit bitmap, and PDFImage decides how to expand and draw it.
Five Design Principles
- Reuse the image-stream template and replace only the decoder. Lazy
bytes, a no-opensureBuffer,canAsyncDecodeImageFromBufferdelegated to upstream and an always-trueisImageStreamare identical to JPEG; a new format only needs its own preprocessing and decoding call. - Format headers are dealt with before the decoder.
stripFileHeaderserves both the main codestream and global segments, cuts exactly 9 or 13 bytes according to the flags and usessubarrayto avoid copying. The decoder therefore only needs to understand one input: the segment stream. - Optional inputs degrade explicitly. When
/JBIG2Globalsis missing or of the wrong type,globalsstaysnullrather than throwing. An image with symbol dictionaries then fails, but an image without them still decodes, keeping the tolerance window minimal. - Asynchronous capability must be declared explicitly.
isAsyncDecoderroutes the upper layer togetImageDatainstead ofgetBytes, avoiding the unimplementedreadBlock. Not faking a synchronous interface exposes the limitation at the type level rather than at runtime. - Intermediate results stay compact. WASM outputs 1-bit packed pixels and does not expand to RGBA early in the worker; the real expansion waits until
PDFImagechooses the drawing path. The larger the pixels, the more memory this principle saves.
This completes the "how JBIG2 becomes a bi-level bitmap" link. Two items remain in the image decoding layer — JPX (JPEG 2000) and CCITT fax: the former is also an asynchronous WASM decoder, the latter is a pure-JS bitstream decoder, but both follow the template in this article. After these three, reading how PDFImage interprets pixels according to ColorSpace, masks and BitsPerComponent closes the loop.
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: ccitt_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_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