StreamPDFStreamPDF
返回博客

PDF.js 源码解读: obj_bin_transform_utils

2026年10月27日 · StreamPDF Team

PDF.js 源码解读: obj_bin_transform_utils

前言

这份 83 行的 src/shared/obj_bin_transform_utils.js 既不编译也不解码二进制对象。它定义 worker 写入端与主线程读取端共同遵守的约定:字段顺序、固定偏移、Pattern 头部位置,以及两端使用的文本编解码器。

core 与 display 的文章已经跟踪过字体、Pattern 和字形路径缓冲区的实际写入与读取。本篇停在 schema 层:ArrayBuffer 本身不携带属性名,如果两端没有导入同一份布局,即使写入完全正确,读取仍可能错位。

五个导出中只有 InfoUtils 包含行为,另外四个都是完全由静态 schema 数据组成的类;这些名称背后并没有一个通用序列化器。

模块总览

导出Schema 或服务对调用方的作用
CSS_FONT_INFO三个有序字符串字段名为独立 CSS 字体字符串块提供索引
SYSTEM_FONT_INFO四个有序字符串字段名为常规系统字体字符串块提供索引
FONT_INFO布尔、数值、字符串顺序与五个偏移定义编译后字体记录的固定前缀
PATTERN_INFO八个字节偏移定位 20 字节 Pattern 头部中的标志与计数
InfoUtils惰性安装的 TextEncoder 与 TextDecoder复用 UTF-8 编解码器,而不缓存字段值

依赖方向刻意保持很小:core 与 display 导入这个模块,而它只从 util.js 导入 shadow。任何一端的缓冲区实现都不会成为另一端的依赖。

CSS_FONT_INFO 与 SYSTEM_FONT_INFO:属性顺序就是线格式

class CSS_FONT_INFO {
  static strings = ['fontFamily', 'fontWeight', 'italicAngle'];
}
 
class SYSTEM_FONT_INFO {
  static strings = ['css', 'loadedName', 'baseFontName', 'src'];
}

这些数组不只是列出预期属性,其索引本身就是协议:

索引CSS_FONT_INFO.stringsSYSTEM_FONT_INFO.strings
0fontFamilycss
1fontWeightloadedName
2italicAnglebaseFontName
3—src

core 写入端按数组顺序编码各个值;display 读取端用同一数组的长度校验索引,并依次扫描带长度前缀的条目,直到目标索引。属性名不会进入缓冲区,各条目也没有独立类型标签。因此,即使属性集合不变,只要调整数组顺序,字段含义就会改变。

这些类并不定义外层的长度前缀、系统字体的 guessFallback 字节或可选 style 块。那些细节属于写入端与读取端;共享层只固定双方必须遵循的顺序。

FONT_INFO:三组字段与一条偏移计算链

字段组决定索引与宽度

static bools = [
  "black",
  "bold",
  "disableFontFace",
  "fontExtraProperties",
  "isInvalidPDFjsFont",
  "isType3Font",
  "italic",
  "missingFile",
  "remeasure",
  "vertical",
];
 
static numbers = ["ascent", "defaultWidth", "descent"];
static strings = ["fallbackName", "loadedName", "mimetype", "name"];

布尔组每项使用两个 bit,一个字节能容纳四项。core 用 00 表示 undefined、01 表示 false、10 表示 true,所以 schema 计算的是位域大小,而不是每个布尔占一个字节。数值组包含三个 8 字节值。字符串组同样以数组位置而非序列化键名作为字段身份。

偏移通过计算得出,而非分别手写

static OFFSET_NUMBERS = Math.ceil((this.bools.length * 2) / 8);
static OFFSET_BBOX = this.OFFSET_NUMBERS + this.numbers.length * 8;
static OFFSET_FONT_MATRIX = this.OFFSET_BBOX + 1 + 2 * 4;
static OFFSET_DEFAULT_VMETRICS = this.OFFSET_FONT_MATRIX + 1 + 8 * 6;
static OFFSET_STRINGS = this.OFFSET_DEFAULT_VMETRICS + 1 + 2 * 3;

在静态字段初始化器中,this 就是 FONT_INFO,因此每个表达式都建立在紧邻的前置 schema 上。按当前数组计算,边界如下:

常量值推导从这里开始的区域
OFFSET_NUMBERS3十个两 bit 布尔,向上取整到字节三个 Float64 值
OFFSET_BBOX273 + 3 * 8长度字节加四个 Int16 槽位
OFFSET_FONT_MATRIX3627 + 1 + 4 * 2长度字节加六个 Float64 槽位
OFFSET_DEFAULT_VMETRICS8536 + 1 + 6 * 8长度字节加三个 Int16 槽位
OFFSET_STRINGS9285 + 1 + 3 * 2字符串块总长度,之后是可变尾部

