StreamPDFStreamPDF
返回博客

PDF.js 源码解读: 共享工具层

2026年10月27日 · StreamPDF Team

PDF.js 源码解读: 共享工具层

前言

本篇分析 src/shared/util.js。这个 1229 行的模块被广泛引入解析器、渲染器、注释系统和编辑器。

乍看之下,它很像一个杂项工具包,但这种理解会忽略它真正的职责:为彼此独立的 PDF.js 子系统定义统一的底层词汇与契约。PDF 数值标志、操作符编号、异常结构、二进制字符串转换、浏览器能力探测和几何计算,都需要唯一且一致的解释。如果 worker 与 display 层各自复制一份,极易产生难以察觉的不兼容。

这个模块还横跨两类边界:一类 helper 负责解释 PDF 数据,必须面对畸形输入和规范定义的数值;另一类负责适配 JavaScript 宿主,因为 Node.js、浏览器、worker 与转译构建提供的能力并不相同。按功能族阅读,才能看清这些边界及其背后的设计取舍。

模块总览

源码可以划分为八个实现区块,外加导出表:

区块行号职责
环境检测与共享常量21-384宿主分类、PDF 标志、注释数值、操作码与日志级别
诊断工具386-426verbosity 状态、info、warn、unreachable 与 assert
URL、路径与属性定义428-519协议白名单、hash 更新、路径截取与 shadow
异常与字节字符串524-616类型化失败信息与二进制字符串转换
FeatureTest618-718惰性执行并缓存结果的宿主能力探测
Util720-1028颜色、仿射变换、缩放恢复、矩形与 Bézier 边界
文本、身份与目标1030-1127UTF-8 字节字符串、Unicode 规范化、UUID 与显式目标校验
工厂与兼容补丁1129-1164可复用集合工厂及小型标准库 shim
导出表1166-1229导出共享常量、类、helper 与工厂

阅读时可以始终抓住两个主题:

  1. 常量同样可以是兼容接口。 给 OPS 重新编号,与直接修改函数签名一样,都可能破坏调用方。
  2. 可变性是刻意设计,而非偶然行为。 多个热路径方法直接写入调用方提供的数组以避免分配;另一些方法则明确返回副本。这个区别本身就是 API 契约。

常量不只是标签,更是契约

渲染意图与注释模式

RenderingIntentFlag 使用彼此独立的 2 的幂,从 ANY 一直到 OPLIST,因此可以在一个整数里同时携带多项事实:display 还是 print、如何处理注释、是否正在编辑、是否保存,以及是否生成操作符列表。

AnnotationMode 则是一组互斥的数值选项:DISABLE、ENABLE、ENABLE_FORMS 与 ENABLE_STORAGE,它不是另一套位掩码。display/api.js 中的 WorkerTransport.getRenderingIntent 会把所选模式翻译成至多一个注释标志,再按需加入编辑和操作符列表标志:

switch (annotationMode) {
  case AnnotationMode.DISABLE:
    renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE;
    break;
  case AnnotationMode.ENABLE:
    break;
  case AnnotationMode.ENABLE_FORMS:
    renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS;
    break;
  case AnnotationMode.ENABLE_STORAGE:
    renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
 
    annotationStorageSerializable = annotationStorage.serializable;
    break;
  default:
    warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
}
 
if (isEditing) {
  renderingIntent += RenderingIntentFlag.IS_EDITING;
}
if (isOpList) {
  renderingIntent += RenderingIntentFlag.OPLIST;
}

源码使用的是 +=;这里之所以安全,是因为每个参与累加的标志都不同,而且至多选择一次。消费方再通过位运算取回各项事实。例如,OptionalContentConfig 检查 DISPLAY 与 PRINT,PDFPageProxy.render 则根据 PRINT 选择打印专属行为。这个整数还会参与渲染缓存 key 的组成,因此这些位同时影响行为与缓存身份。

其余常量族承担着不同的契约职责:

