StreamPDFStreamPDF
返回博客

PDF.js 深入浅出 7: PartialEvaluator

2026年10月25日 · StreamPDF Team

PDF.js 深入浅出 7: PartialEvaluator

前言

这是 PDF.js 系列文章的第7篇,介绍 PartialEvaluator 类的源码解读。

上衔接第三篇文章的 _pumpOperatorList 方法的 GetOperatorList sendWithStream 调用. 主要是解读 operatorList 是如何解析出来的。

PartialEvaluator 类

从类的名字上我们可以看到, 这是一个部分解释器类, 仅用于解析 PDF 文档的 operatorList。而不渲染, 所以叫 Partial, 而不是 Full。

constructor 方法

由 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);
}

构造函数整体采用依赖注入 + 缓存共享的设计:所有跨页共享的重量级资源(字体、CMap、颜色空间、图片等)都由外部(Page/Catalog)注入,PartialEvaluator 自身只持有当前页的临时状态。

// src/core/document.js
 
// 1) 页面级 —— 解析普通页面内容流
_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) 全局级 —— 仅用于 XFA 表单字体加载 (loadXfaFonts)
//    pageIndex: -1, 且关闭 useSystemFonts

下面逐一拆解每个参数与实例字段。

核心依赖(求值必须的运行时能力)

参数作用
xref交叉引用表实例。整个解析过程通过 xref.fetch(ref) 按对象引用取字典/流(字体、图片、ExtGState、ColorSpace 等)。
handlerMessageHandler,Worker 与主线程通信的句柄。当 useWorkerFetch 为 false 时,CMap 与标准字体数据需通过 handler.sendWithPromise("FetchBinaryData", …) 回主线程拉取。
pageIndex当前页索引,用于标识对象归属。-1 表示全局上下文(仅 XFA 字体加载)。图片/字体 ID 会带上页号前缀。
idFactoryID 工厂,为发往主线程的对象(字体 FontFaceObject、图片等)生成唯一 ID。页面级用 _localIdFactory(p${pageIndex}_${n}),全局级用 _globalIdFactory。

跨页共享缓存(由 Catalog 注入,避免每页重复解析)

这些缓存是整个文档级别共享的,同一个字体字典、同一张图片在多页出现时只解析/解码一次:

参数缓存内容
fontCache已翻译的 TranslatedFont,key 为字体字典引用。
builtInCMapCache内建 CMap 二进制数据(CJK 字体的字符编码→CID 映射),key 为 CMap 名。
standardFontDataCache标准 14 字体的原始字体数据。
globalColorSpaceCache已解析的 ColorSpace 对象。
globalImageCache图片解码数据,跨页共享。
systemFontCache系统字体度量(当 useSystemFonts 为 true 时使用)。

求值选项

options 缺省为 DefaultPartialEvaluatorOptions(定义在 evaluator.js 顶部),关键字段:

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: Worker 内直接 fetch; false: 回主线程 fetch
  cMapUrl: null, // 内建 CMap 资源地址
  cMapPacked: true, // 是否使用压缩的 .bcmap
  iccUrl: null,
  standardFontDataUrl: null, // 标准字体数据资源地址
  wasmUrl: null,
  hasGPU: false,
});

这些选项大多来自主线程 getDocument 传入并透传到 Worker(见第 2 篇 GetDocRequest)。useWorkerFetch 决定了资源是 Worker 直接拉取还是委托主线程,影响 fetchBuiltInCMap / fetchStandardFontData 的分支。

实例自身的临时状态

this.type3FontRefs = null;
this._regionalImageCache = new RegionalImageCache();
this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this);
  • type3FontRefs — Type3 字体是「用 PDF 绘图算子描述字形」的字体,其字形流(CharProcs)本身就是一段 PDF 内容流,需要用一个 PartialEvaluator 去解析。此字段有两个作用:

    ① 标记当前是否处于 Type3 字形解析上下文。null 表示普通页面求值;非 null(一个 RefSet)表示正在解析 Type3 字形。get parsingType3Font 就是 !!this.type3FontRefs,整个 evaluator 里有 8 处依据它切换行为:

    行为普通页面Type3 字形
    图片/Pattern 对象 ID 前缀img_${id}${docId}_type3_img_${id}
    图片/Pattern 发送通道obj(按页隔离)commonobj(文档级共享)
    ImageMask 是否用 OffscreenCanvas是否(Type3 字形内不支持)
    ImageMask 是否缓存缓存不缓存(Type3 字形内几乎都是内联图)
    解析出错按 ignoreErrors 决定是否忽略直接抛错(字形只能完整渲染,部分无意义)

    ② 防止循环引用。Type3 字体的字形流里可能再次 Tf 引用字体,极端情况下会引用自身。在 TranslatedFont.loadType3Data 中创建子 evaluator 时:

    // src/core/evaluator.js (TranslatedFont.loadType3Data)
    const type3Evaluator = evaluator.clone({ ignoreErrors: false });
    const type3FontRefs = new RefSet(evaluator.type3FontRefs); // 继承父级集合
    if (dict.objId) {
      type3FontRefs.put(dict.objId); // 把当前 Type3 字体自身加入集合
    }
    type3Evaluator.type3FontRefs = type3FontRefs;

    这样形成一条引用链:解析字体 A 的字形时集合里有 A;如果 A 的字形引用了字体 B,解析 B 时集合变成 {A, B};若 B 的字形又引用回 A,handleSetFont 里的检测就会命中:

    // src/core/evaluator.js (handleSetFont)
    if (this.type3FontRefs?.has(fontRef)) {
      return errorFont(); // 已在解析链中,返回占位错误字体,终止递归
    }

    RefSet 按引用(对象身份)去重,errorFont() 返回一个空字体让渲染跳过该字形,避免无限递归。

  • _regionalImageCache — RegionalImageCache 实例,当前 Evaluator(即当前页)级别的图片缓存。与 globalImageCache(跨页)互补,主要处理 tiled / cropped 图片这类区域性的、只在当前页有效的缓存项,避免污染全局缓存。

  • _fetchBuiltInCMapBound — fetchBuiltInCMap 的预绑定版本。字体解析需要一个「按 CMap 名取数据」的回调,提前 bind 好避免每次调用重复创建闭包,也保证回调内 this 指向当前 evaluator。

