PDF.js Deep Dive from Demo 11: GetOperatorList and Where the 84 Operators Come From
October 27, 2026 · StreamPDF Team

Preface
Part 10 had the main thread execute all 84 operators from the demo first page one by one and paint the 297 x 421 text page.
But the operatorList did not appear out of nowhere. It is the product of GetOperatorList on the worker: the main thread sends a streaming request, the worker decodes the page content stream, translates PDF operators into drawing operators, and pushes them back in chunks.
This article returns to the worker and follows only the real path for the demo first page. Here is the data used throughout:
| Fact | Value for the demo first page |
|---|---|
| Page object | Page 1, pageIndex = 0 |
| MediaBox | [0 0 595.22 842] |
| CropBox | [0 0 595 842] |
/Contents | Single stream 11 0 R, /Filter /FlateDecode, /Length 542 |
| Decoded content stream | 1293 bytes, 83 operators |
| Resources | ExtGState /GS0 13 0 R, Font /T1_0 14 0 R, /T1_1 15 0 R |
| Annotations | No /Annots |
| operatorList | 84 operators, one chunk, lastChunk = true |
Two terms need to be kept apart:
- PDF operator: a token in the content stream, such as
BT,Tf, orTJ; - drawing operator: an
OPSentry in theoperatorList, such asbeginText,setFont, orshowText.
They are not one-to-one. The central question of this article is: why do 83 PDF operators turn into 84 drawing operators?
For the complete evaluator branches, paths, images, and color-space conversions, see the PartialEvaluator source walkthrough. This article explains only the parts reached by the demo first page.
The Request: What GetOperatorList Carries
Step 1: _pumpOperatorList Sends a Streaming Request
When render first runs, as described in Part 5, intentState.displayReadyCapability is still empty, so it calls _pumpOperatorList. The demo sends this payload:
{
pageId: 0, // physical page id after pagesMapper
pageIndex: 0, // logical page index
intent: 2, // RenderingIntentFlag.DISPLAY
cacheKey: "2__",
annotationStorage: {}, // serialized empty Map
modifiedIds: null,
}intent defaults to DISPLAY, which is 2. The two trailing underscores come from an empty annotation hash and an empty modifiedIds hash. This is exactly where the cacheKey in Part 10 comes from.
The request is not a plain call but a stream:
const readableStream = this._transport.messageHandler.sendWithStream(
"GetOperatorList",
{ pageId, pageIndex, intent, cacheKey, annotationStorage, modifiedIds },
undefined,
transfer
);
const reader = readableStream.getReader();The main thread then reads chunk by chunk with reader.read(). The streaming envelope and chunk appending were covered in Part 6; this article only cares about what the worker produces.
Step 2: The Worker Fetches the Page
The first thing the worker handler does is turn pageId into a core-layer Page:
handler.on("GetOperatorList", function (
{ pageId, pageIndex, intent, cacheKey, annotationStorage, modifiedIds },
sink
) {
pdfManager.getPage(pageId).then(function (page) {
const task = new WorkerTask(`GetOperatorList: page ${pageIndex}`);
startWorkerTask(task);
const start = verbosity >= VerbosityLevel.INFOS ? Date.now() : 0;
page.getOperatorList({ handler, sink, task, intent, cacheKey, annotationStorage, modifiedIds, pageIndex })
.then(opListInfo => {
if (start) {
info(`${task.name}; time=${Date.now() - start}ms, len=${opListInfo.length}`);
}
sink.close();
}, reason => sink.error(reason))
.finally(() => finishWorkerTask(task));
});
});The demo does not set verbosity, so it defaults to WARNINGS and the time=.., len=84 line is never printed. It only appears when verbosity >= INFOS.
pdfManager.getPage(0) uses the page cache covered in Part 8: the first call constructs the Page, and later calls reuse the same promise.
Step 3: PDFPage Prepares Content Stream, Resources, and Evaluator
Page.getOperatorList starts three things immediately:
const contentStreamPromise = this.getContentStream();
const resourcesPromise = this.loadResources(RESOURCES_KEYS_OPERATOR_LIST);
const partialEvaluator = this._createPartialEvaluator(handler, pageIndex);RESOURCES_KEYS_OPERATOR_LIST is:
["ColorSpace", "ExtGState", "Font", "Pattern", "Properties", "Shading", "XObject"]The demo page resources contain only ExtGState and Font: no XObject, no Pattern, and no color spaces. That means no paths, images, gradients, or transparency groups will appear later, leaving only text and marked content.
The PartialEvaluator is created once. It handles content-stream parsing, font translation, and resource loading from here on.
Step 4: StartRenderPage Arrives Before Any Operator
After the content stream and resources are ready, the code sends a status message before evaluation begins:
const pageListPromise = Promise.all([
contentStreamPromise,
resourcesPromise,
]).then(async ([contentStream]) => {
const resources = await this.#getMergedResources(
contentStream.dict,
RESOURCES_KEYS_OPERATOR_LIST
);
const opList = new OperatorList(intent, sink);
handler.send("StartRenderPage", {
transparency: partialEvaluator.hasBlendModes(resources, this.nonBlendModesSet),
pageIndex,
cacheKey,
});
await partialEvaluator.getOperatorList({
stream: contentStream,
task,
resources,
operatorList: opList,
});
return opList;
});The demo GS0 dictionary is:
<< /OP false /OPM 1 /SA false /SM 0.02 /Type /ExtGState /op false >>hasBlendModes checks only the BM entry of ExtGState dictionaries and resources nested inside XObjects. GS0 has no BM, and the page has no XObject, so transparency is false.
On the main thread, _startRenderPage only resolves displayReadyCapability:
_startRenderPage(transparency, cacheKey) {
const intentState = this._intentStates.get(cacheKey);
if (!intentState) {
return;
}
intentState.displayReadyCapability?.resolve(transparency);
}So StartRenderPage means "the page request phase is over and transparency is known", not "the operators have all arrived". initializeGraphics from Part 5 depends on this signal, while the actual operator chunk is still ahead.
Step 5: getContentStream Decodes 542 Bytes
getContentStream reads Contents from the page dictionary:
get content() {
return this.pageDict.getArray("Contents");
}The demo /Contents is a single 11 0 R, not an array, so getContentStream returns that Stream directly and no StreamsSequenceStream concatenation is needed.
The stream uses /Filter /FlateDecode and is 542 bytes compressed. It decodes to 1293 bytes, beginning with:
BT
/Artifact <</Type /Pagination >>BDC
/GS0 gs
/T1_0 1 Tf
0.0074 Tc 7.98 0 0 7.98 70.8 36.8003 Tm
(\251 )Tj
/T1_1 1 Tf
0.0009 Tc -0.0002 Tw 10.98 0 0 10.98 78.96 36.8003 Tm
[(Adobe Sys)5(t)1(ems Inc)5(orporated)5( 20)5(08 \226 All rights)5( reser)-9(ved)]TJ
...\251 is octal 251, which is © in WinAnsi; \226 is –. For decoding details, see the FlateStream source walkthrough.
EvaluatorPreprocessor then reads tokens with a Lexer and opMap:
this.parser = new Parser({
lexer: new Lexer(stream, EvaluatorPreprocessor.opMap),
xref,
});opMap maps names such as BT, Tf, Tm, and Tj to OPS values and argument counts, and validates each token. This stream yields 83 operators:
| Operator | Count |
|---|---|
BT | 1 |
ET | 1 |
BDC | 13 |
EMC | 13 |
gs | 1 |
Tf | 4 |
Tc | 12 |
Tw | 11 |
Tm | 6 |
Td | 5 |
Tj | 9 |
TJ | 7 |
| Total | 83 |
How 83 Operators Become 84 Operators
Step 6: The Conversion Table
The main loop in PartialEvaluator.getOperatorList reads one operator, updates state, and calls operatorList.addOp(fn, args). For the demo first page the result is:
| Operator | Produced operator | Count | Demo argument example |
|---|---|---|---|
BT | beginText | 1 | null |
ET | endText | 1 | null |
BDC | beginMarkedContentProps | 13 | ["Artifact", null], ["Span", 0] |
EMC | endMarkedContent | 13 | null |
gs | none | 0 | GS0 contains only unsupported keys |
Tf | setFont + dependency | 4 + 2 | ["g_d0_f1", 1] |
Tc | setCharSpacing | 12 | [0.0074] |
Tw | setWordSpacing | 11 | [-0.0002] |
Tm | setTextMatrix | 6 | Float32Array(7.98, 0, 0, 7.98, 70.8, 36.8003) |
Td | moveText | 5 | [1.825, -1.608] |
Tj | showText | 9 | two glyphs, © and a space |
TJ | showText | 7 | glyphs mixed with numeric spacing |
| Total | 84 |
The arithmetic takes only two steps:
83 operators
- 1 gs -> 0 drawing operators
= 82 direct operators
+ 2 dependencies -> injected by 4 Tf calls after deduplication
= 84 operatorsThe non-obvious parts are explained below.
Step 7: Why gs Produces No Operator
GS0 has the keys OP, OPM, SA, SM, Type, and op. The setGState switch only handles LW, LC, LJ, ML, D, RI, FL, CA, ca, Font, BM, SMask, and a few others; the remaining keys fall through:
case "HT":
case "SM":
case "SA":
case "AIS":
case "TK":
info("graphic state operator " + key);
break;
default:
info("Unknown graphic state operator " + key);
break;Type is ignored directly, and the other keys only produce info logs. As a result, gStateObj remains empty:
if (gStateObj.length > 0) {
operatorList.addOp(OPS.setGState, [gStateObj]);
}Nothing is emitted. This is not "gs never produces an operator": if GS0 carried LW or CA, it would emit one setGState. The demo GS0 happens to contain only keys that drawing does not use.
This is why 83 operators leave only 82 direct drawing operators.
Step 8: Four Tf Calls, Only Two Dependencies
The demo content stream has four Tf calls alternating between two fonts:
| Resource name | BaseFont | fontID | loadedName |
|---|---|---|---|
T1_0 | DDPEFM+Helvetica | f1 | g_d0_f1 |
T1_1 | DDPEIM+Helvetica-Bold | f2 | g_d0_f2 |
loadFont assigns a fontID to each font:
fontID = this.idFactory.createFontId(); // f1 / f2loadedName combines the document id and the fontID:
font.loadedName = `${this.idFactory.getDocId()}_${fontID}`;getDocId() returns g_${pdfManager.docId}. The demo docId is d0, producing g_d0_f1 and g_d0_f2.
The actual operator injection happens in the OPS.setFont branch:
case OPS.setFont:
next(
self.handleSetFont(resources, args, null, operatorList, task, stateManager.state, ...)
.then(function (loadedName) {
operatorList.addDependency(loadedName);
operatorList.addOp(OPS.setFont, [loadedName, fontSize]);
})
);
return;addDependency deduplicates with a Set:
addDependency(dependency) {
if (this.dependencies.has(dependency)) {
return;
}
...
}So the four Tf calls produce:
Tf in the content stream | Emitted operator | Index |
|---|---|---|
/T1_0 1 Tf | dependency("g_d0_f1") | 2 |
| same position | setFont("g_d0_f1", 1) | 3 |
/T1_1 1 Tf | dependency("g_d0_f2") | 7 |
| same position | setFont("g_d0_f2", 1) | 8 |
/T1_0 1 Tf | setFont("g_d0_f1", 1) | 13 |
/T1_1 1 Tf | setFont("g_d0_f2", 1) | 18 |
That is 4 setFont plus 2 dependency = 6 operators. A dependency is a resource placeholder for the main thread: "before executing index 2, g_d0_f1 must be registered". This is exactly where Part 10 pauses when the font is not ready.
After translation, TranslatedFont.send ships the font to the main thread:
handler.send("commonobj", [this.loadedName, "Font", fontData], transfers);send has a #sent flag, so each font is sent once. The worker therefore emits only two commonobj messages even though the content stream calls Tf four times.
The second setFont argument is the font size from the content stream, which is 1 for the demo. The 1.001 browserFontSize scaling in Part 10 happens later on the main thread, not here.
Step 9: Tj and TJ Both Become showText
The content stream has nine Tj and seven TJ calls: 16 text-drawing calls in total, all becoming 16 showText operators:
case OPS.showText:
args[0] = self.handleText(args[0], stateManager.state);
break;
case OPS.showSpacedText:
const combinedGlyphs = [];
for (const arrItem of args[0]) {
if (typeof arrItem === "string") {
combinedGlyphs.push(...self.handleText(arrItem, state));
} else if (typeof arrItem === "number") {
combinedGlyphs.push(arrItem);
}
}
args[0] = combinedGlyphs;
fn = OPS.showText;
break;The key point: a TJ array mixes strings and numbers. Numbers are not separate operators; they are placed into the same showText argument array. When the main thread executes showText and sees a number, it adjusts spacing by number / 1000 * fontSize.
The first two showText entries for the demo look like this:
// i = 6: (\251 )Tj
showText [
Glyph { unicode: "©", width: 737 },
Glyph { unicode: " ", width: 278 },
]
// i = 12: [(Adobe Sys)5(t)1(ems ... reser)-9(ved)]TJ
showText [
Glyph { unicode: "A", width: 722 },
Glyph { unicode: "d", width: 611 },
...
5, 1, 5, 5, 5, 5, 5, 5, -9,
...
]Glyph is a core-layer object that becomes a plain object after postMessage structured cloning. The main thread reads unicode, width, isSpace, and other fields without relying on the prototype.
This also explains why 83 PDF operators contain only 16 text-drawing operators while each may cover dozens of glyphs: one showText can draw a whole string, without generating one operator per character.
Step 10: The Six Tm Components Travel as Float32Array
Tm is explicitly converted to Float32Array in the evaluator:
case OPS.setTextMatrix:
operatorList.addOp(fn, [new Float32Array(args)]);
continue;The first matrix for the demo:
setTextMatrix [Float32Array(7.98, 0, 0, 7.98, 70.8, 36.8003)]Six Tm calls produce six Float32Arrays. Before sending a chunk, OperatorList.flush collects the transfer list:
case OPS.setTextMatrix:
transfers.push(argsArray[i][0].buffer);
break;The chunk therefore carries six ArrayBuffers transferred to the main thread without copying. The main thread receives Float32Array values and can use them directly in matrix math.
This is why Math.hypot(matrix[0], matrix[1]) in Part 10 works directly. If a plain array were sent, the main thread would receive a different type.
Step 11: BDC Keeps Only the Tag and MCID
The demo has 13 BDC calls: two Artifact and eleven Span:
/Artifact <</Type /Pagination >>BDC
/Span <</MCID 0 >>BDC
...
/Span <</MCID 10 >>BDCThe evaluator handles them like this:
case OPS.beginMarkedContentProps:
markedContentLevel++;
if (!(args[0] instanceof Name)) {
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", null]);
continue;
}
if (args[0].name === "OC") {
// Optional content group, handled by parseMarkedContentProps
...
return;
}
// Non-OC property dictionaries keep only MCID
args = [
args[0].name,
args[1] instanceof Dict ? args[1].get("MCID") : null,
];
break;So the Artifact property dictionary is reduced to ["Artifact", null], while Span keeps its MCID and becomes ["Span", 0] through ["Span", 10]. The demo has no OC, so the optional-content branch is never entered.
The 13 EMC calls produce matching endMarkedContent operators. The evaluator maintains markedContentLevel to keep the stack balanced; CanvasGraphics in Part 10 uses the same tags to maintain its visibility stack.
The page dictionary contains /StructParents 1, indicating a tagged PDF with a structure tree. The MCID is preserved here, but drawing only uses it as a marker; the structure-tree and text-layer linkage is a separate path.
Step 12: State Operators Are Copied One-to-One
The remaining operators are text state:
| Operator | Count | Drawing operator | Argument |
|---|---|---|---|
Tc | 12 | setCharSpacing | [0.0074], [0], [-0.0279] |
Tw | 11 | setWordSpacing | [-0.0002], [0.0281] |
Td | 5 | moveText | [1.825, -1.608], [-26.62, -1.188] |
Tm | 6 | setTextMatrix | Float32Array(6) |
They are copied one-to-one because the main thread must rebuild TextState in exactly the same order. showText itself carries no spacing, matrix, or scale information, only glyphs; the correct position comes from the state operators before it. Step 6 of Part 10 already showed how setCharSpacing and setTextMatrix update state, so this article does not repeat it.
One Chunk and Three Arrival Times
Step 13: 84 Is Far Below 1000, So One Flush
OperatorList uses two constants to control chunking:
static CHUNK_SIZE = 1000;
static CHUNK_SIZE_ABOUT = this.CHUNK_SIZE - 5; // 995Every addOp increments weight:
addOp(fn, args) {
this.optimizer.push(fn, args);
this.weight++;
if (this._streamSink) {
if (this.weight >= OperatorList.CHUNK_SIZE) {
this.flush();
} else if (
this.weight >= OperatorList.CHUNK_SIZE_ABOUT &&
(fn === OPS.restore || fn === OPS.endText)
) {
this.flush();
}
}
}The demo has only 84 operators, so neither condition is reached and no flush happens during evaluation. The page has no annotations, so getOperatorList takes this branch:
if (
annotations.length === 0 ||
intent & RenderingIntentFlag.ANNOTATIONS_DISABLE
) {
pageOpList.flush(/* lastChunk = */ true);
return { length: pageOpList.totalLength };
}The final chunk contains:
{
fnArray: [...84 OPS numbers],
argsArray: [...84 argument arrays],
lastChunk: true,
separateAnnots: null,
length: 84,
}Plus a transfer list of six ArrayBuffers. The main thread appends this chunk to its empty arrays and has the complete operator list immediately.
There is also an entry-point difference worth noting:
page.getOperatorList()adds theOPLISTflag and usesNullOptimizer;page.render()withintent = 2usesQueueOptimizer.
QueueOptimizer mainly merges path and image operators. The demo first page has neither, so both entry points emit the same 84 operators. A page with many small paths would not.
Step 14: The Race Between StartRenderPage, commonobj, and the Chunk
The worker sends four kinds of messages during one request:
| Order | Message | Demo content |
|---|---|---|
| 1 | StartRenderPage | { transparency: false, pageIndex: 0, cacheKey: "2__" } |
| 2 | commonobj | ["g_d0_f1", "Font", ...] |
| 3 | commonobj | ["g_d0_f2", "Font", ...] |
| 4 | operatorList chunk | 84 operators + 6 transfers |
| 5 | stream close | sink.close() |
Messages reach the main thread in postMessage FIFO order. But commonobj handling does not resolve synchronously:
messageHandler.on("commonobj", ([id, type, exportedData]) => {
const fontData = new FontInfo(exportedData);
const font = new FontFaceObject(fontData, ...);
this.fontLoader.bind(font)
.catch(() => messageHandler.sendWithPromise("FontFallback", { id }))
.finally(() => {
font.clearData();
this.commonObjs.resolve(id, font);
});
});commonObjs resolves only after fontLoader.bind finishes. OperatorList chunk handling, by contrast, only appends arrays and triggers drawing:
_renderPageChunk(operatorListChunk, intentState) {
for (let i = 0; i < operatorListChunk.length; i++) {
intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
intentState.operatorList.argsArray.push(operatorListChunk.argsArray[i]);
}
intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
intentState.operatorList.separateAnnots = operatorListChunk.separateAnnots;
for (const internalRenderTask of intentState.renderTasks) {
internalRenderTask.operatorListChanged();
}
...
}Two timings are therefore possible for the demo:
- The font finishes binding before the chunk is consumed: when
CanvasGraphicsreaches index 2,g_d0_f1is already incommonObjsand execution continues; - The chunk arrives first and the font is still binding:
CanvasGraphicspauses at index 2 and resumes at the same index aftercommonObjs.resolve.
Nothing in the code guarantees which one happens. The dependency pause in Part 10 is not an exceptional path; it is the normal concurrency result of this request.
In a real browser, fonts usually require registering a FontFace and waiting for loading, so the second timing is more common. The demo font is constructed from fontData directly and may also be ready very quickly. This is why PDF.js makes dependency pausable and resumable instead of blocking the whole operatorList up front.
Step 15: Worker Time Slices and Backpressure
The main loop in getOperatorList does not run to completion in one go:
while (!(stop = timeSlotManager.check())) {
task.ensureNotTerminated();
if (!preprocessor.read(operation)) {
break;
}
...
}
if (stop) {
next(deferred);
return;
}TimeSlotManager follows these rules:
static TIME_SLOT_DURATION_MS = 20;
static CHECK_TIME_EVERY = 100;It looks at the clock only once every 100 checks. The demo has only 83 operators, so the loop finishes after 84 iterations and never even reaches a clock check. This request runs to completion on the worker in one pass.
Large pages differ: every 100 operators it checks the clock, and when 20ms have elapsed it exits the current synchronous loop, re-enters promiseBody through next(deferred), and the preprocessor continues from its current position in the stream. Unclosed marked content and graphics states are only closed at the actual end of the stream, keeping the stacks balanced.
next also waits for operatorList.ready:
const next = function (promise) {
Promise.all([promise, operatorList.ready]).then(function () {
promiseBody(resolve, reject);
}, reject);
};operatorList.ready is tied to sink.ready, which indicates whether downstream can accept more. This is the streaming backpressure from Part 6: if the main thread consumes slowly, the worker does not pile up operators without bound.
When a request is cancelled, task.ensureNotTerminated() throws on the next loop entry, the current promiseBody stops, and sink.error sends the error back. The cancel path in Part 5 ends up here.
Summary
Starting from the main-thread GetOperatorList request, this article followed the demo first page 1293-byte content stream through operator production on the worker:
_pumpOperatorListsends a streaming request withpageId = 0,intent = 2, andcacheKey = "2__";- the worker obtains the
PageviapdfManager.getPage(0)and creates aWorkerTask; Page.getOperatorListprepares the content stream, resources, andPartialEvaluatorin parallel;StartRenderPageis sent before evaluation and carries onlytransparencyandcacheKey;/Contents 11 0 Ris FlateDecode-compressed from 542 bytes to 1293 bytes;EvaluatorPreprocessorreads 83 operators and validates their argument counts;- the
gsforGS0contains only unsupported keys, produces 0 operators, and turns 83 into 82 direct operators; - four
Tfcalls emit foursetFontand two deduplicateddependencyoperators, bringing the total to 84; - sixteen
TjandTJcalls all becomeshowText, withTJnumbers kept in the same argument array; TmusesFloat32Array, and the six matrix buffers enter the transfer list;BDCkeeps only["Artifact", null]and["Span", MCID], with 13BDC/EMCpairs keeping the stack balanced;- 84 is far below 1000, so there is one final
flush(true)with a single chunk andlastChunk = true; - the completion order of
StartRenderPage, the twocommonobjmessages, and the chunk is not fixed, so drawing pauses at adependencyif a font is not bound yet; - the worker checks its 20ms time slot every 100 operators, and the demo 83 operators finish in one pass;
operatorList.readyfeeds main-thread consumption speed back to the worker as backpressure.
GetOperatorList is therefore not a simple "parse the page" call, but an operator production pipeline with resource dependencies, pausing, backpressure, and chunking. The number of PDF operators and the number of drawing operators have no fixed relationship: some operators are dropped, some expand into several, and some inject extra resource placeholders during evaluation.
The operators and font dependencies are now in place. But g_d0_f1 in a dependency is only a placeholder: how does the embedded Type1C byte stream (FontFile3) become a browser-loadable FontFace? Part 12 unpacks the font translation pipeline: fingerprint deduplication, CFF repackaged into OpenType, binary FontInfo transfer, and FontFace mounting on the main thread.
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 Deep Dive from Demo 12: Font Translation, from FontFile3 to FontFaceOctober 28, 2026
- PDF.js Deep Dive from Demo 10: CanvasGraphics and operatorList RenderingOctober 27, 2026
- PDF.js Deep Dive from Demo 9: Network Streams and Range RequestsOctober 27, 2026
- PDF.js Deep Dive from Demo 8: PDFWorker.create and the Real/Fake Worker HandshakeOctober 27, 2026
- PDF.js Deep Dive from Demo 7: WorkerTransport InitializationOctober 27, 2026
- PDF.js Deep Dive from Demo 6: MessageHandler Cross-Thread CommunicationOctober 27, 2026
- PDF.js Deep Dive from Demo 5: renderOctober 27, 2026
- PDF.js Deep Dive from Demo 4: getViewportOctober 27, 2026
- PDF.js Deep Dive from Demo 3: getPageOctober 27, 2026
- PDF.js Deep Dive from Demo 2: getDocumentOctober 27, 2026
- PDF.js Source Walkthrough: TextLayerOctober 27, 2026
- PDF.js Source Walkthrough: font_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_displayOctober 27, 2026
- PDF.js Source Walkthrough: PDFObjectsOctober 27, 2026
- PDF.js Source Walkthrough: Network StreamsOctober 27, 2026
- PDF.js Source Walkthrough: internal_evt.jsOctober 27, 2026
- PDF.js Source Walkthrough: math_clamp.jsOctober 27, 2026
- PDF.js Source Walkthrough: css_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: scripting_utilsOctober 27, 2026
- PDF.js Source Walkthrough: MurmurHash3_64October 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_utilsOctober 27, 2026
- PDF.js Source Walkthrough: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: standard_fontsOctober 27, 2026
- PDF.js Source Walkthrough: fonts_utilsOctober 27, 2026
- PDF.js Source Walkthrough: default_appearanceOctober 27, 2026
- PDF.js Source Walkthrough: metadata_parserOctober 27, 2026
- PDF.js Source Walkthrough: name_number_treeOctober 27, 2026
- PDF.js Source Walkthrough: struct_treeOctober 27, 2026
- PDF.js Source Walkthrough: catalogOctober 27, 2026
- 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: 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 from Demo 1: Rendering a PDF with a Custom ViewerOctober 19, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026