常量族契约
PermissionFlag、AnnotationFlag、AnnotationFieldFlag文档权限、注释行为与表单域的位掩码;权限值遵循 ISO 32000-1 Table 22
AnnotationType、AnnotationBorderStyleType、AnnotationReplyType、AnnotationRenditionOperation解释注释字典和 rendition 动作时使用的数值或字符串词汇
AnnotationActionEventType、DocumentActionEventType、PageActionEventType将 E、WC、O 等 PDF 触发键映射为脚本事件名
AnnotationPrefix、AnnotationEditorPrefix内部注释/编辑器标识符与编辑器存储键的命名空间
AnnotationEditorType、AnnotationEditorParamsType内部编辑器类型与参数更新命令,不应把它们误认为同一个 PDF 规范枚举
TextRenderingModePDF 文本模式 0 至 7,以及用于解码组合模式的 FILL_STROKE_MASK 与 ADD_TO_PATH_FLAG
ImageKind、MeshFigureType解析与渲染共享的图像存储布局和 mesh 图形类别
PasswordResponses、VerbosityLevel密码请求结果与诊断输出阈值

TextRenderingMode 很能说明为什么不能只复制数值而不理解结构。模式 4 至 7 将低位的填充/描边行为与“加入路径”组合起来。因此 canvas 渲染器用 mode & FILL_STROKE_MASK 取得基础操作,再用 mode & ADD_TO_PATH_FLAG 判断裁剪路径行为;如果只写一长串相等判断,就会丢掉这层结构。

稳定的操作码

OPS 为操作符列表中的命令分配数值 ID。源码中的两条注释明确了它的兼容规则:

  • 编号从 1 开始,让未初始化的零值很容易识别;
  • 已删除的条目仍以注释保留,因为重新编号可能破坏第三方用户。

所以列表在 78、79 和 82 处留有空洞,这是刻意为之。worker 代码将数值函数 ID 写入操作符列表,canvas.js 中的 display 代码再按相同 ID 分派;这些数字属于数据格式,而不只是 switch 标签。

DrawOPS 的要求不同。它的五个路径命令连续占用 0 至 4,源码明确说明这样有利于 switch 使用快速跳转表。display/display_utils.js 中的 makePathFromDrawOPS 消费这种紧凑表示。OPS 重视编号稳定,DrawOPS 重视编号连续。

共享数值常量

其余常量防止细微的策略在不同模块间逐渐分叉:

  • FONT_IDENTITY_MATRIX 是 [0.001, 0, 0, 0.001, 0, 0],表示字体矩阵回退时通常使用的 1/1000 字形单位缩放。
  • LINE_FACTOR = 1.35 与 LINE_DESCENT_FACTOR = 0.35 记录 Acrobat 似乎采用的单行域度量。BASELINE_FACTOR 由二者之比推导,不再重复写一个魔法数字。
  • BBOX_INIT 与 F32_BBOX_INIT 分别提供普通数组和 Float32Array 形式的空边界累加模板。
  • SVG_NS 集中定义 DOM/SVG 构建使用的 SVG 命名空间。
  • Util.hexNums 惰性创建全部 256 个两位十六进制字节值。Util.makeHexColor(r, g, b) 直接索引这张表,因此输入应当是字节值;它不负责夹取范围或校验。

这些定义虽小,却让字体渲染、注释外观、编辑器行布局、SVG 生成与颜色序列化共享同一套策略。

诊断与异常层级

模块内部持有一个可变的诊断状态,初始值为 VerbosityLevel.WARNINGS:

let verbosity = VerbosityLevel.WARNINGS;
 
function setVerbosityLevel(level) {
  if (Number.isInteger(level)) {
    verbosity = level;
  }
}
 
function getVerbosityLevel() {
  return verbosity;
}

setter 接受任意整数,而不只限于三个命名级别。这与阈值式输出一致:verbosity >= WARNINGS 时 warn 才输出,info 则要求 verbosity >= INFOS。warn 明确表示非致命警告;unreachable 则始终抛出 Error,assert 在条件失败时也经由它抛错。它们用于发现程序逻辑失败,不能替代可恢复的 PDF 解析错误。

