StreamPDFStreamPDF
Back to blog

PDF.js Deep Dive 7: PartialEvaluator

October 25, 2026 · StreamPDF Team

PDF.js Deep Dive 7: PartialEvaluator

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 off

Below we break down each parameter and instance field one by one.

Core dependencies (runtime capabilities required for evaluation)

ParameterPurpose
xrefCross-reference table instance. The entire parsing process fetches dictionaries/streams by object reference through xref.fetch(ref) (fonts, images, ExtGState, ColorSpace, etc.).
handlerMessageHandler, 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", …).
pageIndexCurrent page index, used to identify object ownership. -1 means global context (XFA font loading only). Image/font IDs carry a page-number prefix.
idFactoryID 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:

ParameterCached content
fontCacheTranslated TranslatedFont objects; key is the font dictionary reference.
builtInCMapCacheBuilt-in CMap binary data (character code → CID mapping for CJK fonts); key is the CMap name.
standardFontDataCacheRaw font data of the standard 14 fonts.
globalColorSpaceCacheParsed ColorSpace objects.
globalImageCacheDecoded image data, shared across pages.
systemFontCacheSystem 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 a PartialEvaluator to parse. This field plays two roles:

    ① Flags whether a Type3 glyph parsing context is active. null means normal page evaluation; non-null (a RefSet) means Type3 glyph parsing is in progress. get parsingType3Font is simply !!this.type3FontRefs, and the evaluator switches behavior at 8 places based on it:

    BehaviorNormal pageType3 glyph
    Image/Pattern object ID prefiximg_${id}${docId}_type3_img_${id}
    Image/Pattern send channelobj (isolated per page)commonobj (shared at document level)
    Whether ImageMask uses OffscreenCanvasYesNo (not supported inside Type3 glyphs)
    Whether ImageMask is cachedCachedNot cached (Type3 glyphs are almost always inline images)
    Parse errorWhether 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. When TranslatedFont.loadType3Data creates 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 inside handleSetFont fires:

    // 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
    }

    RefSet deduplicates by reference (object identity), and errorFont() returns an empty font so the renderer skips that glyph, avoiding infinite recursion.

  • _regionalImageCache — a RegionalImageCache instance, an image cache at the current Evaluator (i.e. current page) level. It complements globalImageCache (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 of fetchBuiltInCMap. 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 that this inside the callback points at the current evaluator.

Design note: PartialEvaluator is 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:

  1. Lightweight — cache references are not copied; they are shared through the prototype chain.
  2. Isolated — the new evaluator can set its own instance fields such as type3FontRefs without affecting the parent.
  3. Overridable options — merged with Object.assign, with newOptions taking 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:

  1. Start from the passed-in resources and run a BFS.
  2. For each node, inspect /BM (Blend Mode) of every graphics state in the ExtGState dictionary:
    • /BM is a single Name and is not Normal → return true.
    • /BM is an array (the fallback mode list of a soft mask) → return true if any entry is not Normal.
  3. Recursively enter the Resources of every Form/Image XObject in the XObject dictionary and keep checking.
  4. Use processed (a RefSet) to record visited object references, preventing circular references and duplicate parsing.
  5. 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 repeated xref.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 calls fetchBinaryData directly to fetch ${cMapUrl}${name}.bcmap; only the compressed .bcmap format is supported.
  • useWorkerFetch: false: delegated to the main thread via MessageHandler.sendWithPromise("FetchBinaryData", …), supporting .bcmap (compressed) or uncompressed format (depending on cMapPacked).

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 warn and returns null; font parsing then falls back to a fallback font.
  • Returns binary data wrapped in a Stream, for StandardFontDataLoader to 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:

  1. Read the transformation matrix and bounding box: Matrix (the transform from xobj to user space) and BBox (the clip box).
  2. Optional content (OC): if the xobj has an /OC dictionary (optional content group), parse it first and insert a beginMarkedContentProps operator, so visibility can be controlled at render time.
  3. Determine whether it is a transparency group (Group):
    • Has /Group with /S /Transparency → must be rendered on a separate offscreen canvas (CheckedOperatorList), handling isolated, knockout, color space, the soft mask backdrop color (backdrop), and so on.
    • No Group → append directly to the current operatorList, wrapped in paintFormXObjectBegin/End.
  4. Recursively parse the content stream: call getOperatorList to parse the xobj's stream, using the xobj's own Resources (inheriting the outer resources when absent).
  5. Assemble the operator sequence:
    • With Group: beginGroup → paintFormXObjectBegin → [child content] → paintFormXObjectEnd → endGroup.
    • Without Group: paintFormXObjectBegin → [child content] → paintFormXObjectEnd.

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 when parsingType3Font is true or cacheGlobally is true. The object ID is document-level (with a docId prefix), and the main thread stores it in the global _objs cache, shared across pages.
  • obj: normal page images, carrying a pageIndex; 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 paintSolidColorImageMask directly without creating an image object.
  • Masks inside Type3 glyphs: try compileType3Glyph to compile them into path operators (constructPath); on failure fall back to an inline mask.
  • Normal masks: send mask_${id} image data and emit paintImageMaskXObject.

② 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 a docId_ 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 record decodeFailed and 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:

  1. Read /S (subtype: Alpha or Luminosity) and /BC (backdrop color).
  2. Read /TR (transfer function); if it is a PDF Function, call #createTransferMap to pre-compute a 256-entry LUT.
  3. Reuse buildFormXObject to parse the soft mask's content stream, passing smaskOptions as the smask parameter — later, in the Group branch of buildFormXObject, groupOptions.hasSoftMask is 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:

  1. If all functions in the array share the same reference, keep only one (optimization).
  2. For each entry: /Identity → null (skip); PDF Function → #createTransferMap generates a LUT.
  3. Validation: only 1 or 4 functions are supported (as required by the spec).
  4. If everything is /Identity, return null (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:

  1. Merge Resources: Dict.merge([patternDict.Resources, resources]), so the pattern dictionary does not miss some resource entries (the fix for issue6541.pdf).
  2. Parse the pattern content stream: call getOperatorList on a CheckedOperatorList to get the pattern's operator list.
  3. Generate the IR: getTilingPatternIR turns the operator list into an intermediate representation containing the tiling parameters and color information.
  4. Send the operator: operatorList.addOp(fn, tilingPatternIR), where fn is setFillPattern or setStrokePattern.
  5. 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:

  1. Load the font: call loadFont to get a TranslatedFont (a translated font object containing metrics, glyph mapping, font data, etc.).
  2. Set the current font: state.font = translated.font; subsequent text operators use this font.
  3. Send the font to the main thread: translated.send(this.handler) sends the font data (FontFaceObject) to the main thread through MessageHandler to 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:

  • textRenderingMode includes ADD_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.disableFontFace is 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:

KeyMeaningHandling
LWLine widthTake the absolute value
LC/LJ/ML/DLine cap/line join/miter limit/dashPass the value through
RIRendering intentPass the value through
FL/CA/caFlatness tolerance/stroke alpha/fill alphaPass the value through
FontFontCall handleSetFont asynchronously, isSimpleGState = false
BMBlend modeNormalized with normalizeBlendMode
SMaskSoft maskCall handleSMask, isSimpleGState = false
TR/TR2Transfer functionhandleTransferFunction 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:

  1. Resolve the reference: use font directly when it is a Ref; otherwise look up the reference by name in resources.Font.
  2. Cycle detection: if fontRef is already in type3FontRefs (a Type3 recursive reference), return errorFont.
  3. Cache hit: if it is already in fontCache, return it directly.
  4. Fetch the font dictionary: xref.fetchIfRef(fontRef) resolves the dictionary.
  5. Fallback: if it is not a valid dictionary, in ignoreErrors mode use fallbackFontDict or fallbackFontDict (standard Helvetica) as a safety net.
  6. Pre-evaluation: preEvaluateFont(font) extracts the font's basic information (Subtype, BaseFont, encoding, descriptor, etc.).
  7. Font ID and alias:
    • If the font has a FontDescriptor and a hash can be computed, use fontAliases[hash] for alias deduplication — several font dictionaries that reference the same font data share one fontID.
    • Otherwise idFactory.createFontId() generates a new ID.
  8. Write to cache: fontCache.put(fontRef, promise); what is cached is the Promise (translation is in progress).
  9. Async translation: translateFont(preEvaluatedFont) produces translatedFont; for Type3 fonts, loadType3Data then parses the glyph streams.
  10. resolve: resolve the TranslatedFont object 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-in ColorSpace directly.
  • 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.ColorSpace dictionary.

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:

  1. Cache lookup: localShadingPatternCache looks up the ID by shading object reference; on a hit it returns immediately.
  2. Parse: Pattern.parseShading turns the shading dictionary into a ShadingPattern object, internally handling the color space, coordinate transform, functions, etc.
  3. Generate the IR: shadingFill.getIR() produces the intermediate representation.
  4. Send to the main thread:
    • Inside Type3 glyphs: use commonobj, prefix the ID with _type3_, and compile the IR into an ArrayBuffer for transfer.
    • Normal pages: use obj, with pageIndex.
  5. 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:

  1. Pop the pattern name: args.pop().
  2. Tiling Pattern cache hit: if the pattern is already in localTilingPatternCache, use the cached IR directly to emit the setFillPattern/setStrokePattern operator.
  3. Otherwise parse the pattern:
    • PatternType.TILING (1) → call handleTilingType.
    • PatternType.SHADING (2) → call parseShading to get the gradient ID and emit the pattern operator.
  4. Color components: if the pattern's underlying color space is DeviceRGB and 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): /OC references, 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

  1. Async stream handling: if stream.isAsync (the data has not fully arrived), call asyncGetBytes() first to get the full data, then wrap it in a synchronous Stream.
  2. Circular reference detection: seenRefs = new RefSet(prevRefs) inherits the parent's reference set; if the current stream's objId is already in the set, throw (XObjects referencing each other).
  3. Initialize local caches: every call creates fresh LocalImageCache, LocalColorSpaceCache, LocalGStateCache, LocalTilingPatternCache — caches within this parse, discarded when it ends.
  4. State and preprocessor: StateManager (the graphics state stack), EvaluatorPreprocessor (the lexer that turns the byte stream into {fn, args} operations).
  5. Time slice management: TimeSlotManager controls 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:

OperatorHandling
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:

  1. closePendingMarkedContentOPS(): fill in unclosed endMarkedContent.
  2. closePendingRestoreOPS(): fill in unclosed restore (Q).
  3. 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:

  1. Initialization: async stream handling, circular reference detection, the TextState state stack.
  2. Iterate operators: use EvaluatorPreprocessor to read operators, but only handle text-related ones (Tf, Tj, Td, T*, Tm, etc.) and ignore drawing operators.
  3. Character → Unicode mapping: convert character codes into Unicode strings through the font's ToUnicode CMap or encoding table.
  4. Text chunking: group consecutive characters into a TextItem based on font, transformation matrix, text rise, etc., recording str (text), transform (position/scale matrix), and width/height (bounds).
  5. Fake space detection: use a twoLastChars ring 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 in TJ, with no real space character).
  6. Output: stream TextItems out through a sink (text receiver), supporting ReadableStream chunk-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:

DataSourceNotes
toUnicodeThe font dictionary's /ToUnicode streamCMap from character code → Unicode, read asynchronously
cidSystemInfo/CIDSystemInfoRegistry/Ordering/Supplement of a CID font
cidToGidMap/CIDToGIDMapCID → GlyphID mapping stream, used for CID fonts
baseEncodingName/Encoding (a Name, or the BaseEncoding of a Dict)Base encoding name (WinAnsi/MacRoman, etc.)
differencesThe /Differences array of /EncodingEncoding 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:

  1. Merge encoding differences: start from defaultEncoding (the base encoding table) and override the glyph names of the corresponding codes with the differences array. Skip .notdef (otherwise spaces would render as boxes).
  2. Look up the Adobe Glyph List: for each code's glyph name, find the Unicode code point in glyphsUnicodeMap (the AGL table).
  3. 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 when forceGlyphs is 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:

  1. The font's own /ToUnicode stream: use it directly. For simple fonts, additionally generate fallbackToUnicode (inferred from the encoding) to complete incomplete ToUnicode data.
  2. Simple font without ToUnicode: use _simpleFontToUnicode to infer from the encoding.
  3. CID fonts: use a registry-ordering-UCS2 CMap (such as Adobe-GB1-UCS2) for CID → Unicode mapping. This requires loading the UCS2 CMap from the built-in CMaps.
  4. 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 via CMapFactory.create.
  • Stream: embedded CMap data; after parsing, returns a CMap instance.
  • 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 /Widths array plus /FirstChar defines the range; unspecified entries use descriptor.MissingWidth or 0.
  • CID fonts: the /W array, in the alternating format [startCID [w1 w2 ...] | startCID endCID width]. The default width is /DW (1000 when absent).
  • Vertical writing: /W2 and /DW2 provide 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 is Type0, take the Subtype of DescendantFonts[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:

  1. extractDataStructures extracts ToUnicode, CIDSystemInfo, encoding, and widths.
  2. 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 by loadType3Data).
    • CIDFontType0 → CFFFont.
    • CIDFontType2 → TrueTypeFont.
  3. When there is no embedded font, use getBaseFontMetrics standard metrics or system font metrics.
  4. Build the toUnicode mapping and the width table.
  5. Return a Font instance, wrapped as a TranslatedFont.

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 translated Font instance (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:

  1. Clone the evaluator: evaluator.clone({ ignoreErrors: false }), and set type3FontRefs to prevent cycles.
  2. Iterate CharProcs: call getOperatorList to parse each glyph stream.
  3. Handle the d0/d1 operators:
    • d1 (setCharWidthAndBounds): call #removeType3ColorOperators to 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 #guessType3FontBBox to infer it from the operator list.
  4. Collect dependencies: add the image/Pattern dependencies from the glyph streams to type3Dependencies.
  5. Store: font.charProcOperatorList keeps 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, gs only takes the preceding /GS2, leaving /F2 for Tf).

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. TJ takes 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) // → boolean

