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

Introduction
This installment of the PDF.js source walkthrough series covers main-thread object-readiness coordination in src/display/pdf_objects.js. In the current repository, this file is only 116 lines long and exports just one class, PDFObjects, yet it sits precisely at the boundary between worker parsing and canvas rendering.
The earlier api.js, MessageHandler, and network-stream articles addressed a different part of the journey: how PDF bytes enter the worker, how the operatorList streams back to the main thread, and how messages are dispatched. This article asks the next question: once fonts, images, and glyph paths decoded by the worker reach the main thread, how can the rendering loop read them synchronously when they are ready, yet pause and wait when they are not?
The answer is neither "make every get return a Promise" nor "hand a cross-thread Promise directly to canvas." PDFObjects keeps one stable entry per object on the main thread: the entry's data field holds its current value, while its local Promise only notifies waiters. Actual drawing still uses synchronous reads.
Its responsibility in one sentence: use a small three-state registry to unify object-first and waiter-first timing, while giving document-level and page-level rendering resources explicit readiness and cleanup boundaries.
Module Overview
The file has no explicit constructor, inheritance, or external dependency. Its entire mechanism consists of one sentinel, one entry factory, and one private Map.
| Part | Responsibility |
|---|---|
INITIAL_DATA | A private Symbol marking "registered, but not resolved yet" |
dataObj | Creates an entry containing data and the result of Promise.withResolvers() |
#objs | Stores entries by objId; an entry keeps its identity while waiters are attached |
get | Reads data synchronously without a callback; registers a readiness notification with one |
has | Probes resolved state without creating an entry |
resolve | Stores the final data and wakes local waiters; each entry resolves only once |
delete | Deletes resolved entries, rejecting absent and unresolved entries |
clear | Closes any ImageBitmap in the entries, then clears the entire registry |
Symbol.iterator | Iterates only resolved [objId, data] pairs, skipping pending entries |
"Objects" here means artifacts consumed by main-thread rendering, not the worker's XRef indirect-object cache. The current message paths store fonts, FontPath values, images, and patterns here; the operatorList remains separately managed by PDFPageProxy's rendering state.
The Entry Model: Separating Data from Readiness Notification
INITIAL_DATA and dataObj
const INITIAL_DATA = Symbol('INITIAL_DATA');
const dataObj = () => ({
...Promise.withResolvers(),
data: INITIAL_DATA,
});Promise.withResolvers() returns { promise, resolve, reject }. Spreading that result and adding a data field produces one registry entry. This factory only creates a local JavaScript object; it sends no message and starts no decoding work.
| Field | Role in PDFObjects |
|---|---|
promise | Lets one or more get(objId, callback) calls register .then handlers |
resolve | Fulfills promise after the data is stored, waking the callbacks |
reject | Supplied by withResolvers, but unused by the current file |
data | Starts as INITIAL_DATA, then holds the actual payload after resolution |
The key point is that data and the Promise do not carry the same information. The Promise's fulfillment value is not the rendering data; the data is always read from the entry's data field. Whether a callback is registered before or after resolution, it therefore reads the same entry object.
INITIAL_DATA is a module-private Symbol, not null, undefined, or an empty string. Consequently, "there is a value, and that value happens to be null" cannot be confused with "there is no value yet," and no ordinary payload can accidentally equal the sentinel.
Three States, Not Simply Present/Absent
| State | Entry in the Map? | data | Local Promise | get without a callback |
|---|---|---|---|---|
| Unregistered | No | Does not exist | Does not exist | Throws |
| Registered, unresolved | Yes | INITIAL_DATA | Pending | Throws |
| Resolved | Yes | Actual data, including null | Fulfilled | Returns data synchronously |
Unregistered and unresolved both mean "cannot read it now" to a synchronous caller, but they differ for an asynchronous caller: get with a callback turns unregistered into registered, while an unresolved entry may already have waiters. resolve can either move an unregistered id directly to resolved or complete an existing pending entry.
The class methods never inspect the Promise's internal state; they determine state solely from entry existence and data === INITIAL_DATA. The Promise is the notification mechanism, while the sentinel is the source of truth for the synchronous state machine.
get: Synchronous Reading and Callback Waiting Are Two Contracts
get(objId, callback = null) {
// If there is a callback, then the get can be async and the object is
// not required to be resolved right now.
if (callback) {
const obj = this.#objs.getOrInsertComputed(objId, dataObj);
obj.promise.then(() => callback(obj.data));
return null;
}
// If there isn't a callback, the user expects to get the resolved data
// directly.
const obj = this.#objs.get(objId);
// If there isn't an object yet or the object isn't resolved, then the
// data isn't ready yet!
if (!obj || obj.data === INITIAL_DATA) {
throw new Error(`Requesting object that isn't resolved yet ${objId}.`);
}
return obj.data;
}With a Callback: Register When Necessary, but Return No Promise
The current version directly uses Map.prototype.getOrInsertComputed: if the key exists, it returns the existing entry; otherwise it calls dataObj to create and insert an entry, then returns that new entry. This is not the hand-written "get, then set" expansion familiar from older code, and this file itself supplies no fallback for older Map implementations.
Whether the entry was pending or already resolved, the next line is always obj.promise.then(...). Multiple callbacks can therefore be registered for the same object, all sharing one readiness signal rather than each creating its own cross-thread request.
The callback wrapper is () => callback(obj.data) and does not use the .then argument. The reason appears in resolve: the entry Promise is fulfilled with no argument, so the callback reads the latest data from the closed-over obj when it runs.
Even if the object is already resolved, a Promise callback is only queued as a microtask; it does not run synchronously inside the get call stack. "Right away" in the source comment means that no new resolution event is needed, not that the callback is synchronous.
The callback branch always returns null, not a Promise and not the future data. A caller cannot use await objs.get(id, cb) to wait for readiness; it has registered a callback rather than received an awaitable handle.
Without a Callback: Throw When Not Ready
The no-callback branch uses ordinary Map.get and never silently creates an entry. Both an absent entry and data === INITIAL_DATA throw the same Requesting object that isn't resolved yet error; this branch never degrades into asynchronous waiting.
When resolved, it directly returns obj.data. Because state is determined by the sentinel, the null produced by resolve(id) is a valid synchronous return value and is not mistaken for pending.
This is exactly the attitude the rendering hot path needs: before an actual drawing operator runs, its object must be ready. Otherwise the dependency scheduler should pause ahead of time, rather than forcing every fill, drawImage, or font operation to become an asynchronous function.
has: A Side-Effect-Free Readiness Probe
has(objId) {
const obj = this.#objs.get(objId);
return !!obj && obj.data !== INITIAL_DATA;
}has uses the same state test as get without a callback, but turns "not ready, so throw" into false. Both unregistered and pending produce false; resolved produces true, even when data is null.
It neither registers the object nor attaches a callback. Canvas dependency operators use it first to decide whether execution can continue; the commonobj / obj message handlers also use it to skip duplicate objects that have already resolved.
Do not interpret has(id) === false as "the Map definitely has no id." It deliberately collapses unregistered and registered-but-unresolved into one consumer-facing meaning: the object cannot be read synchronously right now.
resolve: Store Data First, Then Signal Readiness
resolve(objId, data = null) {
const obj = this.#objs.getOrInsertComputed(objId, dataObj);
if (obj.data !== INITIAL_DATA) {
throw new Error(`Object already resolved ${objId}.`);
}
obj.data = data;
obj.resolve();
}getOrInsertComputed covers both entry paths: when the object arrives first, it creates an entry and resolves it immediately; when callbacks were registered first, it retrieves the original pending entry instead of replacing it. The Promise held by waiters is therefore always the one ultimately awakened.
The duplicate-resolution check happens before the write. Whenever data !== INITIAL_DATA, resolve throws Object already resolved; the second call's data cannot overwrite the first, and the entry Promise cannot be reused.
"Only once" here means once per registration lifetime, not that an id can never be used again. After delete or clear removes an entry, the same id can establish a new one; on the original entry, however, resolve is not a set operation for updating cached values.
The default parameter is an easy detail to miss in the current version: resolve(objId) writes null into data. Because pending is represented by INITIAL_DATA, that object is still resolved, has returns true, and get without a callback returns null.
The assignment order also cannot be reversed. obj.data = data completes synchronously before obj.resolve() fulfills the Promise, so a later microtask running callback(obj.data) is guaranteed to observe the final value.
obj.resolve() receives no argument, so the Promise's fulfillment value is undefined. Once again, it is not a data channel. reject likewise plays no role in the current implementation: whether a processing failure becomes some error value stored as data, or triggers fallback rendering, is decided by the higher-level message handler.
delete: Why Unresolved Entries Cannot Be Removed
delete(objId) {
const obj = this.#objs.get(objId);
if (!obj || obj.data === INITIAL_DATA) {
// Only allow removing the object *after* it's been resolved.
return false;
}
this.#objs.delete(objId);
return true;
}Returning false for an absent id is straightforward; returning false for pending protects already registered waiters. Imagine that pending entry A could be deleted: its callbacks would still be attached to A's Promise, but a later resolve would create entry B through getOrInsertComputed and awaken B instead, leaving A's callbacks with no continuation.
Rejecting deletion keeps the waiting lifetime intact: either the original entry resolves, or a higher-level whole-owner shutdown ends its meaning. The registry cannot leave waiters behind while silently replacing the registration on which they depend.
This method has a concrete use in the font-path flow. After FontFaceObject.getPathGenerator compiles raw drawing commands into a reusable path, it deletes the raw object that is no longer needed:
const objId = this.loadedName + '_path_' + character;
let cmds;
try {
cmds = objs.get(objId);
} catch (ex) {
warn(`getPathGenerator - ignoring character: "${ex}".`);
}
const path = makePathFromDrawOPS(cmds?.path);
if (!this.fontExtraProperties) {
// Remove the raw path-string, since we don't need it anymore.
objs.delete(objId);
}
return (this.compiledGlyphs[character] = path);The synchronous get ensures that, on the normal path to deletion, the object has already resolved; if it was not ready, the preceding exception path does not remove the pending entry. The compiled result enters compiledGlyphs, allowing the raw FontPath entry to leave the registry.
Notice that delete itself only removes a Map entry and never calls bitmap.close(). Bulk bitmap release is clear's responsibility; these are not interchangeable cleanup APIs.
clear: Release Bitmaps First, Then Remove Everything
clear() {
for (const { data } of this.#objs.values()) {
data?.bitmap?.close(); // Release any `ImageBitmap` data.
}
this.#objs.clear();
}The loop visits every entry and calls ImageBitmap.close() on resolved image data that has a bitmap, explicitly releasing its graphics resources. null or the pending INITIAL_DATA sentinel is safely skipped by optional chaining. Map.clear() then removes every registration, leaving other ordinary data to garbage collection.
This releases data.bitmap; it does not assume that every object has its own close method. Font bytes, compiled paths, and ordinary pixel arrays each have their own lifetime. PDFObjects.clear special-cases only this shared bitmap-resource convention.
clear is not a per-object cancellation protocol. It neither resolves nor rejects the Promise of a pending entry, and callbacks attached to an old entry are not notified merely because clear ran. Correctness depends on higher-level lifecycle code stopping consumers first, not on treating this small class as a general-purpose task canceller.
During page cleanup, PDFPageProxy.#tryCleanup first verifies that no renderTask is active and that the operatorList has reached its last chunk; it then clears _intentStates and calls objs.clear(). A late obj message subsequently sees _intentStates.size === 0 and directly closes the arriving imageData.bitmap rather than writing it back into the cleaned page cache.
During document destruction, WorkerTransport.destroy first marks the transport as destroyed so later object messages are ignored, waits for page tasks and the worker Terminate handshake to finish, and only then runs commonObjs.clear(). Explicit document cleanup() likewise clears document-level objects after every page has cleaned up successfully.
The simple implementation of clear and the strict cleanup ordering in api.js are therefore complementary: the former releases resources and removes entries, while the latter guarantees that nobody may still legitimately wait or continue writing at that point.
Symbol.iterator: Iterate Only Consumable Data
*[Symbol.iterator]() {
for (const [objId, { data }] of this.#objs) {
if (data !== INITIAL_DATA) {
yield [objId, data];
}
}
}This is a generator method, so a PDFObjects instance can be placed directly in for...of. It yields [objId, data], not the internal entry and not a Promise.
Pending entries are skipped individually; they neither block the whole iteration nor trigger a wait. A resolved null, however, is still yielded. Consumers therefore see only payloads that can be inspected synchronously.
The current CopyLocalImage branch of commonobj relies on this interface. When the worker wants to reuse an image already decoded for some main-thread page, the handler scans each page's objects:
for (const pageProxy of this.#pageCache.values()) {
for (const [, data] of pageProxy.objs) {
if (data?.ref !== imageRef) {
continue;
}
if (!data.dataLen) {
return null;
}
const copy = structuredClone(data);
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('TESTING')) {
copy.CopyLocalImage = true;
}
this.commonObjs.resolve(id, copy);
return data.dataLen;
}
}Only resolved page-level data participates in ref matching. On a match, the main thread uses structuredClone to copy the data, resolves the copy into document-level commonObjs, and returns dataLen as the answer to this commonobj request, avoiding another decode.
Two kinds of Promise are present here and must not be conflated: the CopyLocalImage request-response uses MessageHandler.sendWithPromise, whereas commonObjs.resolve awakens local main-thread object waiters. They serve different parties and directions.
Two Registry Levels: commonObjs and objs
PDFObjects itself does not choose a scope; scope comes from where api.js creates each instance. Every WorkerTransport creates one document-level registry:
this.commonObjs = new PDFObjects();Every PDFPageProxy shares the transport's instance while creating its own page-level registry:
this.commonObjs = transport.commonObjs;
this.objs = new PDFObjects();| Registry | Owner and sharing scope | Incoming message | Main current data | Cleanup boundary |
|---|---|---|---|---|
commonObjs | One per WorkerTransport, shared by all pages | commonobj | Fonts, glyph paths, global images, some patterns, copied local images | Document cleanup / destroy |
objs | One per PDFPageProxy | obj | Images and patterns for that page | Page cleanup / destroy |
PDFPageProxy.render passes both to InternalRenderTask, which passes them to CanvasGraphics. Canvas chooses a pool by id convention: a string beginning with g_ uses commonObjs; anything else uses the page-level objs.
getObject(opIdx, data, fallback = null) {
if (typeof data === "string") {
this.dependencyTracker?.recordNamedDependency(opIdx, data);
return data.startsWith("g_")
? this.commonObjs.get(data)
: this.objs.get(data);
}
return fallback;
}Actual image and pattern reads go through this synchronous getObject. Font handling in setFont is even more direct: it calls this.commonObjs.get(fontRefName); when built-in glyph paths are needed, paintChar passes the same commonObjs to font.getPathGenerator.
The worker also chooses a message according to resource scope: ordinary page images are sent through obj, while images inside Type3 fonts or selected for global caching are sent through commonobj. The two cache levels are therefore not a classification guessed by the main thread after arrival; they are a routing protocol shared by producer and consumer.
Message Arrival Does Not Mean Object Readiness
The current commonobj handler decides when and with what value to call resolve according to type:
| Type | Main-thread processing | Final registry |
|---|---|---|
Font | Constructs a FontFaceObject, then waits for font binding or the fallback chain | commonObjs |
FontPath | Wraps the payload in FontPathInfo | commonObjs |
Image | Uses the supplied exportedData | commonObjs |
Pattern | Converts through PatternInfo.getIR() | commonObjs |
CopyLocalImage | Scans page caches and clones a reusable image | commonObjs |
Image / Pattern in obj | Resolves directly after validating that the page is still active | The corresponding page's objs |
The font path best illustrates the distance between "data arrived" and "ready to render":
this.fontLoader
.bind(font)
.catch(() => messageHandler.sendWithPromise('FontFallback', { id }))
.finally(() => {
if (!font.fontExtraProperties) {
// Immediately release the `font.data` property once the font
// has been attached to the DOM, since it's no longer needed,
// rather than waiting for a `PDFDocumentProxy.cleanup` call.
// Since `font.data` could be very large, e.g. in some cases
// multiple megabytes, this will help reduce memory usage.
font.clearData();
}
this.commonObjs.resolve(id, font);
});bind(font) may await native FontFace.loaded, or it may use DOM font-loading detection. If binding fails, catch returns the FontFallback request Promise; whether binding succeeds directly or enters fallback, the chain ultimately resolves the object in finally.
PDFObjects therefore neither creates a Promise for font loading nor observes a Promise from the worker. Only after api.js performs the required wrapping, binding, and fallback on the main thread does it explicitly announce that "this id can now be consumed by synchronous rendering."
The font.clearData() inside finally is another resource-management layer: when extra font properties need not be retained, the font bytes are released immediately after attachment to the DOM rather than waiting for a wholesale commonObjs.clear().
In addition, the font-error branch of commonobj calls resolve(id, exportedError) rather than rejecting the entry Promise. This is consistent: PDFObjects standardizes the readiness semantics of "a final value now exists" and does not dictate how higher layers handle an error value.
An entry Promise never crosses a thread. The worker sends structured-cloneable or transferable data through send("commonobj", ...) or send("obj", ...); only after the main-thread message handler receives it does that handler call the local resolve. Cross-thread request-response Promises belong to MessageHandler, while entry Promises belong to main-thread PDFObjects. They must not be stitched together into an imaginary single Promise chain.
How the Rendering Loop Uses Both Forms of get
Dependency waiting is centralized in the OPS.dependency branch of CanvasGraphics.executeOperatorList, rather than scattered across every drawing operator:
for (const depObjId of fnArgs) {
this.dependencyTracker?.recordNamedData(depObjId, i);
const objsPool = depObjId.startsWith('g_') ? commonObjs : objs;
// If the promise isn't resolved yet, add the continueCallback
// to the promise and bail out.
if (!objsPool.has(depObjId)) {
objsPool.get(depObjId, continueCallback);
return i;
}
}When has is true, the loop moves past the dependency operator directly, and subsequent drawing operators read data through synchronous get without a callback. Only when has is false does it use callback-style get to register continueCallback, then return at the current index i.
Returning i rather than i + 1 matters. InternalRenderTask stores it as operatorListIdx; after the object becomes ready, the next pass checks the same dependency operator again, verifies that every dependency can be read synchronously, and only then executes the following drawing operations.
Here continueCallback is InternalRenderTask._continueBound. Although the entry callback passes obj.data, _continue() does not use that argument; it treats the notification solely as a signal to reschedule. The data is still fetched synchronously from the registry by subsequent operators.
Timeline 1: The Object Resolves Before Rendering Registers the Dependency
Consider an image already resolved into the page cache by an obj message:
| Step | Action | Result |
|---|---|---|
| 1 | The message handler runs pageProxy.objs.resolve(id, imageData) | The entry moves directly from unregistered to resolved |
| 2 | The rendering loop later reaches OPS.dependency, and has(id) is true | No waiter is registered and no pause occurs |
| 3 | The image operator calls getObject, entering get without a callback | imageData is returned in the same call stack |
This path imposes no waiting cost on the main-thread drawing hot path. Although the entry Promise is already fulfilled, the rendering loop need not touch it at all; has and synchronous get are sufficient.
If other code explicitly calls get(id, callback) at this point, getOrInsertComputed returns the resolved entry, and the callback is still queued as a microtask through .then on the fulfilled Promise; this does not affect the synchronous nature of no-callback reads.
Timeline 2: Rendering Registers a Waiter Before the Object Resolves
The worker adds an image dependency to the operatorList before decoding the image, so the following path is equally normal:
| Step | Action | Result |
|---|---|---|
| 1 | Canvas reaches the dependency, and has(id) is false | The object may be unregistered or already pending |
| 2 | get(id, continueCallback) reuses or creates the entry and registers .then | get returns null, and executeOperatorList returns the current i |
| 3 | The image message arrives later, and api.js calls resolve(id, imageData) | It stores data first, then fulfills the local Promise |
| 4 | A Promise microtask invokes _continueBound | The render task schedules another pass instead of drawing inside the message callback |
| 5 | The pass retries from index i, and has(id) is true | It passes the dependency, and the drawing operator reads the image synchronously |
A font whose "message arrived but is not ready" also follows the second path: commonobj arrival does not make has true. Waiting rendering continues only after the font binding/fallback chain reaches finally and calls resolve.
Both paths share the same implementation because getOrInsertComputed preserves entry identity, data stores synchronous state, and the Promise only provides notification. Which side arrives first changes only the order of state transitions, not the final reading contract.
Design Takeaways
1. A Sentinel Expresses Three States Better Than Truthiness
Using only the return value of Map.get(id) would make it impossible to distinguish unregistered, pending, null, and other falsy values. A private Symbol separates "unresolved" from every valid payload, which also makes the default null in resolve safe.
2. One Stable Entry Serves Both Producer and Waiters
Both get and resolve obtain the same entry through getOrInsertComputed. Registering a callback first does not block later data, and resolving data first does not deprive later callbacks of notification; no separate waiter array or message-replay cache is needed.
3. Asynchrony Lives Only at the Dependency Boundary, Leaving Drawing Synchronous
PDFObjects does not turn every canvas operator into an async function. Dependency operators handle suspension and resumption, while actual drawing uniformly reads synchronously, keeping the rendering state machine, error paths, and drawing order simple.
4. The Consumer Defines Readiness, Not Message Arrival
An image can resolve as soon as it arrives; a font must wait for main-thread binding or fallback to finish. PDFObjects supplies a neutral notification mechanism, while api.js folds each resource's distinct preparation work into the same id-readiness contract.
5. Single Resolution and Pending-Deletion Guards Protect Waiter Relationships
Forbidding duplicate resolution prevents the final value from being overwritten; forbidding deletion of pending entries prevents old waiters from losing their connection to a new entry. Together, these constraints close the notification relationship within one registration lifetime.
6. Cache Scope and Resource Release Must Be Designed Together
Document-level and page-level pools prevent cross-page resources from being cleaned too early while allowing page resources to be reclaimed independently. clear explicitly closes ImageBitmap values, the iterator exposes only resolved data, and api.js adds the late-message guard after cleanup, completing the resource-ownership chain.
Conclusion
PDFObjects is neither a general-purpose cache nor a cross-thread Promise wrapper. It is a three-state registry concerned only with rendering-object lifetimes: unregistered, registered but unresolved, and resolved. INITIAL_DATA drives synchronous decisions, while a local Promise supplies asynchronous notification.
Once these 116 lines are clear, two apparently contradictory rendering behaviors become consistent: CanvasGraphics can pause through a callback when a dependency is not ready, yet use synchronous get when actual drawing begins. commonObjs and objs determine how long data can be shared, the timing of resolve determines when it is truly usable, and delete / clear / the iterator guard waiter relationships and resource-release boundaries.
Between messages and pixels, PDF.js does not spread asynchrony throughout the renderer; it compresses asynchrony onto the single edge where "a dependency is not ready." That is the value of this small file in the display layer.
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 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: 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