BaseException 为 PDF.js 异常提供统一的 message/name 结构。它通过较传统的 prototype 设置继承 Error,在未构建或测试代码中还禁止直接实例化 BaseException 本身。派生类补充调用方作出决策所需的信息:

异常附加状态类型表达的语义
PasswordExceptioncode需要密码或密码错误,代码由 PasswordResponses 定义
UnknownErrorExceptiondetails无法归类的失败及诊断细节
InvalidPDFException—文档不是有效 PDF
ResponseExceptionstatus、missing网络响应失败,并携带状态码与资源是否缺失的信息
FormatError—PDF 数据未按预期解析
AbortException—任务被取消,而不是普通执行失败

这套类型会跨越 worker 边界使用。message_handler.js 根据 name 重建可识别的异常,并保留 code、status、missing、details 等字段。因此,这些构造函数也是协议定义:即使本地代码仍能编译,删掉一个字段也会削弱错误处理能力。

URL 安全、路径处理与惰性属性

createValidAbsoluteUrl:校验与受控修复

createValidAbsoluteUrl 只返回 URL 对象或 null,不会返回任意“修过”的字符串。协议白名单包含 http:、https:、ftp:、mailto: 与 tel:;javascript:、data: 等 scheme 会被默认分支拒绝。

两个修复选项仅在输入为字符串时生效:

  1. addDefaultProtocol 会给以 www. 开头的字符串补上 http://,但要求至少存在两个点,避免把 www.pdf 这类相对文件名误认为网站。
  2. tryConvertEncoding 针对未按规范的 7-bit ASCII URI 要求存储、实际写入 UTF-8 字节的畸形 PDF,调用 stringToUTF8String 尝试转换。转换失败会被有意忽略,随后仍用原字符串进行正常 URL 解析。

最后一步很短,却决定了安全边界:

const absoluteUrl = baseUrl ? URL.parse(url, baseUrl) : URL.parse(url);
return _isValidProtocol(absoluteUrl) ? absoluteUrl : null;

提供可用的绝对 base 时,URL.parse 会解析相对输入,而协议校验发生在解析之后。假值输入、无法解析或协议不允许的 URL 最终都得到 null。

core/catalog.js 展示了一个真实的信任边界:链接解析同时传入两个修复选项,只有校验成功才设置 resultObj.url,原始值则单独保存到 resultObj.unsafeUrl。PDF 原值没有被静默改写,但也不会因此进入经过安全校验的导航字段。

updateUrlHash 与 stripPath

updateUrlHash 先尝试 URL.parse,修改解析结果的 hash,再返回 href。可选的相对 URL 分支更值得细看:

if (allowRel && createValidAbsoluteUrl(url, 'http://example.com')) {
  return url.split('#', 1)[0] + `${hash ? `#${hash}` : ''}`;
}
return '';

虚拟 origin 只用于确认相对引用结构合法,返回值仍保持相对形式,不会意外变成 http://example.com 地址。hash 为空字符串时表示移除 fragment。未开启 allowRel 时,无法按绝对 URL 解析的输入会得到空字符串。display/filter_factory.js 在建立自身 base URL 时,就用它移除 URL fragment。

stripPath 的契约则窄得多:返回最后一个正斜杠之后的部分。它不是通用文件系统路径规范化工具,也不会把反斜杠当作分隔符。

shadow:用缓存值替换 getter

PDF.js 中很多对象通过 getter 暴露计算昂贵或只需计算一次的值。shadow 用定义好的数据值覆盖该属性,并返回这个值:

function shadow(obj, prop, value, nonSerializable = false) {
  if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('TESTING')) {
    assert(prop in obj, `shadow: Property "${prop && prop.toString()}" not found in object.`);
  }
  Object.defineProperty(obj, prop, {
    value,
    enumerable: !nonSerializable,
    configurable: true,
    writable: false,
  });
  return value;
}