设计要点:PartialEvaluator 不是单例,每页内容流解析都新建一个实例,但它持有的缓存全部是外部注入的共享引用——实例轻量、缓存共享,兼顾了「每页独立的求值状态」与「跨页复用已解析资源」两个目标。

get _pdfFunctionFactory 方法

get _pdfFunctionFactory() {
  return shadow(
    this,
    "_pdfFunctionFactory",
    new PDFFunctionFactory({ xref: this.xref })
  );
}

懒加载 + 缓存。PDFFunctionFactory 负责解析 PDF 中的 Function 对象(/Function 字典,类型 0-4),用于颜色空间转换(/CalRGB、/ICCBased 的 tint transform)、shading、Transfer 函数等。

用 shadow 工具函数实现「首次访问时创建,之后直接取属性」的效果——把新对象写入实例自身属性,覆盖 getter。由于 Function 对象按引用缓存,一个 evaluator 实例内共享一个 factory 即可,无需跨页共享。

get parsingType3Font 方法

get parsingType3Font() {
  return !!this.type3FontRefs;
}

语义就是「当前是否正在解析 Type3 字形流」。在 constructor 章节已详述:type3FontRefs 为 null 时返回 false(普通页面求值),非 null 时返回 true(Type3 字形求值)。整个 evaluator 有 8 处依据它切换图片/Pattern 的对象 ID 前缀、发送通道(obj vs commonobj)、错误处理策略等。

clone 方法

clone(newOptions = null) {
  const newEvaluator = Object.create(this);
  newEvaluator.options = Object.assign(
    Object.create(null),
    this.options,
    newOptions
  );
  return newEvaluator;
}

用 Object.create(this) 创建一个原型指向当前 evaluator 的新对象。这意味着新 evaluator 共享原实例的所有属性(xref、缓存、pageIndex 等),但可以独立覆盖 options。

典型用法是 Type3 字体解析(TranslatedFont.loadType3Data):

const type3Evaluator = evaluator.clone({ ignoreErrors: false });

只改了 ignoreErrors 选项,其余全部继承。这样做的好处:

  1. 轻量——不复制缓存引用,原型链共享。
  2. 隔离——新 evaluator 可以设置自己的 type3FontRefs 等实例字段,不影响父级。
  3. 选项可覆盖——通过 Object.assign 合并,newOptions 优先级最高。

hasBlendModes 方法

hasBlendModes(resources, nonBlendModesSet)

判断一组 Resources 中是否存在非 Normal 的混合模式(Blend Mode)。这是一个预检优化:在渲染组(Transparency Group)之前调用,如果没有混合模式,主线程 CanvasGraphics 可以跳过透明度合成的开销。

算法是一个带环检测的图遍历:

  1. 从传入的 resources 开始,BFS 遍历。
  2. 对每个节点,检查 ExtGState 字典里每个图形状态的 /BM(Blend Mode):
    • /BM 是单个 Name 且不是 Normal → 返回 true。
    • /BM 是数组(软遮罩的备用模式列表)→ 任一非 Normal 即返回 true。
  3. 递归进入 XObject 字典中的每个 Form/Image XObject 的 Resources,继续检查。
  4. 用 processed(RefSet)记录已访问的对象引用,防止循环引用和重复解析。
  5. 如果遍历完没发现混合模式,把所有处理过的引用加入 nonBlendModesSet——这个集合是文档级共享的,后续页面遇到相同对象直接跳过,避免重复 xref.fetch(注释提到 issue6961.pdf,一个有大量 XObject 的坏文件,此优化显著减少加载时间)。

fetchBuiltInCMap 方法

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

按 CMap 名称获取内建 CMap 数据。内建 CMap 是 Adobe 定义的标准字符编码映射(如 GBK-EUC-H、UniGB-UCS2-H),用于 CJK 字体的字符编码→CID 映射。

两条路径:

  • useWorkerFetch: true(默认):Worker 线程内直接 fetchBinaryData 拉取 ${cMapUrl}${name}.bcmap,只支持压缩的 .bcmap 格式。
  • useWorkerFetch: false:通过 MessageHandler.sendWithPromise("FetchBinaryData", …) 委托主线程拉取,支持 .bcmap(压缩)或未压缩格式(取决于 cMapPacked)。

结果写入 builtInCMapCache(文档级缓存),避免重复网络请求。MOZCENTRAL(Firefox 内置)构建强制走 Worker fetch。

fetchStandardFontData 方法

async fetchStandardFontData(name) {
  const cachedData = this.standardFontDataCache.get(name);
  if (cachedData) return new Stream(cachedData);
 
  // Symbol / ZapfDingbats 跨平台不一致,必须加载标准数据
  if (this.options.useSystemFonts && name !== "Symbol" && name !== "ZapfDingbats") {
    return null; // 用系统字体,不加载标准字体数据
  }
 
  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);
}

获取标准 14 字体(Helvetica、Times、Courier、Symbol、ZapfDingbats 及其变体)的原始字体数据。

关键逻辑:

  • useSystemFonts: true 时:优先用操作系统自带的字体度量,不加载标准数据。但 Symbol 和 ZapfDingbats 例外——这两个字体在不同平台上字形不一致(macOS 和 Windows 的 Symbol 字体编码不同),必须加载 PDF.js 自带的标准数据来保证渲染一致。
  • 失败降级:网络错误时 warn 并返回 null,后续字体解析会走 fallback 字体。
  • 返回 Stream 包装的二进制数据,供 StandardFontDataLoader 解析字体度量。

buildFormXObject 方法

async buildFormXObject(resources, xobj, smask, operatorList, task, initialState, localColorSpaceCache, seenRefs)

处理 Form XObject(/Subtype /Form 的 XObject)——PDF 中可复用的独立内容流,相当于「页面中的小页面」。