Reads one operator from the stream, filling in operation.fn and operation.args. Returning false means the stream has ended.

Core logic:

  1. parser.getObj() reads tokens until a Cmd (command) is reached.
  2. Look up opMap to get fn and the argument requirements.
  3. Argument alignment: PostScript commands can nest, so the nonProcessedArgs queue adjusts the argument count until args.length === numArgs.
  4. 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

SubsystemKey methodsResponsibility
Lexing and stateEvaluatorPreprocessor.read, StateManager, EvalStateSplit the byte stream into operators and maintain the graphics state stack
Resource parsingbuildFormXObject, buildPaintImageXObject, setGState, handleTilingType, parseShadingParse XObjects, images, GState, Patterns
Font pipelineloadFont → preEvaluateFont → translateFont → extractDataStructures → buildToUnicodeFont dictionary → renderable font object, including ToUnicode, widths, embedded data
Text extractiongetTextContentText item extraction independent of rendering, for selection/search/accessibility

Key design principles

  1. 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.

  2. 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.

  3. Cooperative async. getOperatorList implements state-machine-style async handling with next(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.

  4. Fault tolerance first. Circular reference detection (seenRefs/type3FontRefs), ignoreErrors degradation, the fallbackFontDict safety net, closePendingRestoreOPS state completion — extremely lenient toward malformed PDFs: if it can open, open it.

  5. 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 using type3FontRefs to 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