结果不可写但可配置;nonSerializable 为真时不可枚举。测试环境下,prop in obj 能发现拼错或原本不存在的属性:这个 helper 的目标是遮蔽已有访问器,而不是随意创造新的 API。FeatureTest、WorkerTransport.annotationStorage 以及大量其他 getter 都复用了这套缓存机制。

字节字符串、UTF-8 与 Unicode 规范化

二进制字符串不是 Unicode 文本

PDF 内部经常把字节序列表示为 JavaScript 字符串,并让每个 code unit 落在字节范围内。bytesToString 与 stringToBytes 服务的正是这种表示,它们不是通用 UTF-8 编解码器。

输入较短时,bytesToString 可以直接调用 String.fromCharCode.apply。输入较大则分块处理,避免触及引擎的函数参数数量上限:

const MAX_ARGUMENT_COUNT = 8192;
if (length < MAX_ARGUMENT_COUNT) {
  return String.fromCharCode.apply(null, bytes);
}
const strBuf = [];
for (let i = 0; i < length; i += MAX_ARGUMENT_COUNT) {
  const chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length);
  const chunk = bytes.subarray(i, chunkEnd);
  strBuf.push(String.fromCharCode.apply(null, chunk));
}
return strBuf.join('');

快速路径接受具有数值 length 的类数组对象;分块路径还要求提供 subarray,typed array 正好满足。stringToBytes 执行反向的存储操作,创建新的 Uint8Array,并通过 charCodeAt(i) & 0xff 只保留每个 code unit 的低 8 位。

真实调用方也遵循这个二进制契约:解析器和字体代码用 bytesToString 处理签名与字体数据,getDataProp 则在字符串形式的 PDF 二进制输入到来时调用 stringToBytes。如果数据格式真正要求字符集解码,就不能拿这两个 helper 替代 TextDecoder。

传统的 UTF-8 转换对

stringToUTF8String 与 utf8StringToString 在 UTF-8 字节字符串和普通 JavaScript 字符串之间转换:

function stringToUTF8String(str) {
  return decodeURIComponent(escape(str));
}
 
function utf8StringToString(str) {
  return unescape(encodeURIComponent(str));
}

它们依赖传统的 escape/unescape,但方向很清楚:前者解码二进制字符串中存放的 UTF-8 字节,后者把 JavaScript 字符串编码回这种字节字符串形式。畸形输入可能令百分号解码步骤抛错。metadata、XMP、dataset 与密码处理等上层代码,会在周边格式已经明确所需表示的地方使用它们。

normalizeUnicode:选择性 NFKC,而非全量规范化

对每个提取出的文本块都执行 NFKC,不仅产生不必要的工作,也可能折叠 PDF.js 本不打算改变的字符。normalizeUnicode 改为惰性构造一张正则,只覆盖已知需要规范化的特定 BMP 字符和 presentation form,再将匹配区段替换为 NFKC 形式;另有独立映射把 ſt 处理为 ſt。

关键设计点正是“选择性”。在 core/evaluator.js 中,文本块先拼接,若未设置 disableNormalization 就经过 normalizeUnicode,之后才进入 bidi 算法。因此,规范化属于文本提取策略,不是这个模块里每个字符串 helper 都会隐式执行的副作用。

特性检测与环境边界

isNodeJS:排除浏览器中的 Node 假象

isNodeJS 在模块求值时计算一次。它要求处于 generic 或未构建的源码环境,存在形似真实 Node 的 process 对象,并排除 NW.js;Electron renderer 类进程也会被排除,而 Electron 的 browser 进程类型仍可通过。

这些额外判断很重要,因为 NW.js 与 Electron 会在浏览器上下文里暴露 Node 风格对象。如果只检查 typeof process !== "undefined",就可能在错误的 realm 中选择 Node 专属工厂或网络行为。display/node_utils.js 在加载 Node canvas 能力前依赖这个判断,display/api.js 也用它决定 worker、fetch 和字体处理等默认行为。