核心流程:

  1. 读取变换矩阵与边界框:Matrix(xobj → 用户空间的变换)和 BBox(裁剪框)。
  2. 可选内容(OC):如果 xobj 有 /OC 字典(可选内容组),先解析并插入 beginMarkedContentProps 算子,渲染时可控制可见性。
  3. 判断是否为透明组(Group):
    • 有 /Group 且 /S /Transparency → 需要在独立的离屏 canvas 上渲染(CheckedOperatorList),处理 isolated、knockout、颜色空间、软遮罩背景色(backdrop)等属性。
    • 无 Group → 直接往当前 operatorList 追加,用 paintFormXObjectBegin/End 包裹。
  4. 递归解析内容流:调 getOperatorList 解析 xobj 的流,使用 xobj 自身的 Resources(没有则继承外部 resources)。
  5. 组装算子序列:
    • 有 Group:beginGroup → paintFormXObjectBegin → [子内容] → paintFormXObjectEnd → endGroup。
    • 无 Group:paintFormXObjectBegin → [子内容] → paintFormXObjectEnd。

seenRefs 用于检测 XObject 循环引用(Form XObject 互相引用)。

_sendImgData 方法

_sendImgData(objId, imgData, cacheGlobally = false)

将图片数据通过 MessageHandler 发送到主线程。两个发送通道:

  • commonobj:parsingType3Font 为 true 或 cacheGlobally 为 true 时使用。对象 ID 为文档级(带 docId 前缀),主线程存到全局 _objs 缓存,跨页共享。
  • obj:普通页面图片,带 pageIndex,主线程按页管理,页面销毁时释放。

transfers 把 ArrayBuffer(图片像素数据)以 Transferable 方式传给主线程(零拷贝),避免大数组的结构化克隆开销。

buildPaintImageXObject 方法

async buildPaintImageXObject({ resources, image, isInline, operatorList, cacheKey, localImageCache, localColorSpaceCache })

处理 Image XObject 和内联图片,是 evaluator 里最复杂的方法之一。分为三大分支:

① ImageMask(图像遮罩)

  • 遮罩图只有 1 位深度,需要用当前填充色染色,不能在 Worker 里完全处理。
  • 特殊优化:如果是「单不透明像素」遮罩(LaTeX 文档常用它画彩色线条),直接输出 paintSolidColorImageMask,不创建图片对象。
  • Type3 字形内的遮罩:尝试 compileType3Glyph 编译为路径算子(constructPath),失败则回退内联遮罩。
  • 普通遮罩:发送 mask_${id} 图片数据,输出 paintImageMaskXObject。

② 小内联图片直接内联

isInline && w + h < 200 && !hasMask 时,在 Worker 里直接解码为 RGBA,通过 paintInlineImageXObject 内联进 operatorList,不经过对象传输。

③ 普通图片

  • 生成 objId,Type3 字形加 _type3_ 前缀,可全局缓存的加 docId_ 前缀。
  • 输出 paintImageXObject [objId, w, h] 算子。
  • 全局缓存优化:大图(>500×500)或带遮罩的图,先尝试从主线程本地缓存复制(CopyLocalImage),避免重新解码。
  • 异步解码:PDFImage.buildImage → createImageData,解码成功发送图片数据,失败记录 decodeFailed 并发送 null(渲染时跳过)。

#createTransferMap 方法

#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;
  });
}

把 PDF 的 Transfer Function(/Function 类型 2/4,用于校准设备响应)预计算成一张 256 项的查找表(LUT)。输入归一化到 [0,1],函数输出再量化回 [0,255]。

这样 handleTransferFunction 应用转移函数时只需 O(1) 查表,避免对每个像素重复执行函数计算。

handleSMask 方法

handleSMask(smask, resources, operatorList, task, stateManager, localColorSpaceCache, seenRefs)

处理 Soft Mask(软遮罩)——/SMask 字典定义了一个基于透明度的遮罩,其子对象 /G 是一个 Form XObject(通常是灰度图),其像素值决定被遮罩对象的不透明度。

核心逻辑:

  1. 读取 /S(子类型:Alpha 或 Luminosity)和 /BC(背景色)。
  2. 读取 /TR(转移函数),如果是 PDF Function,调用 #createTransferMap 预计算成 256 项 LUT。
  3. 复用 buildFormXObject 解析软遮罩的内容流,把 smaskOptions 作为 smask 参数传入——后续在 buildFormXObject 的 Group 分支里,groupOptions.hasSoftMask 会置 true,主线程渲染时启用离屏合成。

stateManager.state.clone({ newPath: true }) 传入一个清空路径的状态副本,避免软遮罩内的绘图算子影响外部路径。

handleTransferFunction 方法

handleTransferFunction(tr)

处理 ExtGState 中的 /TR(Transfer Function),用于校准设备的颜色响应(如补偿打印机的非线性)。

/TR 可以是:

  • 单个 Function:应用于所有颜色分量。
  • 4 个 Function 的数组:分别对应 R/G/B 或 C/M/Y/K。
  • /Identity:恒等映射,不做变换。

处理逻辑:

  1. 如果数组里所有函数引用相同,只保留一个(优化)。
  2. 对每个条目:/Identity → null(跳过);PDF Function → #createTransferMap 生成 LUT。
  3. 校验:只支持 1 或 4 个函数(规范要求)。
  4. 如果全部是 /Identity,返回 null(无操作)。

返回的 transferMaps 数组([mapR, mapG, mapB] 或 [mapAll])会随 ExtGState 发送给主线程,CanvasGraphics 应用颜色时查表。

handleTilingType 方法

handleTilingType(fn, color, resources, pattern, patternDict, operatorList, task, localTilingPatternCache, seenRefs)

处理 Tiling Pattern(平铺图案)——用一段内容流重复铺开来填充区域。pattern 是图案的内容流,patternDict 是图案字典(定义 XStep/YStep 平铺间距、BBox、PaintType 等)。

核心流程:

  1. 合并 Resources:Dict.merge([patternDict.Resources, resources]),防止图案字典缺少部分资源条目(issue6541.pdf 的修复)。
  2. 解析图案内容流:用 CheckedOperatorList 调 getOperatorList,得到图案的操作列表。
  3. 生成 IR:getTilingPatternIR 把操作列表转成中间表示,包含平铺参数和颜色信息。
  4. 发送算子:operatorList.addOp(fn, tilingPatternIR),fn 是 setFillPattern 或 setStrokePattern。
  5. 缓存:把 IR 存入 localTilingPatternCache(按图案对象引用),同一图案复用。

CheckedOperatorList 会检测图案内是否需要隔离(needsIsolation),影响主线程渲染策略。

async handleSetFont 方法

async handleSetFont(resources, fontArgs, fontRef, operatorList, task, state, fallbackFontDict, cssFontInfo, seenRefs)

