PDF.js 深入浅出 7: PartialEvaluator
2026年10月25日 · StreamPDF Team

前言
这是 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 等)。 |
handler | MessageHandler,Worker 与主线程通信的句柄。当 useWorkerFetch 为 false 时,CMap 与标准字体数据需通过 handler.sendWithPromise("FetchBinaryData", …) 回主线程拉取。 |
pageIndex | 当前页索引,用于标识对象归属。-1 表示全局上下文(仅 XFA 字体加载)。图片/字体 ID 会带上页号前缀。 |
idFactory | ID 工厂,为发往主线程的对象(字体 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 选项,其余全部继承。这样做的好处:
- 轻量——不复制缓存引用,原型链共享。
- 隔离——新 evaluator 可以设置自己的
type3FontRefs等实例字段,不影响父级。 - 选项可覆盖——通过
Object.assign合并,newOptions优先级最高。
hasBlendModes 方法
hasBlendModes(resources, nonBlendModesSet)判断一组 Resources 中是否存在非 Normal 的混合模式(Blend Mode)。这是一个预检优化:在渲染组(Transparency Group)之前调用,如果没有混合模式,主线程 CanvasGraphics 可以跳过透明度合成的开销。
算法是一个带环检测的图遍历:
- 从传入的
resources开始,BFS 遍历。 - 对每个节点,检查
ExtGState字典里每个图形状态的/BM(Blend Mode):/BM是单个Name且不是Normal→ 返回true。/BM是数组(软遮罩的备用模式列表)→ 任一非Normal即返回true。
- 递归进入
XObject字典中的每个 Form/Image XObject 的Resources,继续检查。 - 用
processed(RefSet)记录已访问的对象引用,防止循环引用和重复解析。 - 如果遍历完没发现混合模式,把所有处理过的引用加入
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 中可复用的独立内容流,相当于「页面中的小页面」。
核心流程:
- 读取变换矩阵与边界框:
Matrix(xobj → 用户空间的变换)和BBox(裁剪框)。 - 可选内容(OC):如果 xobj 有
/OC字典(可选内容组),先解析并插入beginMarkedContentProps算子,渲染时可控制可见性。 - 判断是否为透明组(Group):
- 有
/Group且/S /Transparency→ 需要在独立的离屏 canvas 上渲染(CheckedOperatorList),处理isolated、knockout、颜色空间、软遮罩背景色(backdrop)等属性。 - 无 Group → 直接往当前
operatorList追加,用paintFormXObjectBegin/End包裹。
- 有
- 递归解析内容流:调
getOperatorList解析 xobj 的流,使用 xobj 自身的Resources(没有则继承外部resources)。 - 组装算子序列:
- 有 Group:
beginGroup → paintFormXObjectBegin → [子内容] → paintFormXObjectEnd → endGroup。 - 无 Group:
paintFormXObjectBegin → [子内容] → paintFormXObjectEnd。
- 有 Group:
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(通常是灰度图),其像素值决定被遮罩对象的不透明度。
核心逻辑:
- 读取
/S(子类型:Alpha或Luminosity)和/BC(背景色)。 - 读取
/TR(转移函数),如果是 PDF Function,调用#createTransferMap预计算成 256 项 LUT。 - 复用
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:恒等映射,不做变换。
处理逻辑:
- 如果数组里所有函数引用相同,只保留一个(优化)。
- 对每个条目:
/Identity→null(跳过);PDF Function →#createTransferMap生成 LUT。 - 校验:只支持 1 或 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 等)。
核心流程:
- 合并 Resources:
Dict.merge([patternDict.Resources, resources]),防止图案字典缺少部分资源条目(issue6541.pdf 的修复)。 - 解析图案内容流:用
CheckedOperatorList调getOperatorList,得到图案的操作列表。 - 生成 IR:
getTilingPatternIR把操作列表转成中间表示,包含平铺参数和颜色信息。 - 发送算子:
operatorList.addOp(fn, tilingPatternIR),fn是setFillPattern或setStrokePattern。 - 缓存:把 IR 存入
localTilingPatternCache(按图案对象引用),同一图案复用。
CheckedOperatorList 会检测图案内是否需要隔离(needsIsolation),影响主线程渲染策略。
async handleSetFont 方法
async handleSetFont(resources, fontArgs, fontRef, operatorList, task, state, fallbackFontDict, cssFontInfo, seenRefs)对应 PDF 的 Tf(set font)算子。做三件事:
- 加载字体:调
loadFont得到TranslatedFont(已翻译的字体对象,包含度量、字形映射、字体数据等)。 - 设置当前字体:
state.font = translated.font,后续文本算子使用此字体。 - 发送字体到主线程:
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)按名称或引用加载字体,是字体管线的入口。核心流程:
- 解析引用:
font是Ref时直接用;否则从resources.Font按名查引用。 - 循环检测:如果
fontRef已在type3FontRefs中(Type3 递归引用),返回errorFont。 - 缓存命中:
fontCache里已有则直接返回。 - 取字体字典:
xref.fetchIfRef(fontRef)解析字典。 - 降级:如果不是有效字典,
ignoreErrors模式下用fallbackFontDict或fallbackFontDict(标准 Helvetica)兜底。 - 预评估:
preEvaluateFont(font)提取字体的基础信息(Subtype、BaseFont、编码、描述符等)。 - 字体 ID 与别名:
- 如果字体有
FontDescriptor且能计算出hash,用fontAliases[hash]做别名去重——多个字体字典引用同一份字体数据时共享一个fontID。 - 否则
idFactory.createFontId()生成新 ID。
- 如果字体有
- 写入缓存:
fontCache.put(fontRef, promise),缓存的是 Promise(异步翻译中)。 - 异步翻译:
translateFont(preEvaluatedFont)得到translatedFont,如果是 Type3 字体再loadType3Data解析字形流。 - 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:函数渐变、轴向渐变、径向渐变、网格渐变等)。
核心流程:
- 缓存查询:
localShadingPatternCache按 shading 对象引用查 ID,命中则直接返回。 - 解析:
Pattern.parseShading把 shading 字典转成ShadingPattern对象,内部处理颜色空间、坐标变换、函数等。 - 生成 IR:
shadingFill.getIR()得到中间表示。 - 发送到主线程:
- Type3 字形内:走
commonobj,ID 加_type3_前缀,IR 编译成ArrayBuffer传输。 - 普通页面:走
obj,带pageIndex。
- Type3 字形内:走
- 缓存:存入
localShadingPatternCache。
失败时如果 ignoreErrors,缓存 null 并返回 null(跳过该渐变)。
handleColorN 方法
handleColorN(operatorList, fn, args, cs, patterns, resources, task, localColorSpaceCache, localTilingPatternCache, localShadingPatternCache, seenRefs)对应 SCN/scn(设置颜色,支持 Pattern)算子。args 的最后一个参数是 pattern 名称,前面是颜色分量。
逻辑:
- 取出 pattern 名:
args.pop()。 - Tiling Pattern 缓存命中:如果该 pattern 已在
localTilingPatternCache,直接用缓存的 IR 生成setFillPattern/setStrokePattern算子。 - 否则解析 pattern:
PatternType.TILING(1)→ 调handleTilingType。PatternType.SHADING(2)→ 调parseShading得到渐变 ID,输出 pattern 算子。
- 颜色分量:如果 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 都通过它解析。
准备阶段
- 异步流处理:如果
stream.isAsync(数据未完全到达),先asyncGetBytes()拿全量数据再包装成同步Stream。 - 循环引用检测:
seenRefs = new RefSet(prevRefs),继承父级的引用集合;如果当前流的objId已在集合中,抛错(XObject 互相引用)。 - 初始化本地缓存:每次调用新建
LocalImageCache、LocalColorSpaceCache、LocalGStateCache、LocalTilingPatternCache——这些是本次解析内的缓存,解析完即丢弃。 - 状态与预处理器:
StateManager(图形状态栈)、EvaluatorPreprocessor(词法分析器,把字节流转成{fn, args}操作)。 - 时间片管理:
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 完成后重新进入循环。
收尾
流读完或时间片耗尽时:
closePendingMarkedContentOPS():补齐未关闭的endMarkedContent。closePendingRestoreOPS():补齐未关闭的restore(Q)。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, ... };处理流程:
- 初始化:异步流处理、循环引用检测、
TextState状态栈。 - 遍历算子:用
EvaluatorPreprocessor读算子,但只处理文本相关的(Tf、Tj、Td、T*、Tm等),忽略绘图算子。 - 字符→Unicode 映射:通过字体的
ToUnicodeCMap 或编码表把字符编码转成 Unicode 字符串。 - 文本分块:按字体、变换矩阵、文本上升量等把连续字符聚合成
TextItem,记录str(文本)、transform(位置/缩放矩阵)、width/height(边界)。 - 伪空格检测:用
twoLastChars环形缓冲区和间距阈值判断是否在两个字符间插入空格(PDF 里文字间距常通过TJ的数字偏移实现,没有真正的空格字符)。 - 输出:通过
sink(文本接收器)流式输出TextItem,支持ReadableStream逐块发送给主线程。
includeMarkedContent 为 true 时,还会提取 ActualText 等标记内容属性,用于无障碍文本提取。
async extractDataStructures 方法
async extractDataStructures(dict, properties)从字体字典中提取字体的结构数据,是字体翻译流程的关键步骤。填充 properties 对象,供 translateFont 使用。
提取的内容:
| 数据 | 来源 | 说明 |
|---|---|---|
toUnicode | 字体字典的 /ToUnicode 流 | 字符编码→Unicode 的 CMap,异步读取 |
cidSystemInfo | /CIDSystemInfo | CID 字体的注册机构/排序/补充号(Registry/Ordering/Supplement) |
cidToGidMap | /CIDToGIDMap | CID→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 流)。
算法三步:
- 合并编码差异:以
defaultEncoding(基础编码表)为底,用differences数组覆盖对应码位的字形名。跳过.notdef(否则空格会渲染成方框)。 - 查 Adobe Glyph List:对每个码位的字形名,在
glyphsUnicodeMap(AGL 表)中查到 Unicode 码点。 - 启发式回退: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 映射,优先级从高到低:
- 字体自带
/ToUnicode流:直接用。对简单字体额外生成fallbackToUnicode(编码推断),用于补全不完整的 ToUnicode 数据。 - 简单字体无 ToUnicode:用
_simpleFontToUnicode基于编码推断。 - CID 字体:用
registry-ordering-UCS2CMap(如Adobe-GB1-UCS2)做 CID→Unicode 映射。需要从内建 CMap 加载 UCS2 CMap。 - 最终兜底:
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 对象。流程:
extractDataStructures提取 ToUnicode、CIDSystemInfo、编码、宽度。- 根据 Subtype 选择字体类:
- Type1/MMType1 →
Type1Font(解析 FontFile/FontFile3 的 CFF/Type1 数据)。 - TrueType →
TrueTypeFont(解析 FontFile2 的 TrueType 数据)。 - Type3 →
Type3Font(特殊处理,字形流由loadType3Data解析)。 - CIDFontType0 →
CFFFont。 - CIDFontType2 →
TrueTypeFont。
- Type1/MMType1 →
- 无嵌入字体时,用
getBaseFontMetrics标准度量或系统字体度量。 - 构建
toUnicode映射和宽度表。 - 返回
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 字体的核心方法——解析所有字形流。流程:
- 克隆 evaluator:
evaluator.clone({ ignoreErrors: false }),设置type3FontRefs防循环。 - 遍历
CharProcs:对每个字形流调getOperatorList解析。 - 处理
d0/d1算子:d1(setCharWidthAndBounds):调#removeType3ColorOperators移除颜色算子(规范要求 d1 开头的字形不能设置颜色)。d0(setCharWidth):如果字体无 BBox,调#guessType3FontBBox从操作列表推断。
- 收集依赖:把字形流中的图片/Pattern 依赖加入
type3Dependencies。 - 存储:
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 表示流结束。
核心逻辑:
parser.getObj()读 token,直到遇到Cmd(命令)。- 查
opMap得到fn和参数要求。 - 参数对齐:PostScript 命令可嵌套,用
nonProcessedArgs队列调整参数数量,使args.length === numArgs。 - 路径完整性检查:路径操作(
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 | 独立于渲染的文本项提取,用于选择/搜索/无障碍 |
关键设计原则
-
求值≠渲染。
Partial的含义是「只解释,不绘制」——所有渲染决策(颜色空间转换、字形轮廓)尽量在 Worker 完成,主线程只做 canvas 绘制,保持主线程轻量。 -
三级缓存。全局缓存(
fontCache/globalImageCache等,文档级共享)→ 区域缓存(_regionalImageCache,页级)→ 本地缓存(localImageCache等,单次解析)。逐层缩小范围,兼顾复用与隔离。 -
协作式异步。
getOperatorList用next(promise)实现状态机式的异步处理——遇到 XObject/字体等需要 I/O 的算子就暂停,Promise 完成后继续,避免阻塞 Worker。 -
容错优先。循环引用检测(
seenRefs/type3FontRefs)、ignoreErrors降级、fallbackFontDict兜底、closePendingRestoreOPS补齐状态——对不规范 PDF 极度宽容,「能打开就打开」。 -
Type3 特殊处理。Type3 字形流本身就是 PDF 内容流,通过
clone子 evaluator 递归解析,同时切换对象 ID 前缀和发送通道(commonobj),并用type3FontRefs防循环。
至此,operatorList 的生成链路完整闭合。下一篇将进入主线程,看 CanvasGraphics 如何消费这份操作列表、真正把 PDF 画到 canvas 上。
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- PDF.js 深入浅出 9: CanvasGraphics2026年10月26日
- PDF.js 深入浅出 8: Document 与 Page2026年10月26日
- AI 时代, 我们到底应该做什么才能活下去(StreamPDF 创立有感)2026年10月26日
- PDF.js 深入浅出 6: PDFManager.initDocument2026年10月25日
- PDF.js 深入浅出 5: PDFFetchStream 和 PDFWorkerStream2026年10月25日
- PDF.js 深入浅出 4: MessageHandler2026年10月22日
- PDF.js 深入浅出 3: getViewport & render2026年10月22日
- PDF.js 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日