FeatureTest:探测一次,用 shadow 缓存

FeatureTest 的每个静态 getter 都会计算一次结果,再在类上遮蔽自身。后续读取不会重复探测:

Getter探测方式与用途
isLittleEndian向 Uint8Array 写入字节 1,再以 Uint32Array 读取同一 buffer 并与 1 比较;图像像素打包依赖这个结果
isOffscreenCanvasSupported检查 OffscreenCanvas,供 canvas 与图像路径使用
isImageDecoderSupported检查 ImageDecoder 全局对象;这里只检测 API 是否存在,并不保证支持所有 codec
isFloat16ArraySupported检查 Float16Array,以启用 float16 专属的变换与字体路径
isSanitizerSupported检查 Sanitizer 全局对象
platform根据 navigator.platform 与 navigator.userAgent 推导操作系统布尔值;Firefox 由 userAgent 检测,并在 MOZCENTRAL 构建中直接为真
isCanvasFilterSupported获取 Offscreen 或 DOM 2D context,检查规范定义的默认 filter 属性是否存在
isAlphaColorInputSupported创建带 alpha 属性的颜色 input,检查八位颜色是否保留 alpha,而不是退化为 #ff0000
isBackdropFilterSupported在 CSS 存在时调用 CSS.supports("backdrop-filter", "blur(1px)")

其中两个探测说明,单纯检查“属性是否存在”并不总够。对于 canvas filter,Safari 在赋值后可能出现普通 JavaScript filter 属性,却并未真正实现 Canvas2D filter。源码改为在赋值前检测默认值,避免这种假阳性。对于 alpha 颜色输入,则检查控件实际保留了什么值,而不是只确认 input 元素能够创建。

缓存是一项明确的取舍:平台与能力答案被视为在当前 realm 生命周期内稳定。它避免重复搭建 DOM/canvas,却也意味着这些 getter 不用于观察首次读取后的动态变化。

仿射变换与缩放恢复

Util 用 [a, b, c, d, e, f] 表示二维仿射变换,与 canvas 和 PDF 的约定一致:

x' = m[0] * x + m[2] * y + m[4]
y' = m[1] * x + m[3] * y + m[5]

Util.transform(m1, m2) 返回的组合等价于先应用 m2、再应用 m1。multiplyByDOMMatrix 使用 md.a 至 md.f 完成同样的“数组乘矩阵”操作,无需把 DOMMatrix 再转换成六元素数组。canvas_dependency_tracker.js 直接使用这种形式。

点处理方法则具有不同的可变性契约:

方法返回值与修改契约
applyTransform(p, m, pos = 0)原地修改 p 中的一个坐标对;pos 支持紧凑排列的坐标数组
applyTransformToBezier(p, transform, pos = 0)原地修改连续三个坐标对:PDF curveTo 存储的两个控制点和终点;当前点由前面的路径数据提供
inverseTransform(m)返回新的六元素逆矩阵
applyInverseTransform(p, m)直接按逆变换公式原地修改一个坐标对
singularValueDecompose2dScale(matrix, output)将两个缩放分量写入调用方提供的输出数组

两个逆变换路径都会除以行列式 m[0] * m[3] - m[1] * m[2],并不处理奇异矩阵。因此,可逆性是调用方必须满足的前置条件,热路径 helper 不会代为修复。

singularValueDecompose2dScale 不是简单的宽高近似。它为线性部分的 2×2 矩阵构造对称乘积,求解特征值,再开平方得到奇异值缩放。在旋转和斜切存在时,这比只读取 m[0]、m[3] 更有意义。|| 1 回退则避免恰好为零的缩放分量继续传播。canvas 与 pattern 代码会在缩放敏感的绘制决策中使用结果。

这里的分配策略也很一致:矩阵组合返回新数组,点变换修改已有紧凑 buffer,缩放恢复写入输出 buffer。渲染热路径因此可以主动控制分配,而不是为每次操作都创建临时数组。

