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

前言
本篇分析 src/shared/css_utils.js,这是一个位于 PDF 字体数据与浏览器 CSS 边界上的小型模块。
尽管文件名很宽泛,当前源码中没有 CSS 单位转换、缩放计算或变换矩阵辅助函数。它实际聚焦在一个更窄、更明确的问题上:对字体族名称进行序列化,使源自 PDF 的数据能够作为 CSS 字符串使用,而不改变周围 CSS 的语法结构。
这个边界很重要。字体名不一定是安全标识符:它可能包含引号、反斜杠、控制字符,甚至正好是 inherit 这样的关键字。若把它直接传给 FontFace 构造函数或插入 @font-face 规则,就会混淆数据与语法。本模块定义了这些边界共同使用的序列化契约。
模块总览
文件包含一个常量、一个私有谓词和一个公开序列化函数:
| 区块 | 行号 | 职责 |
|---|---|---|
CONTROL_CHAR_REGEXP | 16 | 识别 Unicode Cc 类别中的控制字符 |
isCSSString | 25-50 | 判断现有值能否作为 CSS 字符串安全地原样复用 |
serializeFontFamily | 58-74 | 返回经过安全引用与转义的字体族值 |
| 导出 | 76 | 导出控制字符正则与序列化函数,谓词保持私有 |
公开契约刻意保持很小:输入 JavaScript 字符串,输出表示一个 CSS 字符串 token 的源码文本。该函数不负责加载字体、选择回退字体、规范化字体度量,也不计算任何视觉几何信息。
CONTROL_CHAR_REGEXP 与 isCSSString():判断何时可以原样复用
const CONTROL_CHAR_REGEXP = /\p{Cc}/u;Unicode 属性转义针对 Cc 类别,即控制字符类别。正则没有全局标志,因此重复调用 .test() 不会携带 lastIndex 状态。它还会被导出,供 src/core/core_utils.js 中相关的字体名验证路径复用。
isCSSString 是私有快速路径的守卫:
function isCSSString(str) {
const quote = str[0];
if (str.length < 2 || (quote !== `"` && quote !== `'`) || str.at(-1) !== quote) {
return false;
}
const end = str.length - 1;
for (let i = 1; i < end; i++) {
const char = str[i];
if (char === quote || CONTROL_CHAR_REGEXP.test(char)) {
return false;
}
if (char === '\\') {
if (++i >= end || CONTROL_CHAR_REGEXP.test(str[i])) {
return false;
}
}
}
return true;
}这些检查构成一个聚焦的契约,而不是通用 CSS 解析器:
- 值必须以相同的单引号或双引号开始和结束;
- 字符串主体中未转义的同类引号会被拒绝;
- 控制字符即使跟在反斜杠后面也会被拒绝;
- 扫描时反斜杠会消费后一个字符,因此转义引号不会被误认为分隔符;
- 紧邻收尾引号的反斜杠会被拒绝,因为它会转义这个收尾引号。
空的带引号字符串可以通过,另一种引号则可以在字符串内部不转义出现。对于这个聚焦的谓词,这些都是合理结果。如果值不是可复用的 CSS 字符串,序列化函数就会走更安全的处理路径,而不是尝试修补原有形式。
serializeFontFamily():生成 CSS 字符串 token
function serializeFontFamily(fontFamily) {
if (isCSSString(fontFamily)) {
return fontFamily;
}
const escaped = fontFamily.replaceAll(/["\\\p{Cc}]/gu, char =>
char === `"` || char === '\\' ? `\\${char}` : `\\${char.codePointAt(0).toString(16)} `,
);
return `"${escaped}"`;
}这里有两条路径。已经满足 isCSSString 的值会原样返回,保留单引号或双引号形式。其他所有值都会被视为字体族数据,经过转义后再包上双引号。因此结果一定是 CSS 字符串 token,但不一定是新构造的字符串。
为什么使用带引号的字符串,而不是自定义标识符?
CSS 也允许用标识符序列书写字体族,但这种表示有额外的语法和关键字规则。PDF 中一个字面名称为 serif 的字体不能悄悄变成通用字体族选择器,名为 inherit 的字体也不能变成 CSS 全局关键字。带引号的字符串能把名称保留为数据,避免这两种解释。
这里做的是序列化,不是 HTML 转义或 URL 编码。输出面向 CSS 消费者,不能直接插入任意 HTML 或 JavaScript 上下文。
转义规则与具体结果
回退路径只处理双引号输出中需要特殊处理的字符:
"变为\";\变为\\;- 每个
Cc控制字符变为一个后随空格的十六进制 CSS 转义。
末尾空格用于终止十六进制转义。如果没有它,后续十六进制数字可能会被并入转义序列,进而改变字体名。十六进制转义也避免依赖“反斜杠 + 字面换行符”,后者在 CSS 中具有不同语义。
下面的例子展示 CSS 源码文本;A<newline>B 表示值中含有一个真实换行符:
字体族值 输出的 CSS 文本
Acme "Acme"
A"B "A\"B"
A\B "A\\B"
A<newline>B "A\a B"对于 "Acme" 这种已经带引号的值,函数会原样返回,而不是再包一层引号。
调用契约:在每个 CSS 边界使用同一个字体族名
同一个源自 PDF 的字体族名会通过不同 API 进入 CSS,各处都使用这个序列化函数:
| 调用方 | 返回值用途 | 共享契约的意义 |
|---|---|---|
FontFaceLoader.createNativeFontFace | new FontFace(...) 的字体族参数 | 以序列化后的字体族名注册原生字体 |
FontFaceLoader.createFontFaceRule | @font-face 规则中的 font-family | 防止 PDF 数据突破声明边界并注入 CSS |
XFA setFontFamily | style.fontFamily,包括替代字体的族名 | 与字体注册使用相同表示,确保名称匹配 |
validateFontName | 复用 CONTROL_CHAR_REGEXP,而非序列化函数 | 在核心侧验证阶段拒绝不合适的带引号名称 |
最后一行体现了清晰的职责划分:验证决定核心路径是否接受这个名称,序列化则负责在 CSS 输出边界安全表示名称。两者不能互相替代。
总结
当前的 css_utils.js 是字体族序列化模块,不是通用 CSS 几何工具箱。它的设计建立在四个明确决策上:
- 只有
isCSSString证明分隔符和转义安全时,才原样复用现有值。 - 其他情况一律输出带引号的 CSS 字符串,避免被解释为通用字体族或 CSS 全局关键字。
- 按 CSS 字符串语法转义引号、反斜杠和控制字符,并保留十六进制转义必需的终止空格。
- 在
FontFace、@font-face与 XFA 样式间共享同一序列化,让字体注册与消费对字体族名达成一致。
理解这个契约,比把文件名想象成一组无关 CSS 工具更有价值:模块只保护一个特定的“数据到语法”边界,并且不承担渲染或布局职责。
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 源码解读: 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 源码解读: 共享工具层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日