PDF.js 源码解读: 共享工具层
2026年10月27日 · StreamPDF Team

前言
本篇分析 src/shared/util.js。这个 1229 行的模块被广泛引入解析器、渲染器、注释系统和编辑器。
乍看之下,它很像一个杂项工具包,但这种理解会忽略它真正的职责:为彼此独立的 PDF.js 子系统定义统一的底层词汇与契约。PDF 数值标志、操作符编号、异常结构、二进制字符串转换、浏览器能力探测和几何计算,都需要唯一且一致的解释。如果 worker 与 display 层各自复制一份,极易产生难以察觉的不兼容。
这个模块还横跨两类边界:一类 helper 负责解释 PDF 数据,必须面对畸形输入和规范定义的数值;另一类负责适配 JavaScript 宿主,因为 Node.js、浏览器、worker 与转译构建提供的能力并不相同。按功能族阅读,才能看清这些边界及其背后的设计取舍。
模块总览
源码可以划分为八个实现区块,外加导出表:
| 区块 | 行号 | 职责 |
|---|---|---|
| 环境检测与共享常量 | 21-384 | 宿主分类、PDF 标志、注释数值、操作码与日志级别 |
| 诊断工具 | 386-426 | verbosity 状态、info、warn、unreachable 与 assert |
| URL、路径与属性定义 | 428-519 | 协议白名单、hash 更新、路径截取与 shadow |
| 异常与字节字符串 | 524-616 | 类型化失败信息与二进制字符串转换 |
FeatureTest | 618-718 | 惰性执行并缓存结果的宿主能力探测 |
Util | 720-1028 | 颜色、仿射变换、缩放恢复、矩形与 Bézier 边界 |
| 文本、身份与目标 | 1030-1127 | UTF-8 字节字符串、Unicode 规范化、UUID 与显式目标校验 |
| 工厂与兼容补丁 | 1129-1164 | 可复用集合工厂及小型标准库 shim |
| 导出表 | 1166-1229 | 导出共享常量、类、helper 与工厂 |
阅读时可以始终抓住两个主题:
- 常量同样可以是兼容接口。 给
OPS重新编号,与直接修改函数签名一样,都可能破坏调用方。 - 可变性是刻意设计,而非偶然行为。 多个热路径方法直接写入调用方提供的数组以避免分配;另一些方法则明确返回副本。这个区别本身就是 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 规范枚举 |
TextRenderingMode | PDF 文本模式 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 本身。派生类补充调用方作出决策所需的信息:
| 异常 | 附加状态 | 类型表达的语义 |
|---|---|---|
PasswordException | code | 需要密码或密码错误,代码由 PasswordResponses 定义 |
UnknownErrorException | details | 无法归类的失败及诊断细节 |
InvalidPDFException | — | 文档不是有效 PDF |
ResponseException | status、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 会被默认分支拒绝。
两个修复选项仅在输入为字符串时生效:
addDefaultProtocol会给以www.开头的字符串补上http://,但要求至少存在两个点,避免把www.pdf这类相对文件名误认为网站。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 处理三种数值分支:
- 二次项系数接近零时,在可行情况下退化为求线性根;
- 判别式为负时不存在实数极值;
- 否则检查两个二次方程根,并由
#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 名称决定参数契约:
| Zoom | zoom 后允许的参数 |
|---|---|
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 解析器和渲染器架构之下的共享契约层。它最值得关注的不是孤立的便利函数,而是这些函数守住的边界:
- 数值就是接口。 PDF 标志、文本模式位组合、
OPS的兼容空洞以及连续的DrawOPS,都必须结合消费方理解。 - 异常承载决策。 异常名称和附加字段可以穿过 worker 传输,让调用方区分密码、无效文档、响应失败、取消和未知错误。
- 校验不等于静默改写。 URL 修复范围很窄,协议白名单在解析后执行,原始不安全值与已验证导航 URL 分开保存。
- 二进制字符串与 Unicode 是不同表示。 字节截断、分块
fromCharCode、显式 UTF-8 转换和选择性 NFKC 分别解决不同问题,不能互相替代。 - 宿主适配集中完成。
isNodeJS、缓存的FeatureTest探测与小型 prototype shim,避免环境分支渗入渲染代码各处。 - 几何 API 暴露成本模型。 新数组、原地点更新、输出累加器、解析式 Bézier 极值和惰性缓存,都是面向渲染热路径的刻意选择。
理解这些契约后,许多表面无关的 PDF.js 模块会更容易阅读:相同标志决定缓存行为,相同异常跨线程传播,相同矩阵布局驱动文本和 pattern,相同边界累加器同时服务解析器与编辑器几何。
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- PDF.js 从 demo 深入浅出 10: CanvasGraphics 与 operatorList 绘制2026年10月27日
- PDF.js 从 demo 深入浅出 9: 网络流与 Range 请求2026年10月27日
- PDF.js 从 demo 深入浅出 8: PDFWorker.create 与真假 Worker 握手2026年10月27日
- PDF.js 从 demo 深入浅出 7: WorkerTransport 初始化2026年10月27日
- PDF.js 从 demo 深入浅出 6: MessageHandler 跨线程通讯2026年10月27日
- PDF.js 从 demo 深入浅出 5: render 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 4: getViewport 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 3: getPage 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 2: getDocument 方法全貌2026年10月27日
- PDF.js 源码解读: TextLayer2026年10月27日
- PDF.js 源码解读: font_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_display2026年10月27日
- PDF.js 源码解读: PDFObjects2026年10月27日
- PDF.js 源码解读: 网络流2026年10月27日
- PDF.js 源码解读: internal_evt.js2026年10月27日
- PDF.js 源码解读: math_clamp.js2026年10月27日
- PDF.js 源码解读: css_utils.js2026年10月27日
- PDF.js 源码解读: scripting_utils2026年10月27日
- PDF.js 源码解读: MurmurHash3_642026年10月27日
- PDF.js 源码解读: obj_bin_transform_utils2026年10月27日
- PDF.js 源码解读: image_utils.js2026年10月27日
- PDF.js 源码解读: BasePDFStream2026年10月27日
- PDF.js 源码解读: MessageHandler2026年10月27日
- PDF.js 源码解读: api2026年10月27日
- PDF.js 源码解读: fonts2026年10月27日
- PDF.js 源码解读: glyphlist2026年10月27日
- PDF.js 源码解读: binary_cmap2026年10月27日
- PDF.js 源码解读: cmap2026年10月27日
- PDF.js 源码解读: encodings2026年10月27日
- PDF.js 源码解读: metrics2026年10月27日
- PDF.js 源码解读: standard_fonts2026年10月27日
- PDF.js 源码解读: fonts_utils2026年10月27日
- PDF.js 源码解读: default_appearance2026年10月27日
- PDF.js 源码解读: metadata_parser2026年10月27日
- PDF.js 源码解读: name_number_tree2026年10月27日
- PDF.js 源码解读: struct_tree2026年10月27日
- PDF.js 源码解读: catalog2026年10月27日
- PDF.js 源码解读: pdf_manager2026年10月27日
- PDF.js 源码解读: object_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_core2026年10月27日
- PDF.js 源码解读: crypto2026年10月27日
- PDF.js 源码解读: core_utils2026年10月27日
- PDF.js 源码解读: primitives2026年10月27日
- PDF.js 源码解读: worker_stream2026年10月27日
- PDF.js 源码解读: ccitt_stream2026年10月27日
- PDF.js 源码解读: jpx_stream2026年10月27日
- PDF.js 源码解读: jbig2_stream2026年10月27日
- PDF.js 源码解读: jpeg_stream2026年10月27日
- PDF.js 源码解读: decrypt_stream2026年10月27日
- PDF.js 源码解读: brotli_stream2026年10月27日
- PDF.js 源码解读: run_length_stream2026年10月27日
- PDF.js 源码解读: ascii_hex_stream2026年10月27日
- PDF.js 源码解读: ascii_85_stream2026年10月27日
- PDF.js 源码解读: lzw_stream2026年10月27日
- PDF.js 源码解读: predictor_stream2026年10月27日
- PDF.js 源码解读: flate_stream2026年10月27日
- PDF.js 源码解读: decode_stream2026年10月27日
- PDF.js 源码解读: chunked_stream2026年10月27日
- PDF.js 源码解读: stream2026年10月27日
- PDF.js 源码解读: base_stream2026年10月27日
- PDF.js 深入浅出 11: Parser2026年10月26日
- PDF.js 深入浅出 10: XRef2026年10月26日
- PDF.js 深入浅出 9: CanvasGraphics2026年10月26日
- PDF.js 深入浅出 8: Document 与 Page2026年10月26日
- AI 时代, 我们到底应该做什么才能活下去(StreamPDF 创立有感)2026年10月26日
- PDF.js 深入浅出 7: PartialEvaluator2026年10月25日
- 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 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF2026年10月19日
- PDF.js 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日