矩形与精确边界计算

累加边界与矩形操作

边界函数采用 [minX, minY, maxX, maxY],并扩展调用方提供的累加器:

const BBOX_INIT = [Infinity, Infinity, -Infinity, -Infinity];
const F32_BBOX_INIT = new Float32Array(BBOX_INIT);

无穷初值让第一个点或矩形自然替换“空边界”。调用方通常会先对模板执行 slice(),或复制进 typed array 后再修改;不应把导出模板本身变成某份文档的累加结果。

pointBoundingBox 与 rectBoundingBox 原地更新四个极值。axialAlignedBoundingBox(rect, transform, output) 变换矩形角点,并将其并入 output。当 m1、m2 都为零时走缩放/平移快速路径;否则加入旋转、斜切项并考虑全部四个角。因此,在保持轴对齐结果的同时,不会错误地假设两个对角点就足够。

其他矩形 API 则有意采用不同约定:

  • normalizeRect 克隆输入,只在副本中交换反向坐标,不修改原数组。
  • intersect 通过 Math.min/Math.max 兼容反向端点,只在某个轴完全分离时返回 null;两个矩形仅相接时,会返回零宽或零高矩形。
  • axialAlignedBoundingBox、pointBoundingBox 与 rectBoundingBox 修改累加器,而不是返回新矩形。

canvas 裁剪、注释矩形、pattern、操作符列表边界和依赖追踪器都建立在这些约定上。例如,core/document.js 求 crop box 与 media box 的交集;display 代码再把计算边界与 canvas 或 clip 边界求交,避免不必要的工作。

不压平曲线的三次 Bézier 边界

计算三次曲线控制点的外包框很容易,但结果可能远大于曲线真实边界。Util.bezierBoundingBox 则先纳入两个端点,分别寻找 x、y 导数的极值,再只对严格位于 0 到 1 开区间内的根求曲线坐标。

私有 helper 处理三种数值分支:

  1. 二次项系数接近零时,在可行情况下退化为求线性根;
  2. 判别式为负时不存在实数极值;
  3. 否则检查两个二次方程根,并由 #getExtremumOnCurve 排除曲线区间之外的根。

这是解析式边界计算,不是曲线压平或采样。core/evaluator.js 在累加路径边界时使用它,ink/free-draw 编辑器也对用户绘制几何复用同一实现,由此让解析器与编辑器的边界算法保持一致。

目标校验、身份与集合工厂

_isValidExplicitDest:共享结构,注入 PDF 类型

显式目标在结构上是 [page, zoom, ...args] 数组。校验需要 PDF 引用与名称类型,但若把 core primitives 导入这个 shared 模块,会造成错误的依赖方向。因此,_isValidExplicitDest(validRef, validName, dest) 改为接收两个谓词。

同一套算法由此可以运行在架构两侧:

  • core/catalog.js 绑定真实的 Ref 判断和 isName 谓词;
  • display/api_utils.js 绑定结构化代理:引用具有整数 num/gen,名称具有字符串 name。

校验器只检查结构,不确认页面对象是否真实存在。page 必须满足 validRef 或为整数,zoom 必须满足 validName,随后由 zoom 名称决定参数契约:

Zoomzoom 后允许的参数
XYZ两个或三个数值/可空参数
Fit、FitB无参数
FitH、FitBH、FitV、FitBV零个或一个数值/可空参数
FitR恰好四个数值参数,不允许 null
其他无效

其余可接受参数必须是数值;除 FitR 外可以使用 null。因此,catalog 的目标解析与 display 侧校验无需共享 worker 专属具体类,也能对结构达成一致。

不透明身份、浅比较与可复用工厂

getUuid 在 Firefox 构建中,或检测到 crypto.randomUUID 函数时,优先调用 crypto.randomUUID()。回退路径生成 32 个随机字节,再返回 bytesToString(buf):

