PDF.js Deep Dive 7: PartialEvaluator
October 25, 2026 · StreamPDF Team

Introduction
This is the seventh article in the PDF.js series and covers the source of the PartialEvaluator class.
It follows on from the GetOperatorList sendWithStream call in the _pumpOperatorList method from Article 3. The main point is how the operatorList is actually produced.
The PartialEvaluator class
From the class name we can already tell this is a partial evaluator. It only parses the operatorList of a PDF document; it does not render, hence Partial rather than Full.
The constructor method
Instantiated by page.getOperatorList
constructor({
xref,
handler,
pageIndex,
idFactory,
fontCache,
builtInCMapCache,
standardFontDataCache,
globalColorSpaceCache,
globalImageCache,
systemFontCache,
options = null,
}) {
this.xref = xref;
this.handler = handler;
this.pageIndex = pageIndex;
this.idFactory = idFactory;
this.fontCache = fontCache;
this.builtInCMapCache = builtInCMapCache;
this.standardFontDataCache = standardFontDataCache;
this.globalColorSpaceCache = globalColorSpaceCache;
this.globalImageCache = globalImageCache;
this.systemFontCache = systemFontCache;
this.options = options || DefaultPartialEvaluatorOptions;
this.type3FontRefs = null;
this._regionalImageCache = new RegionalImageCache();
this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this);
}The constructor as a whole follows a dependency injection plus shared cache design: every heavyweight resource shared across pages (fonts, CMaps, color spaces, images) is injected from the outside (Page/Catalog), and PartialEvaluator itself only holds temporary state for the current page.
// src/core/document.js
// 1) Page level -- parse a normal page content stream
_createPartialEvaluator(handler, pageIndex = this.pageIndex) {
return new PartialEvaluator({
xref: this.xref,
handler,
pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
}
// 2) Global level -- only used for XFA form font loading (loadXfaFonts)
// pageIndex: -1, with useSystemFonts turned offBelow we break down each parameter and instance field one by one.
Core dependencies (runtime capabilities required for evaluation)
| Parameter | Purpose |
|---|---|
xref | Cross-reference table instance. The entire parsing process fetches dictionaries/streams by object reference through xref.fetch(ref) (fonts, images, ExtGState, ColorSpace, etc.). |
handler | MessageHandler, the handle used for worker/main-thread communication. When useWorkerFetch is false, CMap and standard font data have to be pulled from the main thread via handler.sendWithPromise("FetchBinaryData", …). |
pageIndex | Current page index, used to identify object ownership. -1 means global context (XFA font loading only). Image/font IDs carry a page-number prefix. |
idFactory | ID factory that generates unique IDs for objects sent to the main thread (font FontFaceObject, images, etc.). Page level uses _localIdFactory (p${pageIndex}_${n}), global level uses _globalIdFactory. |
Cross-page shared caches (injected by Catalog, so each page is not parsed again)
These caches are shared at the whole-document level: the same font dictionary or the same image appearing on several pages is parsed/decoded only once:
| Parameter | Cached content |
|---|---|
fontCache | Translated TranslatedFont objects; key is the font dictionary reference. |
builtInCMapCache | Built-in CMap binary data (character code → CID mapping for CJK fonts); key is the CMap name. |
standardFontDataCache | Raw font data of the standard 14 fonts. |
globalColorSpaceCache | Parsed ColorSpace objects. |
globalImageCache | Decoded image data, shared across pages. |
systemFontCache | System font metrics (used when useSystemFonts is true). |
Evaluation options
options defaults to DefaultPartialEvaluatorOptions (defined at the top of evaluator.js). Key fields:
const DefaultPartialEvaluatorOptions = Object.freeze({
maxImageSize: -1,
disableFontFace: false,
ignoreErrors: false,
isOffscreenCanvasSupported: false,
isImageDecoderSupported: false,
canvasMaxAreaInBytes: -1,
fontExtraProperties: false,
useSystemFonts: true,
useWasm: true,
useWorkerFetch: true, // true: fetch directly inside the worker; false: fetch from the main thread
cMapUrl: null, // URL of the built-in CMap resources
cMapPacked: true, // whether to use compressed .bcmap files
iccUrl: null,
standardFontDataUrl: null, // URL of the standard font data resources
wasmUrl: null,
hasGPU: false,
});Most of these options come from getDocument on the main thread and are passed through to the worker (see GetDocRequest in Article 2). useWorkerFetch decides whether resources are fetched directly by the worker or delegated to the main thread, which drives the branches in fetchBuiltInCMap / fetchStandardFontData.
Temporary state on the instance itself
this.type3FontRefs = null;
this._regionalImageCache = new RegionalImageCache();
this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this);-
type3FontRefs— Type3 fonts describe glyphs with PDF drawing operators, so their glyph streams (CharProcs) are themselves PDF content streams that need aPartialEvaluatorto parse. This field plays two roles:① Flags whether a Type3 glyph parsing context is active.
nullmeans normal page evaluation; non-null(aRefSet) means Type3 glyph parsing is in progress.get parsingType3Fontis simply!!this.type3FontRefs, and the evaluator switches behavior at 8 places based on it:Behavior Normal page Type3 glyph Image/Pattern object ID prefix img_${id}${docId}_type3_img_${id}Image/Pattern send channel obj(isolated per page)commonobj(shared at document level)Whether ImageMask uses OffscreenCanvas Yes No (not supported inside Type3 glyphs) Whether ImageMask is cached Cached Not cached (Type3 glyphs are almost always inline images) Parse error Whether to ignore it depends on ignoreErrorsThrow immediately (a glyph can only be rendered as a whole; partial is meaningless) ② Prevents circular references. A Type3 font's glyph stream may reference fonts again via
Tf, and in extreme cases may reference the font itself. WhenTranslatedFont.loadType3Datacreates a child evaluator:// src/core/evaluator.js (TranslatedFont.loadType3Data) const type3Evaluator = evaluator.clone({ ignoreErrors: false }); const type3FontRefs = new RefSet(evaluator.type3FontRefs); // inherit the parent set if (dict.objId) { type3FontRefs.put(dict.objId); // add the current Type3 font itself to the set } type3Evaluator.type3FontRefs = type3FontRefs;This forms a reference chain: when parsing the glyphs of font A the set contains A; if A's glyphs reference font B, parsing B turns the set into
{A, B}; if B's glyphs reference back to A, the check insidehandleSetFontfires:// src/core/evaluator.js (handleSetFont) if (this.type3FontRefs?.has(fontRef)) { return errorFont(); // already in the parsing chain; return a placeholder error font and stop the recursion }RefSetdeduplicates by reference (object identity), anderrorFont()returns an empty font so the renderer skips that glyph, avoiding infinite recursion. -
_regionalImageCache— aRegionalImageCacheinstance, an image cache at the current Evaluator (i.e. current page) level. It complementsglobalImageCache(cross-page) and mainly holds regional entries such as tiled / cropped images that are only valid on the current page, so the global cache does not get polluted. -
_fetchBuiltInCMapBound— a pre-bound version offetchBuiltInCMap. Font parsing needs a callback that fetches data by CMap name; binding it in advance avoids creating a new closure on every call and guarantees thatthisinside the callback points at the current evaluator.
Design note:
PartialEvaluatoris not a singleton — a new instance is created for every page content stream parse — but every cache it holds is a shared reference injected from outside: lightweight instances, shared caches. This satisfies both goals at once: per-page evaluation state and cross-page reuse of already-parsed resources.
The get _pdfFunctionFactory method
get _pdfFunctionFactory() {
return shadow(
this,
"_pdfFunctionFactory",
new PDFFunctionFactory({ xref: this.xref })
);
}Lazy loading plus caching. PDFFunctionFactory parses the Function objects in a PDF (/Function dictionaries, types 0-4) for color space conversion (the tint transform of /CalRGB, /ICCBased), shading, Transfer functions, and so on.
It uses the shadow utility to get "create on first access, then read the property directly" — the new object is written onto the instance's own property, shadowing the getter. Since Function objects are cached by reference, one factory per evaluator instance is enough; there is no need to share it across pages.
The get parsingType3Font method
get parsingType3Font() {
return !!this.type3FontRefs;
}The semantics are simply "whether a Type3 glyph stream is currently being parsed". This was covered in detail in the constructor section: type3FontRefs is null and it returns false (normal page evaluation); it is non-null and it returns true (Type3 glyph evaluation). Across the evaluator there are 8 places that use it to switch the object ID prefix for images/Patterns, the send channel (obj vs commonobj), the error handling policy, and so on.
The clone method
clone(newOptions = null) {
const newEvaluator = Object.create(this);
newEvaluator.options = Object.assign(
Object.create(null),
this.options,
newOptions
);
return newEvaluator;
}Object.create(this) creates a new object whose prototype points at the current evaluator. That means the new evaluator shares all of the original instance's properties (xref, caches, pageIndex, etc.) but can independently override options.
The typical use is Type3 font parsing (TranslatedFont.loadType3Data):
const type3Evaluator = evaluator.clone({ ignoreErrors: false });Only the ignoreErrors option changes; everything else is inherited. The benefits:
- Lightweight — cache references are not copied; they are shared through the prototype chain.
- Isolated — the new evaluator can set its own instance fields such as
type3FontRefswithout affecting the parent. - Overridable options — merged with
Object.assign, withnewOptionstaking highest priority.
The hasBlendModes method
hasBlendModes(resources, nonBlendModesSet)Determines whether a set of Resources contains any blend mode other than Normal. This is a pre-check optimization: it is called before rendering a group (Transparency Group); if there is no blend mode, the main-thread CanvasGraphics can skip the cost of alpha compositing.
The algorithm is a graph traversal with cycle detection:
- Start from the passed-in
resourcesand run a BFS. - For each node, inspect
/BM(Blend Mode) of every graphics state in theExtGStatedictionary:/BMis a singleNameand is notNormal→ returntrue./BMis an array (the fallback mode list of a soft mask) → returntrueif any entry is notNormal.
- Recursively enter the
Resourcesof every Form/Image XObject in theXObjectdictionary and keep checking. - Use
processed(aRefSet) to record visited object references, preventing circular references and duplicate parsing. - If no blend mode is found by the end of the traversal, add every processed reference to
nonBlendModesSet— this set is shared at document level, so later pages that encounter the same objects skip them directly and avoid repeatedxref.fetch(the comment mentions issue6961.pdf, a broken file with a large number of XObjects, where this optimization noticeably cuts load time).
The fetchBuiltInCMap method
async fetchBuiltInCMap(name) {
const cachedData = this.builtInCMapCache.get(name);
if (cachedData) return cachedData;
let data;
if (this.options.useWorkerFetch) {
data = {
cMapData: await fetchBinaryData(`${this.options.cMapUrl}${name}.bcmap`),
isCompressed: true,
};
} else {
data = {
cMapData: await this.handler.sendWithPromise("FetchBinaryData", {
kind: "cMapUrl",
filename: `${name}${this.options.cMapPacked ? ".bcmap" : ""}`,
}),
isCompressed: this.options.cMapPacked,
};
}
this.builtInCMapCache.set(name, data);
return data;
}Fetches built-in CMap data by CMap name. Built-in CMaps are Adobe-defined standard character encoding mappings (such as GBK-EUC-H, UniGB-UCS2-H), used for character code → CID mapping in CJK fonts.
Two paths:
useWorkerFetch: true(the default): the worker thread callsfetchBinaryDatadirectly to fetch${cMapUrl}${name}.bcmap; only the compressed.bcmapformat is supported.useWorkerFetch: false: delegated to the main thread viaMessageHandler.sendWithPromise("FetchBinaryData", …), supporting.bcmap(compressed) or uncompressed format (depending oncMapPacked).
The result is written into builtInCMapCache (a document-level cache) to avoid repeated network requests. The MOZCENTRAL (Firefox built-in) build forces worker fetch.
The fetchStandardFontData method
async fetchStandardFontData(name) {
const cachedData = this.standardFontDataCache.get(name);
if (cachedData) return new Stream(cachedData);
// Symbol / ZapfDingbats differ across platforms, so the standard data must be loaded
if (this.options.useSystemFonts && name !== "Symbol" && name !== "ZapfDingbats") {
return null; // use a system font and skip the standard font data
}
const filename = getFontNameToFileMap()[name];
let data;
try {
if (this.options.useWorkerFetch) {
data = await fetchBinaryData(`${this.options.standardFontDataUrl}${filename}`);
} else {
data = await this.handler.sendWithPromise("FetchBinaryData", {
kind: "standardFontDataUrl",
filename,
});
}
} catch (ex) {
warn(ex);
return null;
}
this.standardFontDataCache.set(name, data);
return new Stream(data);
}Fetches the raw font data of the standard 14 fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats and their variants).
Key logic:
- When
useSystemFonts: true: prefer the font metrics shipped with the operating system and do not load standard data. But Symbol and ZapfDingbats are exceptions — these two fonts have inconsistent glyphs across platforms (macOS and Windows use different Symbol font encodings), so the standard data shipped with PDF.js must be loaded to guarantee consistent rendering. - Failure fallback: on a network error it calls
warnand returnsnull; font parsing then falls back to a fallback font. - Returns binary data wrapped in a
Stream, forStandardFontDataLoaderto parse font metrics.
The buildFormXObject method
async buildFormXObject(resources, xobj, smask, operatorList, task, initialState, localColorSpaceCache, seenRefs)Handles Form XObjects (XObjects with /Subtype /Form) — reusable standalone content streams in a PDF, essentially "a small page inside a page".
Core flow:
- Read the transformation matrix and bounding box:
Matrix(the transform from xobj to user space) andBBox(the clip box). - Optional content (OC): if the xobj has an
/OCdictionary (optional content group), parse it first and insert abeginMarkedContentPropsoperator, so visibility can be controlled at render time. - Determine whether it is a transparency group (Group):
- Has
/Groupwith/S /Transparency→ must be rendered on a separate offscreen canvas (CheckedOperatorList), handlingisolated,knockout, color space, the soft mask backdrop color (backdrop), and so on. - No Group → append directly to the current
operatorList, wrapped inpaintFormXObjectBegin/End.
- Has
- Recursively parse the content stream: call
getOperatorListto parse the xobj's stream, using the xobj's ownResources(inheriting the outerresourceswhen absent). - Assemble the operator sequence:
- With Group:
beginGroup → paintFormXObjectBegin → [child content] → paintFormXObjectEnd → endGroup. - Without Group:
paintFormXObjectBegin → [child content] → paintFormXObjectEnd.
- With Group:
seenRefs is used to detect XObject circular references (Form XObjects referencing each other).
The _sendImgData method
_sendImgData(objId, imgData, cacheGlobally = false)Sends image data to the main thread through MessageHandler. There are two send channels:
commonobj: used whenparsingType3Fontis true orcacheGloballyis true. The object ID is document-level (with adocIdprefix), and the main thread stores it in the global_objscache, shared across pages.obj: normal page images, carrying apageIndex; the main thread manages them per page and releases them when the page is destroyed.
transfers hands the ArrayBuffer (image pixel data) to the main thread as a Transferable (zero-copy), avoiding the structured clone overhead of large arrays.
The buildPaintImageXObject method
async buildPaintImageXObject({ resources, image, isInline, operatorList, cacheKey, localImageCache, localColorSpaceCache })Handles Image XObjects and inline images — one of the most complex methods in the evaluator. It splits into three main branches:
① ImageMask
- A mask image has only 1 bit of depth and must be tinted with the current fill color, so it cannot be fully processed in the worker.
- Special optimization: if it is a "single opaque pixel" mask (LaTeX documents often use it to draw colored rules), emit
paintSolidColorImageMaskdirectly without creating an image object. - Masks inside Type3 glyphs: try
compileType3Glyphto compile them into path operators (constructPath); on failure fall back to an inline mask. - Normal masks: send
mask_${id}image data and emitpaintImageMaskXObject.
② Small inline images are inlined directly
When isInline && w + h < 200 && !hasMask, decode to RGBA directly in the worker and inline it into the operatorList via paintInlineImageXObject, bypassing object transfer.
③ Normal images
- Generate an
objId; Type3 glyphs get a_type3_prefix, and images eligible for global caching get adocId_prefix. - Emit the
paintImageXObject [objId, w, h]operator. - Global cache optimization: for large images (greater than 500×500) or images with a mask, first try to copy from the main thread's local cache (
CopyLocalImage) to avoid re-decoding. - Async decoding:
PDFImage.buildImage → createImageData; on success send the image data, on failure recorddecodeFailedand send null (skipped at render time).
The #createTransferMap method
#createTransferMap(fn) {
const transferFn = this._pdfFunctionFactory.create(fn);
const tmp = new Float32Array(1);
return Uint8Array.from({ length: 256 }, (_, i) => {
tmp[0] = i / 255;
transferFn(tmp, 0, tmp, 0);
return (tmp[0] * 255) | 0;
});
}Pre-computes a PDF Transfer Function (/Function type 2/4, used to calibrate device response) into a 256-entry lookup table (LUT). The input is normalized to [0,1], and the function output is quantized back to [0,255].
That way, when handleTransferFunction applies a transfer function it only needs an O(1) table lookup, instead of re-running the function computation for every pixel.
The handleSMask method
handleSMask(smask, resources, operatorList, task, stateManager, localColorSpaceCache, seenRefs)Handles Soft Mask — the /SMask dictionary defines a transparency-based mask whose child object /G is a Form XObject (usually a grayscale image), and whose pixel values determine the opacity of the masked object.
Core logic:
- Read
/S(subtype:AlphaorLuminosity) and/BC(backdrop color). - Read
/TR(transfer function); if it is a PDF Function, call#createTransferMapto pre-compute a 256-entry LUT. - Reuse
buildFormXObjectto parse the soft mask's content stream, passingsmaskOptionsas thesmaskparameter — later, in the Group branch ofbuildFormXObject,groupOptions.hasSoftMaskis set to true, and the main thread enables offscreen compositing at render time.
stateManager.state.clone({ newPath: true }) passes in a state copy with the path cleared, so drawing operators inside the soft mask do not affect the outer path.
The handleTransferFunction method
handleTransferFunction(tr)Handles /TR (Transfer Function) in ExtGState, used to calibrate the device's color response (for example, to compensate for a printer's non-linearity).
/TR can be:
- A single Function: applied to all color components.
- An array of 4 Functions: corresponding to R/G/B or C/M/Y/K respectively.
/Identity: identity mapping, no transformation.
Processing logic:
- If all functions in the array share the same reference, keep only one (optimization).
- For each entry:
/Identity→null(skip); PDF Function →#createTransferMapgenerates a LUT. - Validation: only 1 or 4 functions are supported (as required by the spec).
- If everything is
/Identity, returnnull(no-op).
The returned transferMaps array ([mapR, mapG, mapB] or [mapAll]) is sent to the main thread along with the ExtGState, and CanvasGraphics looks it up when applying colors.
The handleTilingType method
handleTilingType(fn, color, resources, pattern, patternDict, operatorList, task, localTilingPatternCache, seenRefs)Handles Tiling Patterns — a content stream repeated across an area to fill it. pattern is the pattern's content stream, and patternDict is the pattern dictionary (defining XStep/YStep tiling spacing, BBox, PaintType, etc.).
Core flow:
- Merge Resources:
Dict.merge([patternDict.Resources, resources]), so the pattern dictionary does not miss some resource entries (the fix for issue6541.pdf). - Parse the pattern content stream: call
getOperatorListon aCheckedOperatorListto get the pattern's operator list. - Generate the IR:
getTilingPatternIRturns the operator list into an intermediate representation containing the tiling parameters and color information. - Send the operator:
operatorList.addOp(fn, tilingPatternIR), wherefnissetFillPatternorsetStrokePattern. - Cache: store the IR in
localTilingPatternCache(keyed by the pattern object reference), so the same pattern is reused.
CheckedOperatorList detects whether isolation is needed inside the pattern (needsIsolation), which affects the main thread's rendering strategy.
The async handleSetFont method
async handleSetFont(resources, fontArgs, fontRef, operatorList, task, state, fallbackFontDict, cssFontInfo, seenRefs)Corresponds to the PDF Tf (set font) operator. It does three things:
- Load the font: call
loadFontto get aTranslatedFont(a translated font object containing metrics, glyph mapping, font data, etc.). - Set the current font:
state.font = translated.font; subsequent text operators use this font. - Send the font to the main thread:
translated.send(this.handler)sends the font data (FontFaceObject) to the main thread throughMessageHandlerto register the@font-face.
Returns loadedName, the CSS font name the main thread can use.
For Type3 fonts, it additionally adds type3Dependencies (the image/Pattern dependencies in the glyph streams) to the parent operatorList's dependency list, ensuring they are parsed before synchronous execution.
The handleText method
handleText(chars, state)Corresponds to text-showing operators such as Tj/TJ/'/". It converts character codes into glyph indexes:
const glyphs = font.charsToGlyphs(chars);Key branch — when glyph outlines have to become paths:
When any of the following conditions holds, call buildFontPaths to turn glyph outlines into PDF path operators instead of rendering with the font:
textRenderingModeincludesADD_TO_PATH_FLAG(render modes 2/6/7: fill + stroke + add to path).- The fill or stroke color space is
Pattern(pattern colors cannot be achieved by filling with a font). font.disableFontFaceis true (the font cannot be rendered through@font-face, as with some Type1 fonts).
Returns a glyphs array, which the later showText operator carries to the main thread for rendering.
The ensureStateFont method
ensureStateFont(state) {
if (state.font) return;
const reason = new FormatError("Missing setFont (Tf) operator before text rendering operator.");
if (this.options.ignoreErrors) {
warn(`ensureStateFont: "${reason}".`);
return;
}
throw reason;
}A defensive check: before running a text-showing operator (Tj etc.), make sure a font has already been set via Tf. Otherwise the PDF is invalid per the spec. In ignoreErrors mode it only warns; otherwise it throws a FormatError.
The async setGState method
async setGState({ resources, gState, operatorList, cacheKey, task, stateManager, localGStateCache, localColorSpaceCache, seenRefs })Corresponds to the gs operator — sets extended graphics state from an ExtGState dictionary. It walks every key-value pair, converts each one, pushes it into the gStateObj array, and finally emits a setGState operator.
Supported state parameters:
| Key | Meaning | Handling |
|---|---|---|
LW | Line width | Take the absolute value |
LC/LJ/ML/D | Line cap/line join/miter limit/dash | Pass the value through |
RI | Rendering intent | Pass the value through |
FL/CA/ca | Flatness tolerance/stroke alpha/fill alpha | Pass the value through |
Font | Font | Call handleSetFont asynchronously, isSimpleGState = false |
BM | Blend mode | Normalized with normalizeBlendMode |
SMask | Soft mask | Call handleSMask, isSimpleGState = false |
TR/TR2 | Transfer function | handleTransferFunction generates the LUT array |
isSimpleGState marks whether the GState holds only simple parameters — only simple ones are cached in localGStateCache (those containing a font or soft mask have side effects and are not cached).
Unimplemented parameters (OP/BG/HT etc.) only produce an info log and do not affect rendering.
The loadFont method
loadFont(fontName, font, resources, task, fallbackFontDict, cssFontInfo, seenRefs)Loads a font by name or reference; the entry point of the font pipeline. Core flow:
- Resolve the reference: use
fontdirectly when it is aRef; otherwise look up the reference by name inresources.Font. - Cycle detection: if
fontRefis already intype3FontRefs(a Type3 recursive reference), returnerrorFont. - Cache hit: if it is already in
fontCache, return it directly. - Fetch the font dictionary:
xref.fetchIfRef(fontRef)resolves the dictionary. - Fallback: if it is not a valid dictionary, in
ignoreErrorsmode usefallbackFontDictorfallbackFontDict(standard Helvetica) as a safety net. - Pre-evaluation:
preEvaluateFont(font)extracts the font's basic information (Subtype, BaseFont, encoding, descriptor, etc.). - Font ID and alias:
- If the font has a
FontDescriptorand ahashcan be computed, usefontAliases[hash]for alias deduplication — several font dictionaries that reference the same font data share onefontID. - Otherwise
idFactory.createFontId()generates a new ID.
- If the font has a
- Write to cache:
fontCache.put(fontRef, promise); what is cached is the Promise (translation is in progress). - Async translation:
translateFont(preEvaluatedFont)producestranslatedFont; for Type3 fonts,loadType3Datathen parses the glyph streams. - resolve: resolve the
TranslatedFontobject for the caller.
The loadedName format is ${docId}_${fontID}, the font name used when the main thread registers the @font-face.
The buildPath method
buildPath(operatorList, args)Converts the PDF path construction operators (m/l/c/v/y/h/re) into path operations in the operatorList. args is the operator's argument array, and buildPath maps them into the internal path instruction format.
This is the single entry point for path construction: all path operators ultimately emit OPS.constructPath through it, and the main-thread CanvasGraphics uses that to call the canvas path APIs.
The _getColorSpace method
_getColorSpace(cs, resources, localColorSpaceCache)Looks up a color space object in the ColorSpace dictionary of Resources. It returns the raw color space object (which may be a Name, an Array, an ICCBased stream, etc.) without parsing it.
If the color space is already in localColorSpaceCache, the cached value is returned directly. This is the preparatory step of _handleColorSpace — get the raw object first, parse second.
The async _handleColorSpace method
async _handleColorSpace(cs, resources, localColorSpaceCache, cacheKey)Parses the raw color space object into a ColorSpace instance and sends it to the main thread through MessageHandler.
Supported color space types:
- Device spaces:
DeviceGray,DeviceRGB,DeviceCMYK— use the built-inColorSpacedirectly. - CIE spaces:
CalGray,CalRGB,Lab— require Function computation. - ICCBased: parse the ICC profile stream (may require external ICC data from
iccUrl). - Special spaces:
Pattern,Indexed,Separation,DeviceN. - Named spaces: looked up in the
resources.ColorSpacedictionary.
Results are cached in both localColorSpaceCache and globalColorSpaceCache, avoiding expensive repeated work such as parsing ICC profiles.
The parseShading method
parseShading({ shading, resources, localColorSpaceCache, localShadingPatternCache })Parses a Shading Pattern (gradient pattern), i.e. the PDF /Shading dictionary (types 1-7: function-based shading, axial shading, radial shading, mesh shading, etc.).
Core flow:
- Cache lookup:
localShadingPatternCachelooks up the ID by shading object reference; on a hit it returns immediately. - Parse:
Pattern.parseShadingturns the shading dictionary into aShadingPatternobject, internally handling the color space, coordinate transform, functions, etc. - Generate the IR:
shadingFill.getIR()produces the intermediate representation. - Send to the main thread:
- Inside Type3 glyphs: use
commonobj, prefix the ID with_type3_, and compile the IR into anArrayBufferfor transfer. - Normal pages: use
obj, withpageIndex.
- Inside Type3 glyphs: use
- Cache: store it in
localShadingPatternCache.
On failure, with ignoreErrors set, it caches null and returns null (the gradient is skipped).
The handleColorN method
handleColorN(operatorList, fn, args, cs, patterns, resources, task, localColorSpaceCache, localTilingPatternCache, localShadingPatternCache, seenRefs)Corresponds to the SCN/scn (set color, with Pattern support) operators. The last argument of args is the pattern name; the preceding ones are the color components.
Logic:
- Pop the pattern name:
args.pop(). - Tiling Pattern cache hit: if the pattern is already in
localTilingPatternCache, use the cached IR directly to emit thesetFillPattern/setStrokePatternoperator. - Otherwise parse the pattern:
PatternType.TILING(1) → callhandleTilingType.PatternType.SHADING(2) → callparseShadingto get the gradient ID and emit the pattern operator.
- Color components: if the pattern's underlying color space is
DeviceRGBand the like,cs.base.getRgbHex(args, 0)converts the color components into a hex color value and passes it to the pattern IR.
The async parseMarkedContentProps method
async parseMarkedContentProps(contentProperties, resources) {
return parseMarkedContentProps(this.xref, contentProperties, resources);
}Parses the property list of Marked Content, i.e. the property dictionary carried by the BDC/BMC/DP operators. These properties can be:
- Optional content groups (OCG):
/OCreferences, controlling content visibility. - Actual text (ActualText):
/ActualText, used for accessibility/text extraction when the visual text differs from the logical text (ligatures, for example). - Structure elements:
/StructParent/StructParents, linking to the document's logical structure tree.
It delegates to the standalone parseMarkedContentProps function, because the logic is fairly complex and independent of evaluator state.
The async getOperatorList method
async getOperatorList({ stream, task, resources, operatorList, initialState, fallbackFontDict, prevRefs })This is the core method of PartialEvaluator — it parses a PDF content stream (stream) into an operator list (operatorList). Page content streams, Form XObjects, Type3 glyphs, and Tiling Patterns are all parsed through it.
Preparation
- Async stream handling: if
stream.isAsync(the data has not fully arrived), callasyncGetBytes()first to get the full data, then wrap it in a synchronousStream. - Circular reference detection:
seenRefs = new RefSet(prevRefs)inherits the parent's reference set; if the current stream'sobjIdis already in the set, throw (XObjects referencing each other). - Initialize local caches: every call creates fresh
LocalImageCache,LocalColorSpaceCache,LocalGStateCache,LocalTilingPatternCache— caches within this parse, discarded when it ends. - State and preprocessor:
StateManager(the graphics state stack),EvaluatorPreprocessor(the lexer that turns the byte stream into{fn, args}operations). - Time slice management:
TimeSlotManagercontrols the duration of a single execution, keeping the worker from blocking for a long time.
Main loop
while (!timeSlotManager.check()) {
preprocessor.read(operation); // read one operator
switch (operation.fn) { ... } // dispatch and handle
}
Handling branches for the key operators:
| Operator | Handling |
|---|---|
paintXObject (Do) | Look up the cache → Form XObject goes through buildFormXObject (async), Image goes through buildPaintImageXObject (async) |
setFont (Tf) | Call handleSetFont asynchronously, emit setFont [loadedName, size] |
showText family (Tj/TJ/'/") | ensureStateFont → handleText converts characters to glyphs, then emits directly |
setFillColorSpace (cs) | _getColorSpace + _handleColorSpace (async parsing of ICC and the like) |
setFillColor family (sc/scn/g/rg/k) | Color space conversion → uniformly emit setFillRGBColor [hex] |
setGState (gs) | Call setGState asynchronously |
| Path operators (m/l/c/re/…) | buildPath emits constructPath |
beginMarkedContent (BDC/BMC) | parseMarkedContentProps, markedContentLevel++ |
Async operator handling: for operators that need async I/O — XObject, font, color space, GState, and so on — the loop pauses via next(promise), and resumes with promiseBody(resolve, reject) once the async work finishes. This is a cooperative async state machine: every time an async operator is hit it returns, and the loop is re-entered once the Promise settles.
Wrap-up
When the stream is exhausted or the time slice runs out:
closePendingMarkedContentOPS(): fill in unclosedendMarkedContent.closePendingRestoreOPS(): fill in unclosedrestore(Q).resolve(): the operatorList is complete.
These two "fill-in" operations deal with malformed PDFs — a content stream where q has no matching Q, or BDC has no matching EMC. PDF.js completes them automatically so the rendering state does not leak.
The async getTextContent method
async getTextContent({ stream, task, resources, stateManager, includeMarkedContent, sink, seenStyles, viewBox, lang, disableNormalization, keepWhiteSpace, prevRefs, intersector })Extracts the page's text content for text selection, search, and accessibility. Unlike getOperatorList, it does not emit drawing operators; it emits text items (TextItem) and styles (TextStyle).
Core data structures:
const textContent = { items: [], styles: Object.create(null), lang };
const textContentItem = { str: [], totalWidth, transform, fontName, hasEOL, ... };Processing flow:
- Initialization: async stream handling, circular reference detection, the
TextStatestate stack. - Iterate operators: use
EvaluatorPreprocessorto read operators, but only handle text-related ones (Tf,Tj,Td,T*,Tm, etc.) and ignore drawing operators. - Character → Unicode mapping: convert character codes into Unicode strings through the font's
ToUnicodeCMap or encoding table. - Text chunking: group consecutive characters into a
TextItembased on font, transformation matrix, text rise, etc., recordingstr(text),transform(position/scale matrix), andwidth/height(bounds). - Fake space detection: use a
twoLastCharsring buffer and a spacing threshold to decide whether to insert a space between two characters (in PDFs, word spacing is often produced by numeric offsets inTJ, with no real space character). - Output: stream
TextItems out through asink(text receiver), supportingReadableStreamchunk-by-chunk delivery to the main thread.
When includeMarkedContent is true, it also extracts marked-content properties such as ActualText, for accessible text extraction.
The async extractDataStructures method
async extractDataStructures(dict, properties)Extracts the font's structural data from the font dictionary; a key step in the font translation flow. It fills the properties object that translateFont uses.
Extracted content:
| Data | Source | Notes |
|---|---|---|
toUnicode | The font dictionary's /ToUnicode stream | CMap from character code → Unicode, read asynchronously |
cidSystemInfo | /CIDSystemInfo | Registry/Ordering/Supplement of a CID font |
cidToGidMap | /CIDToGIDMap | CID → GlyphID mapping stream, used for CID fonts |
baseEncodingName | /Encoding (a Name, or the BaseEncoding of a Dict) | Base encoding name (WinAnsi/MacRoman, etc.) |
differences | The /Differences array of /Encoding | Encoding difference table, overriding part of the base encoding's character mapping |
Parsing the Differences array is at the heart of the PDF encoding mechanism — it is an alternating [number, name, name, ...] array where the number is the starting code and the following names map to successive codes. For example, [65 /A /B /C] means 65→A, 66→B, 67→C.
The _simpleFontToUnicode method
_simpleFontToUnicode(properties, forceGlyphs = false)Builds a character code → Unicode mapping for simple fonts (non-CID), using only the font's encoding information (not relying on the /ToUnicode stream).
Three-step algorithm:
- Merge encoding differences: start from
defaultEncoding(the base encoding table) and override the glyph names of the corresponding codes with thedifferencesarray. Skip.notdef(otherwise spaces would render as boxes). - Look up the Adobe Glyph List: for each code's glyph name, find the Unicode code point in
glyphsUnicodeMap(the AGL table). - Heuristic fallback: when the AGL lookup fails, guess from the glyph name format:
Gxx(3 digits) /g00xx(5 digits): the last two digits are a hexadecimal code point.Cdd/cdd(3-4 digits): decimal code point (hexadecimal whenforceGlyphsis set).uniXXXX/uXXXX: directly a Unicode code point.
This is where PDF.js is more lenient than Adobe Reader — Reader only honors the AGL, while PDF.js adds heuristic guessing to improve text extraction.
The async buildToUnicode method
async buildToUnicode(properties)Builds the complete ToUnicode mapping, in descending order of priority:
- The font's own
/ToUnicodestream: use it directly. For simple fonts, additionally generatefallbackToUnicode(inferred from the encoding) to complete incomplete ToUnicode data. - Simple font without ToUnicode: use
_simpleFontToUnicodeto infer from the encoding. - CID fonts: use a
registry-ordering-UCS2CMap (such asAdobe-GB1-UCS2) for CID → Unicode mapping. This requires loading the UCS2 CMap from the built-in CMaps. - Final fallback:
IdentityToUnicodeMap, treating the character code directly as the Unicode code point.
The async readToUnicode method
async readToUnicode(cmapObj)Reads and parses the /ToUnicode stream. cmapObj can be:
Name: a predefined CMap name (such as/Identity-H), created viaCMapFactory.create.Stream: embedded CMap data; after parsing, returns aCMapinstance.null: returns null (no ToUnicode).
The parsed CMap provides forEach(charcode, cid) iteration, which buildToUnicode uses to build the mapping.
The readCidToGidMap method
readCidToGidMap(glyphsData, toUnicode)Parses the /CIDToGIDMap stream — a UInt16BE array where the index is the CID and the value is the GlyphID. One GID is read every two bytes.
Optimization: skip entries where the GID is 0 and the CID has no mapping in toUnicode (the .notdef glyph). Returns a result[code] = glyphID array.
The extractWidths method
extractWidths(dict, descriptor, properties)Extracts glyph widths and vertical metrics from the font dictionary.
- Simple fonts: the
/Widthsarray plus/FirstChardefines the range; unspecified entries usedescriptor.MissingWidthor 0. - CID fonts: the
/Warray, in the alternating format[startCID [w1 w2 ...] | startCID endCID width]. The default width is/DW(1000 when absent). - Vertical writing:
/W2and/DW2provide the vertical metrics[w, x, y].
The isSerifFont method
isSerifFont(baseFontName)Determines whether a font is serif from substrings of the font name. Matching keywords such as Times, Garamond, Georgia, Palatino returns true, otherwise false. Used to guess the font style when there is no FontDescriptor.
The getBaseFontMetrics method
getBaseFontMetrics(name)Fetches the metric data of the standard 14 fonts (a Metrics object containing the widths glyph width table, stemV, etc.). If it is not a standard 14 font, it falls back to the metrics of Times-Roman or Helvetica, depending on serif versus sans-serif.
The buildCharCodeToWidth method
buildCharCodeToWidth(widthsByGlyphName, properties)Converts a "width table indexed by glyph name" into "a width array indexed by character code". It uses the encoding table to map each character code to a glyph name, then looks up the width. Used for width inference in standard fonts.
The preEvaluateFont method
preEvaluateFont(dict)The first step of font translation — extract the basic information from the font dictionary and return a preEvaluatedFont object for translateFont to use.
Extracted content:
Subtype: the font type; if it isType0, take the Subtype ofDescendantFonts[0](CIDFontType0/CIDFontType2).composite: whether it is a composite font.FirstChar/LastChar: character code range (0-0xFFFF by default for CID fonts).FontDescriptor: the font descriptor.ToUnicode: reference to the ToUnicode stream.hash: a MurmurHash3 64-bit hash computed from FontDescriptor + Encoding, used for font alias deduplication.defaultEncoding: the default encoding chosen from the font name/Subtype (StandardEncoding/WinAnsi, etc.).
The translateFont method
async translateFont(preEvaluatedFont)The core of font translation — turn the PDF font dictionary into a renderable Font object. Flow:
extractDataStructuresextracts ToUnicode, CIDSystemInfo, encoding, and widths.- Choose the font class by Subtype:
- Type1/MMType1 →
Type1Font(parses the CFF/Type1 data of FontFile/FontFile3). - TrueType →
TrueTypeFont(parses the TrueType data of FontFile2). - Type3 →
Type3Font(special handling; glyph streams are parsed byloadType3Data). - CIDFontType0 →
CFFFont. - CIDFontType2 →
TrueTypeFont.
- Type1/MMType1 →
- When there is no embedded font, use
getBaseFontMetricsstandard metrics or system font metrics. - Build the
toUnicodemapping and the width table. - Return a
Fontinstance, wrapped as aTranslatedFont.
The static buildFontPaths method
static buildFontPaths(font, glyphs, handler, evaluatorOptions)Pre-compiles the glyph outlines that must be rendered as paths and sends them to the main thread. Used when textRenderingMode includes ADD_TO_PATH_FLAG, when the color is a Pattern, or when disableFontFace is set.
For each glyph, call font.renderer.getPath(fontChar) to get the path instructions as an ArrayBuffer and send it via commonobj (document-level cache). Glyphs with accents additionally compile the path of the accent character.
The static get fallbackFontDict method
static get fallbackFontDict() {
const dict = new Dict();
dict.set("BaseFont", Name.get("Helvetica"));
dict.set("Type", Name.get("FallbackType"));
dict.set("Subtype", Name.get("FallbackType"));
dict.set("Encoding", Name.get("WinAnsiEncoding"));
return shadow(this, "fallbackFontDict", dict);
}Returns a fallback font dictionary — when a font reference is invalid and ignoreErrors is set, it falls back to Helvetica + WinAnsiEncoding so the text at least displays (even if it may be incorrect). It is cached as a class property with shadow, created only once.
The TranslatedFont class
TranslatedFont is the wrapper object around a translated font. It holds the font name, the font instance, and the original dictionary, and is responsible for sending the font to the main thread. Type3 fonts additionally manage parsing of the glyph streams.
The constructor method
constructor({ loadedName, font, dict }) {
this.loadedName = loadedName;
this.font = font;
this.dict = dict;
this.type3Dependencies = font.isType3Font ? new Set() : null;
}loadedName: the font name the main thread uses to register@font-face(${docId}_${fontID}).font: the translatedFontinstance (with metrics, glyph mapping, renderer).dict: the original font dictionary.type3Dependencies: the set of image/Pattern dependencies referenced by the Type3 font's glyph streams; they must be added to the parent operatorList's dependencies.
The send method
send(handler) {
if (this.#sent) return;
this.#sent = true;
const fontData = this.font.exportData();
const transfers = fontData.buffer ? [fontData.buffer] : null;
handler.send("commonobj", [this.loadedName, "Font", fontData], transfers);
}Sends the font data to the main thread through commonobj (document-level sharing). The #sent flag guarantees it is sent only once. transfers transfers the font binary data as a Transferable, with zero copies.
The fallback method
fallback(handler, evaluatorOptions) {
if (!this.font.data) return;
this.font.disableFontFace = true;
PartialEvaluator.buildFontPaths(this.font, this.font.glyphCacheValues, handler, evaluatorOptions);
}This is the fallback when main-thread font loading fails (@font-face registration fails): set disableFontFace to true and switch to rendering glyphs as paths. It re-sends the path data of all parsed glyphs to the main thread so subsequent text renders correctly.
The async loadType3Data method
async loadType3Data(evaluator, resources, task, seenRefs)The core method for Type3 fonts — parses all glyph streams. Flow:
- Clone the evaluator:
evaluator.clone({ ignoreErrors: false }), and settype3FontRefsto prevent cycles. - Iterate
CharProcs: callgetOperatorListto parse each glyph stream. - Handle the
d0/d1operators:d1(setCharWidthAndBounds): call#removeType3ColorOperatorsto remove the color operators (the spec requires that glyphs starting with d1 must not set colors).d0(setCharWidth): if the font has no BBox, call#guessType3FontBBoxto infer it from the operator list.
- Collect dependencies: add the image/Pattern dependencies from the glyph streams to
type3Dependencies. - Store:
font.charProcOperatorListkeeps the mapping from each glyph name to its IR.
It caches through the #type3Loaded Promise to avoid repeated parsing.
The #removeType3ColorOperators method
#removeType3ColorOperators(operatorList, fontBBoxSize)Per PDF spec §9.6.5, Type3 glyphs starting with d1 must not set colors. This method iterates the operatorList and removes every color-related operator (setFillColor, setStrokeColor, setColorSpace, shadingFill, setRenderingIntent, etc.), as well as color entries in the GState.
It also handles the d1 bounding box: if charBBox is an order of magnitude larger than fontBBox, the font BBox is updated with charBBox (a fix for broken files).
The #guessType3FontBBox method
#guessType3FontBBox(operatorList)When the Type3 font dictionary has no FontBBox and the glyphs start with d0 (no bounding box), it infers the glyph bounding box from the path operations in the operator list, so clipping is correct.
The StateManager class
The constructor method
constructor(initialState = new EvalState()) {
this.state = initialState;
this.stateStack = [];
}A graphics state stack manager, corresponding to the PDF q (save) and Q (restore) operators.
The save method
save() {
this.stateStack.push(this.state);
this.state = old.clone();
}Pushes the current state onto the stack, then clones it as the new state. Later modifications do not affect the state saved on the stack.
The restore method
restore() {
const prev = this.stateStack.pop();
if (prev) this.state = prev;
}Pops the top state and restores it. No-op when the stack is empty (tolerant).
The transform method
transform(args) {
this.state.ctm = Util.transform(this.state.ctm, args);
}Corresponds to the cm operator — left-multiplies the transformation matrix args onto the current CTM (Current Transformation Matrix).
The TextState class
The constructor method
TextState is the text state object used in getTextContent (as opposed to EvalState, which is used by getOperatorList). Fields include:
ctm: the current transformation matrix.fontName/fontSize/loadedName/font: current font information.fontMatrix: the transform from font space to text space.textMatrix/textLineMatrix: the text matrix and line matrix.charSpacing/wordSpacing/leading/textHScale/textRise: text parameters.
The setTextMatrix / setTextLineMatrix methods
Corresponds to the Tm operator, setting the 6 elements of the text matrix/line matrix directly.
The translateTextMatrix / translateTextLineMatrix methods
Corresponds to the Td/TD operators, translating relative to the current text matrix: e' = a*x + c*y + e, f' = b*x + d*y + f.
The carriageReturn method
Corresponds to the T* (next line) operator: the line matrix moves down by leading, and the text matrix is copied from the line matrix.
The clone method
Shallow-copies every field, but deep-copies the three arrays textMatrix/textLineMatrix/fontMatrix (so modifications do not affect the original object).
The EvalState class
The constructor method
EvalState is the graphics state object used by getOperatorList. Key fields:
ctm: the current transformation matrix.font: the current font.textRenderingMode: the text rendering mode (fill/stroke/fill+stroke/invisible/add to path)._fillColorSpace/_strokeColorSpace: fill/stroke color space.patternFillColorSpace/patternStrokeColorSpace: Pattern color spaces.currentPointX/currentPointY/pathMinMax/pathBuffer: path construction state.
The get/set fillColorSpace methods
The setter sets both _fillColorSpace and patternFillColorSpace — because when a color space is set, if that space is a Pattern, the later SCN then knows which pattern space to use.
The get/set strokeColorSpace methods
Same as above, for the stroke color space.
The clone method
clone({ newPath = false } = {}) {
const clone = Object.create(this);
if (newPath) {
clone.pathBuffer = [];
clone.pathMinMax = F32_BBOX_INIT.slice();
}
return clone;
}Shallow copy with Object.create(this). With newPath: true it resets the path-related fields — used for child content streams such as Form XObjects, to avoid leaking path state.
The EvaluatorPreprocessor class
EvaluatorPreprocessor is the lexer and operator dispatcher for PDF content streams; it turns the byte stream into {fn, args} operations for getOperatorList to handle.
The constructor method
constructor(stream, xref, stateManager) {
this.parser = new Parser({ lexer: new Lexer(stream, opMap), xref });
this.stateManager = stateManager;
this.nonProcessedArgs = [];
this._isPathOp = false;
this._numInvalidPathOPS = 0;
}parser: the PDF parser; reads tokens from the stream.nonProcessedArgs: an argument queue that handles PostScript-style nested commands (for example, in/F2 /GS2 gs 5.711 Tf,gsonly takes the preceding/GS2, leaving/F2forTf).
The static get opMap method
The operator mapping table — maps PDF operator names (such as m, l, Tj, Do) to {id, numArgs, variableArgs}.
id: the internal operator ID (OPS.*).numArgs: the number of arguments.variableArgs: whether the number of arguments is variable (e.g.TJtakes an array).
It also contains placeholders for reserved words such as BM/BD/true/false/null, so they are not mistaken for commands.
The static MAX_INVALID_PATH_OPS value
10 — the maximum number of consecutive path operation failures tolerated; past that the stream is considered corrupt and path parsing stops.
The get savedStatesDepth method
Returns stateManager.stateStack.length, i.e. the current nesting depth of q. getOperatorList uses it at wrap-up to fill in unmatched Qs.
The read method
read(operation) // → booleanReads one operator from the stream, filling in operation.fn and operation.args. Returning false means the stream has ended.
Core logic:
parser.getObj()reads tokens until aCmd(command) is reached.- Look up
opMapto getfnand the argument requirements. - Argument alignment: PostScript commands can nest, so the
nonProcessedArgsqueue adjusts the argument count untilargs.length === numArgs. - Path integrity check: when a path operator (
m/l/c, etc.) has too few arguments, it counts against_numInvalidPathOPS; past the threshold the operation is discarded.
Passing null for operation.args makes the method create the array internally (keeping null when there are no arguments, to avoid an allocation) — a performance optimization, since read is a hot path.
The preprocessCommand method
Preprocesses some commands (for example, expanding re into the path sequence m/l/l/l/h) to simplify later handling. It is a helper method called internally by read.
Summary
PartialEvaluator is the content stream interpreter on the PDF.js worker side. Its job is to turn a PDF content stream (a byte sequence) into an operator list — a series of high-level drawing instructions independent of rendering — for the main-thread CanvasGraphics to execute.
Core data flow
PDF content stream (bytes)
↓ EvaluatorPreprocessor (lexing)
{fn, args} operation sequence
↓ getOperatorList (dispatch)
OperatorList (OPS operator + arguments)
↓ MessageHandler (obj/commonobj)
Main-thread CanvasGraphics renders
The four subsystems
| Subsystem | Key methods | Responsibility |
|---|---|---|
| Lexing and state | EvaluatorPreprocessor.read, StateManager, EvalState | Split the byte stream into operators and maintain the graphics state stack |
| Resource parsing | buildFormXObject, buildPaintImageXObject, setGState, handleTilingType, parseShading | Parse XObjects, images, GState, Patterns |
| Font pipeline | loadFont → preEvaluateFont → translateFont → extractDataStructures → buildToUnicode | Font dictionary → renderable font object, including ToUnicode, widths, embedded data |
| Text extraction | getTextContent | Text item extraction independent of rendering, for selection/search/accessibility |
Key design principles
-
Evaluation is not rendering. "Partial" means "interpret only, do not draw" — every rendering decision (color space conversion, glyph outlines) is made in the worker where possible, and the main thread only does canvas drawing, staying lightweight.
-
Three-level caching. Global cache (
fontCache/globalImageCache, etc., shared at document level) → regional cache (_regionalImageCache, page level) → local cache (localImageCache, etc., single parse). Scope narrows level by level, balancing reuse and isolation. -
Cooperative async.
getOperatorListimplements state-machine-style async handling withnext(promise): on hitting an operator that needs I/O, such as an XObject or a font, it pauses; the loop continues once the Promise settles, so the worker is never blocked. -
Fault tolerance first. Circular reference detection (
seenRefs/type3FontRefs),ignoreErrorsdegradation, thefallbackFontDictsafety net,closePendingRestoreOPSstate completion — extremely lenient toward malformed PDFs: if it can open, open it. -
Special handling for Type3. A Type3 glyph stream is itself a PDF content stream, parsed recursively through a
cloned child evaluator, while switching the object ID prefix and the send channel (commonobj), and usingtype3FontRefsto prevent cycles.
With this, the operatorList generation chain is fully closed. The next article moves to the main thread to see how CanvasGraphics consumes this operator list and actually paints the PDF onto the canvas.
StreamPDF's design principle: anything that can be done ahead of time is never 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 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 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