PDF.js 源码解读: obj_bin_transform_utils
2026年10月27日 · StreamPDF Team

前言
这份 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.strings | SYSTEM_FONT_INFO.strings |
|---|---|---|
| 0 | fontFamily | css |
| 1 | fontWeight | loadedName |
| 2 | italicAngle | baseFontName |
| 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_NUMBERS | 3 | 十个两 bit 布尔,向上取整到字节 | 三个 Float64 值 |
OFFSET_BBOX | 27 | 3 + 3 * 8 | 长度字节加四个 Int16 槽位 |
OFFSET_FONT_MATRIX | 36 | 27 + 1 + 4 * 2 | 长度字节加六个 Float64 槽位 |
OFFSET_DEFAULT_VMETRICS | 85 | 36 + 1 + 6 * 8 | 长度字节加三个 Int16 槽位 |
OFFSET_STRINGS | 92 | 85 + 1 + 3 * 2 | 字符串块总长度,之后是可变尾部 |
每个数组区域公式中的 1 是长度标记。即使写入端把某个数组标记为缺失,其载荷宽度仍计入后续偏移,因此后面的区域不会移动。部分数值偏移并未对齐;读取方使用 DataView,schema 不额外插入对齐填充。
OFFSET_STRINGS 并不是完整字体记录的终点。它标记可变长尾部的起点,尾部依次包含字符串块、嵌套的系统/CSS 信息和字体数据。后面的区域只有长度而没有共享的绝对偏移,因为其位置取决于此前内容。
这些公式消除了重复运算,但不代表任何 schema 修改都天然安全。增删布尔或数值字段会更新下游偏移;调整字段名顺序则只改变语义,不改变任何偏移。固定数组数量和元素宽度也直接写在公式里,修改时必须与 core、display 同步。
PATTERN_INFO:Pattern 头部的字节位置
| 常量 | 偏移 | 协议使用的宽度 | 含义 |
|---|---|---|---|
KIND | 0 | 1 字节 | 1 为 axial,2 为 radial,3 为 mesh |
HAS_BBOX | 1 | 1 字节 | 是否存在可选边界框 |
HAS_BACKGROUND | 2 | 1 字节 | 是否存在 mesh 背景 |
SHADING_TYPE | 3 | 1 字节 | mesh 的 shading 类型,其他类型不使用 |
N_COORD | 4 | 4 字节 | 坐标对数量 |
N_COLOR | 8 | 4 字节 | 按 RGBA 步长排列的颜色条目数 |
N_STOP | 12 | 4 字节 | 渐变停靠点数量 |
N_FIGURES | 16 | 4 字节 | 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 中每个
DataViewsetter 或 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 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- 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 源码解读: image_utils.js2026年10月27日
- PDF.js 源码解读: BasePDFStream2026年10月27日
- PDF.js 源码解读: 共享工具层2026年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日