function getUuid() {
  if (
    (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) ||
    typeof crypto.randomUUID === 'function'
  ) {
    return crypto.randomUUID();
  }
  const buf = new Uint8Array(32);
  crypto.getRandomValues(buf);
  return bytesToString(buf);
}

回退结果是包含 32 个 code unit 的不透明随机标识符,不是带连字符的 RFC UUID。调用方只能依赖唯一性,不能依赖格式。编辑器中的 ImageManager 会把这个随机前缀与递增计数器组合成图片 ID。

三个较小的 helper 也有明确边界:

  • isArrayEqual 比较长度,并用 !== 比较各索引元素。它是浅比较,可用于 typed array 等带索引序列,不会递归比较嵌套数组。
  • makeArr、makeMap、makeObj、makeSet 是供 Map/RefCache 的计算式插入 API 复用的工厂函数,避免每个调用点都新建闭包。
  • makeObj 返回 Object.create(null),得到不含 constructor、toString 等继承键的字典对象。

真实调用方会把这些工厂传给 getOrInsertComputed 或相关缓存 API,例如嵌套位图缓存、popup 数组、注释存储 Map 与字形 Set。工厂只负责创建空值,所有权和插入流程仍由集合 API 管理。

运行时兼容补丁

文件末尾是三个极简标准库 shim。它们把兼容逻辑集中起来,让具体功能模块可以直接调用现代 API。

对于 Blob 与 Response,缺少 .bytes() 时基于 .arrayBuffer() 实现:

Blob.prototype.bytes = async function () {
  return new Uint8Array(await this.arrayBuffer());
};

补丁仅在存在 PDFJSDev 的构建环境、未跳过 Babel 处理且原生方法缺失时安装,已有原生实现不会被替换。返回值统一为 Uint8Array,正好满足 PDF writer、图像和 fetch 相关调用方的需求。

Iterator.prototype.join 有另一段回退:

if (typeof Iterator.prototype.join !== 'function') {
  // eslint-disable-next-line no-extend-native
  Iterator.prototype.join = function (separator) {
    return [...this].join(separator);
  };
}

这个 shim 有意保持简单:把迭代器耗尽为数组,再委托给 Array.prototype.join。它提供了预期的字符串操作,但既不是流式处理,也不是常量内存,只适用于有限迭代器。实现中的取舍很明确——在一个地方提供最小语义,而不是让各调用方分散编写迭代器转数组逻辑。

这些补丁会修改全局 prototype,因此它们位于模块作用域这一点很重要。它们属于影响当前 realm 的兼容基础设施,不像 stringToBytes 或 Util.transform 那样是纯 helper。

总结

util.js 是隐藏在 PDF.js 解析器和渲染器架构之下的共享契约层。它最值得关注的不是孤立的便利函数,而是这些函数守住的边界:

  1. 数值就是接口。 PDF 标志、文本模式位组合、OPS 的兼容空洞以及连续的 DrawOPS,都必须结合消费方理解。
  2. 异常承载决策。 异常名称和附加字段可以穿过 worker 传输,让调用方区分密码、无效文档、响应失败、取消和未知错误。
  3. 校验不等于静默改写。 URL 修复范围很窄,协议白名单在解析后执行,原始不安全值与已验证导航 URL 分开保存。
  4. 二进制字符串与 Unicode 是不同表示。 字节截断、分块 fromCharCode、显式 UTF-8 转换和选择性 NFKC 分别解决不同问题,不能互相替代。
  5. 宿主适配集中完成。 isNodeJS、缓存的 FeatureTest 探测与小型 prototype shim,避免环境分支渗入渲染代码各处。
  6. 几何 API 暴露成本模型。 新数组、原地点更新、输出累加器、解析式 Bézier 极值和惰性缓存,都是面向渲染热路径的刻意选择。

理解这些契约后,许多表面无关的 PDF.js 模块会更容易阅读:相同标志决定缓存行为,相同异常跨线程传播,相同矩阵布局驱动文本和 pattern,相同边界累加器同时服务解析器与编辑器几何。

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

相关文章