对应 PDF 的 Tf(set font)算子。做三件事:

  1. 加载字体:调 loadFont 得到 TranslatedFont(已翻译的字体对象,包含度量、字形映射、字体数据等)。
  2. 设置当前字体:state.font = translated.font,后续文本算子使用此字体。
  3. 发送字体到主线程:translated.send(this.handler) 把字体数据(FontFaceObject)通过 MessageHandler 发给主线程注册 @font-face。

返回 loadedName,即主线程可使用的 CSS 字体名。

如果是 Type3 字体,额外把 type3Dependencies(字形流里的图片/Pattern 依赖)加入父 operatorList 的依赖列表,确保同步执行前已解析完毕。

handleText 方法

handleText(chars, state)

对应 Tj/TJ/'/" 等文本显示算子。把字符编码转成字形索引:

const glyphs = font.charsToGlyphs(chars);

关键分支——何时需要把字形轮廓转成路径:

当满足以下任一条件时,调 buildFontPaths 把字形轮廓(glyph outlines)转成 PDF 路径算子,而不是用字体渲染:

  • textRenderingMode 包含 ADD_TO_PATH_FLAG(渲染模式 2/6/7:填充+描边+添加到路径)。
  • 填充或描边颜色空间是 Pattern(图案颜色无法通过字体填充实现)。
  • font.disableFontFace 为 true(字体不支持通过 @font-face 渲染,如某些 Type1 字体)。

返回 glyphs 数组,后续由 showText 算子携带给主线程渲染。

ensureStateFont 方法

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

防御性检查:在执行文本显示算子(Tj 等)前,确保已经通过 Tf 设置了字体。否则 PDF 规范上是非法的。ignoreErrors 模式下只 warn,否则抛 FormatError。

async setGState 方法

async setGState({ resources, gState, operatorList, cacheKey, task, stateManager, localGStateCache, localColorSpaceCache, seenRefs })

对应 gs 算子——从 ExtGState 字典设置扩展图形状态。遍历字典的每个键值对,转换后压入 gStateObj 数组,最终输出 setGState 算子。

支持的状态参数:

键含义处理方式
LW线宽取绝对值
LC/LJ/ML/D线帽/线连接/斜接限制/虚线直接传值
RI渲染意图直接传值
FL/CA/ca平场阈值/描边透明度/填充透明度直接传值
Font字体异步调 handleSetFont,isSimpleGState = false
BM混合模式normalizeBlendMode 标准化
SMask软遮罩调 handleSMask,isSimpleGState = false
TR/TR2转移函数handleTransferFunction 生成 LUT 数组

isSimpleGState 标记该 GState 是否只含简单参数——只有简单的才缓存到 localGStateCache(含字体/软遮罩的有副作用,不缓存)。

未实现的参数(OP/BG/HT 等)只打 info 日志,不影响渲染。

loadFont 方法

loadFont(fontName, font, resources, task, fallbackFontDict, cssFontInfo, seenRefs)

按名称或引用加载字体,是字体管线的入口。核心流程:

  1. 解析引用:font 是 Ref 时直接用;否则从 resources.Font 按名查引用。
  2. 循环检测:如果 fontRef 已在 type3FontRefs 中(Type3 递归引用),返回 errorFont。
  3. 缓存命中:fontCache 里已有则直接返回。
  4. 取字体字典:xref.fetchIfRef(fontRef) 解析字典。
  5. 降级:如果不是有效字典,ignoreErrors 模式下用 fallbackFontDict 或 fallbackFontDict(标准 Helvetica)兜底。
  6. 预评估:preEvaluateFont(font) 提取字体的基础信息(Subtype、BaseFont、编码、描述符等)。
  7. 字体 ID 与别名:
    • 如果字体有 FontDescriptor 且能计算出 hash,用 fontAliases[hash] 做别名去重——多个字体字典引用同一份字体数据时共享一个 fontID。
    • 否则 idFactory.createFontId() 生成新 ID。
  8. 写入缓存:fontCache.put(fontRef, promise),缓存的是 Promise(异步翻译中)。
  9. 异步翻译:translateFont(preEvaluatedFont) 得到 translatedFont,如果是 Type3 字体再 loadType3Data 解析字形流。
  10. resolve:把 TranslatedFont 对象 resolve 给调用者。

loadedName 格式为 ${docId}_${fontID},是主线程注册 @font-face 时用的字体名。

buildPath 方法

buildPath(operatorList, args)

把 PDF 路径构造算子(m/l/c/v/y/h/re)转换成 operatorList 中的路径操作。args 是算子参数数组,buildPath 负责将它们映射到内部的路径指令格式。

这是路径构建的统一入口,所有路径算子最终都通过它输出 OPS.constructPath,主线程 CanvasGraphics 据此调用 canvas 的路径 API。

_getColorSpace 方法

_getColorSpace(cs, resources, localColorSpaceCache)

从 Resources 的 ColorSpace 字典中查找颜色空间对象。返回原始的颜色空间对象(可能是 Name、Array、ICCBased 流等),不做解析。

如果颜色空间已在 localColorSpaceCache 中,直接返回缓存。这是 _handleColorSpace 的前置步骤——先取原始对象,再解析。

async _handleColorSpace 方法

async _handleColorSpace(cs, resources, localColorSpaceCache, cacheKey)

把原始颜色空间对象解析成 ColorSpace 实例,并通过 MessageHandler 发送到主线程。

支持的颜色空间类型:

  • 设备空间:DeviceGray、DeviceRGB、DeviceCMYK——直接用内置 ColorSpace。
  • CIE 空间:CalGray、CalRGB、Lab——需要 Function 计算。
  • ICCBased:解析 ICC profile 流(可能需要 iccUrl 的外部 ICC 数据)。
  • 特殊空间:Pattern(图案)、Indexed(索引色)、Separation(分色)、DeviceN(多分色)。
  • 命名空间:从 resources.ColorSpace 字典查找。

解析结果缓存到 localColorSpaceCache 和 globalColorSpaceCache,避免重复解析 ICC profile 等开销大的操作。

parseShading 方法

parseShading({ shading, resources, localColorSpaceCache, localShadingPatternCache })

解析 Shading Pattern(渐变图案),即 PDF 的 /Shading 字典(类型 1-7:函数渐变、轴向渐变、径向渐变、网格渐变等)。