每个数组区域公式中的 1 是长度标记。即使写入端把某个数组标记为缺失,其载荷宽度仍计入后续偏移,因此后面的区域不会移动。部分数值偏移并未对齐;读取方使用 DataView,schema 不额外插入对齐填充。

OFFSET_STRINGS 并不是完整字体记录的终点。它标记可变长尾部的起点,尾部依次包含字符串块、嵌套的系统/CSS 信息和字体数据。后面的区域只有长度而没有共享的绝对偏移,因为其位置取决于此前内容。

这些公式消除了重复运算,但不代表任何 schema 修改都天然安全。增删布尔或数值字段会更新下游偏移;调整字段名顺序则只改变语义,不改变任何偏移。固定数组数量和元素宽度也直接写在公式里,修改时必须与 core、display 同步。

PATTERN_INFO:Pattern 头部的字节位置

常量偏移协议使用的宽度含义
KIND01 字节1 为 axial,2 为 radial,3 为 mesh
HAS_BBOX11 字节是否存在可选边界框
HAS_BACKGROUND21 字节是否存在 mesh 背景
SHADING_TYPE31 字节mesh 的 shading 类型,其他类型不使用
N_COORD44 字节坐标对数量
N_COLOR84 字节按 RGBA 步长排列的颜色条目数
N_STOP124 字节渐变停靠点数量
N_FIGURES164 字节figure 数量;当前写入零且读取端不读取

四个 N_* 值都是条目数,不是字节数。它们的位置共同形成 20 字节头部,core 与 display 都从这之后开始处理载荷。这个 20 没有以 HEADER_SIZE 常量导出,正文偏移也不在这里描述:共享类只覆盖头部词汇。

当前 core 向 N_FIGURES 写入零,当前 display 不读取它。因此在这个版本里应把它视为保留容量,而不是已经实现 figure 载荷的证据。这个类本身不校验 kind、不校验计数,也不计算缓冲区大小。

InfoUtils.decoder 与 InfoUtils.encoder:缓存编解码器,而非缓存值

class InfoUtils {
  static get decoder() {
    return shadow(this, 'decoder', new TextDecoder());
  }
 
  static get encoder() {
    return shadow(this, 'encoder', new TextEncoder());
  }
}

第一次访问时,this 是 InfoUtils。shadow 把静态访问器替换成保存编解码器的数据属性,并返回该实例,所以之后的属性读取会复用它,不会再次执行 getter。

这里的默认行为很重要:编解码器负责在 JavaScript 字符串与 UTF-8 字节之间转换。缓存的只有编解码器对象,不包括编码后的字节数组、解码后的字符串或任何字体属性;display 的 getter 仍可能在每次访问时重新扫描并解码。每个 JavaScript realm 也有自己的模块状态,编解码器对象不会随缓冲区跨过 worker 边界。

协议边界:这个模块不保证什么

  • 不做版本协商。 这些类中没有 magic number、schema 版本或功能掩码。写入端与读取端应当来自相同、兼容的 PDF.js 构建。
  • 不规定字节序。 这里只有偏移与宽度;core/display 中每个 DataView setter 或 getter 才决定大端、小端或原生 TypedArray 解释方式。
  • 不是校验层。 边界检查和布局断言位于实现中。导入这些常量,并不代表任意缓冲区都能安全解码。
  • 不是完整对象 schema。 可变字体尾部、Pattern 正文和原始字形路径仍需要 core 与 display 中的额外约定。尤其是原始字体路径,在这个文件里没有字段定义。

这些省略让共享模块保持小巧,也意味着它只是协议的一部分,而不是一份可独立使用的二进制格式规范。

总结

obj_bin_transform_utils.js 是 PDF.js 字体与 Pattern 二进制协议的共享词汇。CSS_FONT_INFO、SYSTEM_FONT_INFO 和 FONT_INFO 把属性顺序变成索引;FONT_INFO 根据字段宽度推导固定边界;PATTERN_INFO 命名头部位置;InfoUtils 则让两端复用 UTF-8 编解码器。

它最关键的设计决策是共享 schema,同时不让写入端与读取端互相耦合。代价也同样明确:常量能防止偏移漂移,但字段顺序、字节序、版本和其余载荷布局,仍要求两端严谨地同步修改。

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

相关文章