核心流程:

  1. 缓存查询:localShadingPatternCache 按 shading 对象引用查 ID,命中则直接返回。
  2. 解析:Pattern.parseShading 把 shading 字典转成 ShadingPattern 对象,内部处理颜色空间、坐标变换、函数等。
  3. 生成 IR:shadingFill.getIR() 得到中间表示。
  4. 发送到主线程:
    • Type3 字形内:走 commonobj,ID 加 _type3_ 前缀,IR 编译成 ArrayBuffer 传输。
    • 普通页面:走 obj,带 pageIndex。
  5. 缓存:存入 localShadingPatternCache。

失败时如果 ignoreErrors,缓存 null 并返回 null(跳过该渐变)。

handleColorN 方法

handleColorN(operatorList, fn, args, cs, patterns, resources, task, localColorSpaceCache, localTilingPatternCache, localShadingPatternCache, seenRefs)

对应 SCN/scn(设置颜色,支持 Pattern)算子。args 的最后一个参数是 pattern 名称,前面是颜色分量。

逻辑:

  1. 取出 pattern 名:args.pop()。
  2. Tiling Pattern 缓存命中:如果该 pattern 已在 localTilingPatternCache,直接用缓存的 IR 生成 setFillPattern/setStrokePattern 算子。
  3. 否则解析 pattern:
    • PatternType.TILING(1)→ 调 handleTilingType。
    • PatternType.SHADING(2)→ 调 parseShading 得到渐变 ID,输出 pattern 算子。
  4. 颜色分量:如果 pattern 的底层颜色空间是 DeviceRGB 等,cs.base.getRgbHex(args, 0) 把颜色分量转成十六进制色值,传给图案 IR。

async parseMarkedContentProps 方法

async parseMarkedContentProps(contentProperties, resources) {
  return parseMarkedContentProps(this.xref, contentProperties, resources);
}

解析 Marked Content 的属性列表,即 BDC/BMC/DP 算子携带的属性字典。这些属性可以是:

  • 可选内容组(OCG):/OC 引用,控制内容的可见性。
  • 实际文本(ActualText):/ActualText,用于无障碍/文本提取,当视觉文本与逻辑文本不一致时(如连字)。
  • 结构元素:/StructParent/StructParents,关联到文档的逻辑结构树。

委托给独立的 parseMarkedContentProps 函数处理,因为逻辑较复杂且与 evaluator 状态无关。

async getOperatorList 方法

async getOperatorList({ stream, task, resources, operatorList, initialState, fallbackFontDict, prevRefs })

这是 PartialEvaluator 的核心方法——把一段 PDF 内容流(stream)解析成操作列表(operatorList)。页面内容流、Form XObject、Type3 字形、Tiling Pattern 都通过它解析。

准备阶段

  1. 异步流处理:如果 stream.isAsync(数据未完全到达),先 asyncGetBytes() 拿全量数据再包装成同步 Stream。
  2. 循环引用检测:seenRefs = new RefSet(prevRefs),继承父级的引用集合;如果当前流的 objId 已在集合中,抛错(XObject 互相引用)。
  3. 初始化本地缓存:每次调用新建 LocalImageCache、LocalColorSpaceCache、LocalGStateCache、LocalTilingPatternCache——这些是本次解析内的缓存,解析完即丢弃。
  4. 状态与预处理器:StateManager(图形状态栈)、EvaluatorPreprocessor(词法分析器,把字节流转成 {fn, args} 操作)。
  5. 时间片管理:TimeSlotManager 控制单次执行时长,避免 Worker 长时间阻塞。

主循环

while (!timeSlotManager.check()) {
  preprocessor.read(operation);   // 读一个算子
  switch (operation.fn) { ... }   // 分发处理
}

关键算子的处理分支:

算子处理方式
paintXObject (Do)查缓存 → Form XObject 走 buildFormXObject(异步),Image 走 buildPaintImageXObject(异步)
setFont (Tf)异步调 handleSetFont,输出 setFont [loadedName, size]
showText 系列 (Tj/TJ/'/")ensureStateFont → handleText 把字符转字形,直接输出
setFillColorSpace (cs)_getColorSpace + _handleColorSpace(异步解析 ICC 等)
setFillColor 系列 (sc/scn/g/rg/k)颜色空间转换 → 统一输出 setFillRGBColor [hex]
setGState (gs)异步调 setGState
路径算子 (m/l/c/re/…)buildPath 输出 constructPath
beginMarkedContent (BDC/BMC)parseMarkedContentProps,markedContentLevel++

异步算子的处理:XObject、字体、颜色空间、GState 等需要异步 I/O 的算子,通过 next(promise) 暂停循环,等异步完成后再继续 promiseBody(resolve, reject)。这是一个协作式的异步状态机——每次遇到异步算子就 return,Promise 完成后重新进入循环。

收尾

流读完或时间片耗尽时:

  1. closePendingMarkedContentOPS():补齐未关闭的 endMarkedContent。
  2. closePendingRestoreOPS():补齐未关闭的 restore(Q)。
  3. resolve():operatorList 填充完毕。

这两个「补齐」操作处理的是不规范 PDF——内容流里 q 没有配对的 Q、BDC 没有配对的 EMC,PDF.js 自动补全以保证渲染状态不泄漏。

async getTextContent 方法

async getTextContent({ stream, task, resources, stateManager, includeMarkedContent, sink, seenStyles, viewBox, lang, disableNormalization, keepWhiteSpace, prevRefs, intersector })

提取页面的文本内容,供文本选择、搜索、无障碍使用。与 getOperatorList 不同,它不输出绘图算子,而是输出文本项(TextItem)和样式(TextStyle)。

核心数据结构:

const textContent = { items: [], styles: Object.create(null), lang };
const textContentItem = { str: [], totalWidth, transform, fontName, hasEOL, ... };

处理流程:

  1. 初始化:异步流处理、循环引用检测、TextState 状态栈。
  2. 遍历算子:用 EvaluatorPreprocessor 读算子,但只处理文本相关的(Tf、Tj、Td、T*、Tm 等),忽略绘图算子。
  3. 字符→Unicode 映射:通过字体的 ToUnicode CMap 或编码表把字符编码转成 Unicode 字符串。
  4. 文本分块:按字体、变换矩阵、文本上升量等把连续字符聚合成 TextItem,记录 str(文本)、transform(位置/缩放矩阵)、width/height(边界)。
  5. 伪空格检测:用 twoLastChars 环形缓冲区和间距阈值判断是否在两个字符间插入空格(PDF 里文字间距常通过 TJ 的数字偏移实现,没有真正的空格字符)。
  6. 输出:通过 sink(文本接收器)流式输出 TextItem,支持 ReadableStream 逐块发送给主线程。

includeMarkedContent 为 true 时,还会提取 ActualText 等标记内容属性,用于无障碍文本提取。

async extractDataStructures 方法

async extractDataStructures(dict, properties)

从字体字典中提取字体的结构数据,是字体翻译流程的关键步骤。填充 properties 对象,供 translateFont 使用。

提取的内容:

数据来源说明
toUnicode字体字典的 /ToUnicode 流字符编码→Unicode 的 CMap,异步读取
cidSystemInfo/CIDSystemInfoCID 字体的注册机构/排序/补充号(Registry/Ordering/Supplement)
cidToGidMap/CIDToGIDMapCID→GlyphID 映射流,用于 CID 字体
baseEncodingName/Encoding(Name 或 Dict 的 BaseEncoding)基础编码名(WinAnsi/MacRoman 等)
differences/Encoding 的 /Differences 数组编码差异表,覆盖基础编码的部分字符映射

Differences 数组的解析是 PDF 编码机制的核心——它是一个 [number, name, name, ...] 交替的数组,数字表示起始码位,后面的名称依次映射到后续码位。例如 [65 /A /B /C] 表示 65→A, 66→B, 67→C。

_simpleFontToUnicode 方法

_simpleFontToUnicode(properties, forceGlyphs = false)

为简单字体(非 CID)构建字符编码→Unicode 的映射表,仅用字体的编码信息(不依赖 /ToUnicode 流)。

算法三步:

  1. 合并编码差异:以 defaultEncoding(基础编码表)为底,用 differences 数组覆盖对应码位的字形名。跳过 .notdef(否则空格会渲染成方框)。
  2. 查 Adobe Glyph List:对每个码位的字形名,在 glyphsUnicodeMap(AGL 表)中查到 Unicode 码点。
  3. 启发式回退:AGL 查不到时,按字形名格式猜测:
    • Gxx(3 位)/ g00xx(5 位):后两位是十六进制码点。
    • Cdd/cdd(3-4 位):十进制码点(forceGlyphs 时按十六进制)。
    • uniXXXX / uXXXX:直接是 Unicode 码点。

这是 PDF.js 比 Adobe Reader 更「宽容」的地方——Reader 只认 AGL,PDF.js 额外做了启发式猜测以改善文本提取。

async buildToUnicode 方法

async buildToUnicode(properties)

构建完整的 ToUnicode 映射,优先级从高到低:

  1. 字体自带 /ToUnicode 流:直接用。对简单字体额外生成 fallbackToUnicode(编码推断),用于补全不完整的 ToUnicode 数据。
  2. 简单字体无 ToUnicode:用 _simpleFontToUnicode 基于编码推断。
  3. CID 字体:用 registry-ordering-UCS2 CMap(如 Adobe-GB1-UCS2)做 CID→Unicode 映射。需要从内建 CMap 加载 UCS2 CMap。
  4. 最终兜底:IdentityToUnicodeMap,字符码直接当 Unicode 码点。

async readToUnicode 方法

async readToUnicode(cmapObj)

读取并解析 /ToUnicode 流。cmapObj 可以是:

  • Name:预定义 CMap 名(如 /Identity-H),通过 CMapFactory.create 创建。
  • Stream:内嵌的 CMap 数据,解析后返回 CMap 实例。
  • null:返回 null(无 ToUnicode)。

解析后的 CMap 提供 forEach(charcode, cid) 遍历能力,供 buildToUnicode 构建映射。

readCidToGidMap 方法

readCidToGidMap(glyphsData, toUnicode)

解析 /CIDToGIDMap 流——一个 UInt16BE 数组,下标是 CID,值是 GlyphID。每两个字节读一个 GID。

优化:GID 为 0 且该 CID 在 toUnicode 中没有映射时跳过(.notdef 字形)。返回 result[code] = glyphID 数组。

extractWidths 方法

extractWidths(dict, descriptor, properties)

从字体字典提取字形宽度和垂直度量。

  • 简单字体:/Widths 数组 + /FirstChar 确定范围,未指定的用 descriptor.MissingWidth 或 0。
  • CID 字体:/W 数组,格式为 [startCID [w1 w2 ...] | startCID endCID width] 交替。默认宽度 /DW(缺省 1000)。
  • 垂直书写:/W2 和 /DW2 提供垂直度量 [w, x, y]。

isSerifFont 方法

isSerifFont(baseFontName)

根据字体名的子串判断是否为衬线字体。匹配 Times、Garamond、Georgia、Palatino 等关键词返回 true,否则 false。用于无 FontDescriptor 时猜测字体风格。

getBaseFontMetrics 方法

getBaseFontMetrics(name)

获取标准 14 字体的度量数据(Metrics 对象,含 widths 字形宽度表、stemV 等)。如果不是标准 14 字体,按衬线/无衬线回退到 Times-Roman 或 Helvetica 的度量。

buildCharCodeToWidth 方法

buildCharCodeToWidth(widthsByGlyphName, properties)

把「按字形名索引的宽度表」转成「按字符码索引的宽度数组」。利用编码表把每个字符码映射到字形名,再查宽度。用于标准字体的宽度推断。

preEvaluateFont 方法

preEvaluateFont(dict)

字体翻译的第一步——从字体字典提取基础信息,返回 preEvaluatedFont 对象供 translateFont 使用。

提取内容:

  • Subtype:字体类型;如果是 Type0,取 DescendantFonts[0] 的 Subtype(CIDFontType0/CIDFontType2)。
  • composite:是否为复合字体。
  • FirstChar/LastChar:字符码范围(CID 字体默认 0-0xFFFF)。
  • FontDescriptor:字体描述符。
  • ToUnicode:ToUnicode 流引用。
  • hash:基于 FontDescriptor + Encoding 计算的 MurmurHash3 64 位哈希,用于字体别名去重。
  • defaultEncoding:根据字体名/Subtype 选择的默认编码(StandardEncoding/WinAnsi 等)。

translateFont 方法

async translateFont(preEvaluatedFont)

字体翻译的核心——把 PDF 字体字典转成可渲染的 Font 对象。流程:

  1. extractDataStructures 提取 ToUnicode、CIDSystemInfo、编码、宽度。
  2. 根据 Subtype 选择字体类:
    • Type1/MMType1 → Type1Font(解析 FontFile/FontFile3 的 CFF/Type1 数据)。
    • TrueType → TrueTypeFont(解析 FontFile2 的 TrueType 数据)。
    • Type3 → Type3Font(特殊处理,字形流由 loadType3Data 解析)。
    • CIDFontType0 → CFFFont。
    • CIDFontType2 → TrueTypeFont。
  3. 无嵌入字体时,用 getBaseFontMetrics 标准度量或系统字体度量。
  4. 构建 toUnicode 映射和宽度表。
  5. 返回 Font 实例,封装为 TranslatedFont。

static buildFontPaths 方法

static buildFontPaths(font, glyphs, handler, evaluatorOptions)

把需要以路径方式渲染的字形轮廓预编译并发送到主线程。用于 textRenderingMode 含 ADD_TO_PATH_FLAG、Pattern 颜色、或 disableFontFace 的场景。

对每个 glyph,调 font.renderer.getPath(fontChar) 得到路径指令的 ArrayBuffer,通过 commonobj 发送(文档级缓存)。带重音符号的字形额外编译重音字符的路径。

static get fallbackFontDict 方法

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);
}

返回一个兜底字体字典——当字体引用无效且 ignoreErrors 时,用 Helvetica + WinAnsiEncoding 兜底,保证文本至少能显示(虽然可能不正确)。用 shadow 缓存为类属性,只创建一次。

TranslatedFont 类

TranslatedFont 是字体翻译后的封装对象,持有字体名、字体实例、原始字典,并负责把字体发送到主线程。Type3 字体还额外管理字形流的解析。

constructor 方法

constructor({ loadedName, font, dict }) {
  this.loadedName = loadedName;
  this.font = font;
  this.dict = dict;
  this.type3Dependencies = font.isType3Font ? new Set() : null;
}
  • loadedName:主线程注册 @font-face 用的字体名(${docId}_${fontID})。
  • font:翻译后的 Font 实例(含度量、字形映射、渲染器)。
  • dict:原始字体字典。
  • type3Dependencies:Type3 字体的字形流中引用的图片/Pattern 依赖集合,需加入父 operatorList 的依赖。

send 方法

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);
}

把字体数据通过 commonobj 发送到主线程(文档级共享)。#sent 标志保证只发一次。transfers 把字体二进制数据以 Transferable 方式零拷贝传输。

fallback 方法

fallback(handler, evaluatorOptions) {
  if (!this.font.data) return;
  this.font.disableFontFace = true;
  PartialEvaluator.buildFontPaths(this.font, this.font.glyphCacheValues, handler, evaluatorOptions);
}

当主线程字体加载失败(@font-face 注册失败)时回退:把 disableFontFace 设为 true,改为用路径方式渲染字形。重新发送所有已解析字形的路径数据到主线程,保证后续文本能正确渲染。

async loadType3Data 方法

async loadType3Data(evaluator, resources, task, seenRefs)

Type3 字体的核心方法——解析所有字形流。流程:

  1. 克隆 evaluator:evaluator.clone({ ignoreErrors: false }),设置 type3FontRefs 防循环。
  2. 遍历 CharProcs:对每个字形流调 getOperatorList 解析。
  3. 处理 d0/d1 算子:
    • d1(setCharWidthAndBounds):调 #removeType3ColorOperators 移除颜色算子(规范要求 d1 开头的字形不能设置颜色)。
    • d0(setCharWidth):如果字体无 BBox,调 #guessType3FontBBox 从操作列表推断。
  4. 收集依赖:把字形流中的图片/Pattern 依赖加入 type3Dependencies。
  5. 存储:font.charProcOperatorList 保存每个字形名→IR 的映射。

用 #type3Loaded Promise 缓存,避免重复解析。

#removeType3ColorOperators 方法

#removeType3ColorOperators(operatorList, fontBBoxSize)

按 PDF 规范 §9.6.5,以 d1 开头的 Type3 字形不能设置颜色。此方法遍历 operatorList,移除所有颜色相关算子(setFillColor、setStrokeColor、setColorSpace、shadingFill、setRenderingIntent 等),以及 GState 中的颜色条目。

同时处理 d1 的边界框:如果 charBBox 比 fontBBox 大一个数量级,用 charBBox 更新字体 BBox(修复坏文件)。

#guessType3FontBBox 方法

#guessType3FontBBox(operatorList)

当 Type3 字体字典没有 FontBBox 且字形以 d0(无边界框)开头时,从操作列表的路径操作中推断字形边界框,确保裁剪正确。

StateManager 类

constructor 方法

constructor(initialState = new EvalState()) {
  this.state = initialState;
  this.stateStack = [];
}

图形状态栈管理器,对应 PDF 的 q(save)和 Q(restore)算子。

save 方法

save() {
  this.stateStack.push(this.state);
  this.state = old.clone();
}

把当前状态压栈,然后克隆一份作为新状态。后续修改不影响栈中保存的状态。

restore 方法

restore() {
  const prev = this.stateStack.pop();
  if (prev) this.state = prev;
}

弹出栈顶状态恢复。栈为空时不操作(容错)。

transform 方法

transform(args) {
  this.state.ctm = Util.transform(this.state.ctm, args);
}

对应 cm 算子——把变换矩阵 args 左乘到当前 CTM(Current Transformation Matrix)上。

TextState 类

constructor 方法

TextState 是文本状态对象,在 getTextContent 中使用(区别于 EvalState 用于 getOperatorList)。字段包括:

  • ctm:当前变换矩阵。
  • fontName/fontSize/loadedName/font:当前字体信息。
  • fontMatrix:字体空间→文本空间的变换矩阵。
  • textMatrix/textLineMatrix:文本矩阵和行矩阵。
  • charSpacing/wordSpacing/leading/textHScale/textRise:文本参数。

setTextMatrix / setTextLineMatrix 方法

对应 Tm 算子,直接设置文本矩阵/行矩阵的 6 个元素。

translateTextMatrix / translateTextLineMatrix 方法

对应 Td/TD 算子,在当前文本矩阵基础上做平移:e' = a*x + c*y + e,f' = b*x + d*y + f。

carriageReturn 方法

对应 T*(换行)算子:行矩阵向下平移 leading,文本矩阵复制行矩阵。

clone 方法

浅拷贝所有字段,但 textMatrix/textLineMatrix/fontMatrix 三个数组深拷贝(避免修改影响原对象)。

EvalState 类

constructor 方法

EvalState 是 getOperatorList 使用的图形状态对象。关键字段:

  • ctm:当前变换矩阵。
  • font:当前字体。
  • textRenderingMode:文本渲染模式(填充/描边/填充+描边/不可见/添加到路径)。
  • _fillColorSpace/_strokeColorSpace:填充/描边颜色空间。
  • patternFillColorSpace/patternStrokeColorSpace:Pattern 颜色空间。
  • currentPointX/currentPointY/pathMinMax/pathBuffer:路径构建状态。

get/set fillColorSpace 方法

setter 同时设置 _fillColorSpace 和 patternFillColorSpace——因为设置颜色空间时,若该空间是 Pattern,后续 SCN 才知道用哪个 pattern 空间。

get/set strokeColorSpace 方法

同上,针对描边颜色空间。

clone 方法

clone({ newPath = false } = {}) {
  const clone = Object.create(this);
  if (newPath) {
    clone.pathBuffer = [];
    clone.pathMinMax = F32_BBOX_INIT.slice();
  }
  return clone;
}

用 Object.create(this) 浅拷贝。newPath: true 时重置路径相关字段——用于 Form XObject 等子内容流,避免路径状态泄漏。

EvaluatorPreprocessor 类

EvaluatorPreprocessor 是 PDF 内容流的词法分析器 + 算子分发器,把字节流转成 {fn, args} 操作供 getOperatorList 处理。

constructor 方法

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:PDF 解析器,从流中读 token。
  • nonProcessedArgs:参数队列,处理 PostScript 风格的嵌套命令(如 /F2 /GS2 gs 5.711 Tf 中 gs 只取前面的 /GS2,/F2 留给 Tf)。

static get opMap 方法

算子映射表——把 PDF 算子名(如 m、l、Tj、Do)映射到 {id, numArgs, variableArgs}。

  • id:内部算子 ID(OPS.*)。
  • numArgs:参数个数。
  • variableArgs:是否可变参数(如 TJ 接受数组)。

还包含 BM/BD/true/false/null 等保留字占位,避免被误判为命令。

static MAX_INVALID_PATH_OPS 值

10——路径操作连续失败的最大容忍次数,超过则认为流损坏,停止解析路径。

get savedStatesDepth 方法

返回 stateManager.stateStack.length,即当前 q 的嵌套深度。getOperatorList 收尾时用它补齐未匹配的 Q。

read 方法

read(operation) // → boolean

从流中读取一个算子,填入 operation.fn 和 operation.args。返回 false 表示流结束。

核心逻辑:

  1. parser.getObj() 读 token,直到遇到 Cmd(命令)。
  2. 查 opMap 得到 fn 和参数要求。
  3. 参数对齐:PostScript 命令可嵌套,用 nonProcessedArgs 队列调整参数数量,使 args.length === numArgs。
  4. 路径完整性检查:路径操作(m/l/c 等)参数不足时,计入 _numInvalidPathOPS,超过阈值则丢弃。

operation.args 传 null 时内部创建数组(零参数时保持 null 避免分配),这是性能优化——read 是热点路径。

preprocessCommand 方法

对部分命令做预处理(如把 re 展开成 m/l/l/l/h 路径序列),简化后续处理。是 read 内部调用的辅助方法。

总结

PartialEvaluator 是 PDF.js Worker 端的内容流解释器,它的职责是把 PDF 内容流(字节序列)转换成操作列表(OperatorList)——一串与渲染无关的高层绘图指令,供主线程 CanvasGraphics 执行。

核心数据流

PDF 内容流 (字节)
    ↓  EvaluatorPreprocessor (词法分析)
{fn, args} 操作序列
    ↓  getOperatorList (分发处理)
OperatorList (OPS 算子 + 参数)
    ↓  MessageHandler (obj/commonobj)
主线程 CanvasGraphics 执行渲染

四大子系统

子系统关键方法职责
词法与状态EvaluatorPreprocessor.read、StateManager、EvalState把字节流切成算子,维护图形状态栈
资源解析buildFormXObject、buildPaintImageXObject、setGState、handleTilingType、parseShading解析 XObject、图片、GState、Pattern
字体管线loadFont → preEvaluateFont → translateFont → extractDataStructures → buildToUnicode字体字典→可渲染字体对象,含 ToUnicode、宽度、嵌入数据
文本提取getTextContent独立于渲染的文本项提取,用于选择/搜索/无障碍

关键设计原则

  1. 求值≠渲染。Partial 的含义是「只解释,不绘制」——所有渲染决策(颜色空间转换、字形轮廓)尽量在 Worker 完成,主线程只做 canvas 绘制,保持主线程轻量。

  2. 三级缓存。全局缓存(fontCache/globalImageCache 等,文档级共享)→ 区域缓存(_regionalImageCache,页级)→ 本地缓存(localImageCache 等,单次解析)。逐层缩小范围,兼顾复用与隔离。

  3. 协作式异步。getOperatorList 用 next(promise) 实现状态机式的异步处理——遇到 XObject/字体等需要 I/O 的算子就暂停,Promise 完成后继续,避免阻塞 Worker。

  4. 容错优先。循环引用检测(seenRefs/type3FontRefs)、ignoreErrors 降级、fallbackFontDict 兜底、closePendingRestoreOPS 补齐状态——对不规范 PDF 极度宽容,「能打开就打开」。

  5. Type3 特殊处理。Type3 字形流本身就是 PDF 内容流,通过 clone 子 evaluator 递归解析,同时切换对象 ID 前缀和发送通道(commonobj),并用 type3FontRefs 防循环。

至此,operatorList 的生成链路完整闭合。下一篇将进入主线程,看 CanvasGraphics 如何消费这份操作列表、真正把 PDF 画到 canvas 上。

StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.

相关文章