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

前言
前面几篇章已经把「文档」这条线走完:xref 负责按对象号取回对象,parser 负责把字节变成对象,document 负责页面模型,evaluator 负责把内容流翻译成 operator 列表,stream 负责分块字节,primitives 定义了对象原语,catalog 把根字典组织成文档语义。从这一篇开始进入「字体层」,也就是内容流里 Tj、TJ 这些文字操作符真正落地的地方。
字体解析在 PDF.js 里分成三块:evaluator.js 读取字体字典与资源,fonts.js 决定字体类型、构造 Font 子类、建立字符到字形的映射,Type1、CFF、TrueType、Type3 各自还有自己的解析器。夹在它们之间的 src/core/fonts_utils.js(387 行)不是某一类字体的实现,而是这些分支共用的常量与算法抽屉。
用一句话概括:它是字体层的公共工具箱,不持有字体状态,也不读取 PDF 对象,只提供标志位、标准字形序、字形名恢复、字符映射、字体名归一化、竖排映射和 Type3 位图编译这些可复用的纯函数与常量表。
模块总览
fonts_utils.js 可以切成八块,每块都被至少两个字体分支复用:
| 区块 | 关键成员 | 职责 |
|---|---|---|
| 策略开关 | SEAC_ANALYSIS_ENABLED | 决定 seac 合成字形是否拆成基字与重音分别绘制 |
| 标志位 | FontFlags | 把 /Flags 里的位映射成数值掩码 |
| 字形序 | MacStandardGlyphOrdering | 258 个按 glyph id 排列的标准 Mac 字形名 |
| 字形名工具 | recoverGlyphName | 把 uniXXXX 之类非标准名恢复成标准名 |
| 字符映射 | type1FontGlyphMapping | Type1 与简单 CFF 的 char code 到 glyph id 映射 |
| 字体名工具 | normalizeFontName | 统一逗号、下划线与空白,供标准字体表匹配 |
| 竖排工具 | getVerticalPresentationForm | 横向字符到竖排呈现形式码位的懒加载表 |
| Type3 编译 | compileType3Glyph | 把位图字形编译成矢量路径的 operator 参数 |
它在字体链路里的位置可以画成这样:
// 页面内容流
// → evaluator.js 读取 /Font、/Encoding、/Widths、/FontDescriptor
// → fonts.js Font 子类:TrueType / Type1 / CFF / Type3
// → fonts_utils.js 本篇:共享标志、字形名、字符映射、Type3 编译
// → type1_font.js 内置编码与 seac 分析
// → cff_font.js 内置编码与 charset
// → font_renderer.js 生成字形轮廓
// → glyph.js 单字符的宽度、unicode 与轮廓版本说明:本篇严格按当前仓库这一版
fonts_utils.js讲解,它只导出八个成员:compileType3Glyph、FontFlags、getVerticalPresentationForm、MacStandardGlyphOrdering、normalizeFontName、recoverGlyphName、SEAC_ANALYSIS_ENABLED、type1FontGlyphMapping。旧资料里经常提到的getFontNameToFileMap、getStdFontMap、getNonStdFontMap、getSerifFonts、getSymbolsFonts、getGlyphMapForStandardFonts在本版不在这个文件,而在standard_fonts.js;两边共用的只有normalizeFontName与MacStandardGlyphOrdering。读源码时不要按记忆里的旧文件划分去找这些函数。
依赖方向
文件头只有五个导入,全部指向更底层的公共模块:
import { DrawOPS, info, OPS } from '../shared/util.js';
import { getEncoding, StandardEncoding } from './encodings.js';
import { getGlyphsUnicode } from './glyphlist.js';
import { getLookupTableFactory } from './core_utils.js';
import { getUnicodeForGlyph } from './unicode.js';| 依赖 | 提供的能力 | 在本文件的使用点 |
|---|---|---|
shared/util | OPS、DrawOPS、info | Type3 编译结果、恢复失败的日志 |
encodings | getEncoding、StandardEncoding | type1FontGlyphMapping 的基准编码 |
glyphlist | getGlyphsUnicode | 字形名反查与 /Differences 恢复 |
core_utils | getLookupTableFactory | 竖排呈现形式表的懒加载 |
unicode | getUnicodeForGlyph | 从 uniXXXX 解析码位 |
它没有反向依赖 fonts.js、type1_font.js、cff_font.js,也没有依赖 evaluator.js。这条单向依赖是它可以被所有字体分支安全引用的前提:公共层不知道字体格式的存在,字体格式却可以自由引用公共层。
常量层:SEAC 开关与 FontFlags
SEAC 分析开关
文件开头是一段很长的注释和一个布尔常量:
// Accented characters have issues on Windows and Linux. When this flag is
// enabled glyphs that use seac and seac style endchar operators are truncated
// and we instead just store the glyph id's of the base glyph and its accent to
// be drawn individually.
// Linux (freetype) requires that when a seac style endchar is used
// that the charset must be a predefined one, however we build a
// custom one. Windows just refuses to draw glyphs with seac operators.
const SEAC_ANALYSIS_ENABLED = true;seac 是 Type1 字体里的合成重音操作:一个字符由「基字 + 重音」两个字形拼出来,最后用 endchar 的四个额外参数指定。浏览器侧的字体渲染器并不吃这一套:Linux 的 freetype 要求使用 seac 时 charset 必须是预定义编码,而 PDF.js 为了正确处理子集字体,构建的是自定义 charset;Windows 则直接拒绝绘制带 seac 的字形。
所以 PDF.js 的选择是在解析阶段就把合成拆开:遇到 seac 时不再保留那条 endchar,而是记下基字与重音的 glyph id,交给上层分别绘制。这样渲染器只需要处理普通路径,不再依赖平台对 seac 的支持。
SEAC_ANALYSIS_ENABLED 是模块级常量,运行时不可配置。它被四个位置引用:
| 使用点 | 传给谁 | 作用 |
|---|---|---|
fonts.js 的 FontRendererFactory | 字形渲染器 | 决定是否按拆分后的基字与重音绘制 |
type1_font.js 的 Type1Parser | 头部与 eexec 段 | 记录 seac 的组成字形 |
cff_font.js 的 CFFParser | CFF 解析器 | 处理 CFF 里的 seac 风格 endchar |
fonts.js 的字形映射 | 合成重音映射 | 把 seac 拆成两组 glyph id |
四处必须看到同一个值,否则「拆开的重音」和「按 seac 解释的字符」会在同一个字体里混用。
FontFlags 位掩码
第二个常量把 PDF 规范里的字体标志位固化成数字:
const FontFlags = {
FixedPitch: 1,
Serif: 2,
Symbolic: 4,
Script: 8,
Nonsymbolic: 32,
Italic: 64,
AllCap: 65536,
SmallCap: 131072,
ForceBold: 262144,
};这些值对应 /FontDescriptor 里 /Flags 的位位置,而不是连续编号:
| 成员 | 十进制 | 位位置 | 规范含义 |
|---|---|---|---|
FixedPitch | 1 | 1 | 等宽字体 |
Serif | 2 | 2 | 有衬线 |
Symbolic | 4 | 3 | 符号字体,使用内置编码 |
Script | 8 | 4 | 手写体 |
Nonsymbolic | 32 | 6 | 非符号字体,使用标准编码 |
Italic | 64 | 7 | 斜体 |
AllCap | 65536 | 17 | 全大写 |
SmallCap | 131072 | 18 | 小型大写 |
ForceBold | 262144 | 19 | 强制加粗 |
本版核心路径真正读取的只有四个:FixedPitch、Serif、Symbolic、Nonsymbolic。Italic、Script、AllCap、SmallCap、ForceBold 按规范表定义,但当前代码不拿它们做分支判断。注意 Symbolic 与 Nonsymbolic 在规范里应当互斥,现实中却有大量两者都置位的字体;evaluator.js 对这种情况有专门的回退:TrueType 字体同时置位且存在 /Differences 时,清掉 Symbolic,按非符号字体处理。
| 位 | 消费位置 | 用途 |
|---|---|---|
FixedPitch | fonts.js 的 isMonospace、evaluator.js 的宽度推断 | 等宽字体判断 |
Serif | fonts.js 的 isSerifFont、evaluator.js | 衬线字体回退 |
Symbolic | type1FontGlyphMapping、fonts.js | 选择内置编码还是标准编码 |
Nonsymbolic | evaluator.js 的默认编码回退 | 默认使用 StandardEncoding |
fonts_utils.js 只定义掩码,不解释掩码。判断逻辑留在 evaluator.js 与 fonts.js,因为「这个位对当前字体类型意味着什么」依赖于字体是 Type1、TrueType 还是 CFF,不能放在公共常量层。
MacStandardGlyphOrdering:按 glyph id 排列的名字表
MacStandardGlyphOrdering 是一张 258 项的数组,下标就是 glyph id,元素是字形名:
// prettier-ignore
const MacStandardGlyphOrdering = [
".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl",
"numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft",
"parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
// ...
"Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar",
"Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply",
// ...
"scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat"];前 258 个名字来自 Macintosh 标准字形序,前三个固定是 .notdef、.null、nonmarkingreturn,第四个开始才是可打印的 space。这张表最容易被误认为「又一张编码表」,但它和 encodings.js 里的表方向正好相反:
| 表 | 下标含义 | 元素含义 | 典型消费者 |
|---|---|---|---|
StandardEncoding 等 | char code,0 到 255 | 字形名 | evaluator.js、fonts.js 建立默认编码 |
MacStandardGlyphOrdering | glyph id,0 到 257 | 字形名 | TrueType post 表、标准字体 glyph id 映射 |
它有两个直接消费者。第一是 TrueType 的 post 表:format 1 直接等于这张表,format 2 的自定义名字从下标 258 开始追加,源码里的判断就是 j < 258 时取这张表,否则取 customNames[j - 258]。第二是 standard_fonts.js 的 getGlyphMapForMacOrderedFonts,它从下标 3 开始遍历,把每个字形名通过 getGlyphsUnicode 换成 Unicode,得到 glyph id 到 Unicode 的映射。
这张表是模块级常量,不走 getLookupTableFactory。因为它本身只是一个固定数组,没有构建成本;每次访问都返回同一个引用,也不会被 cleanup 清掉。真正需要懒加载的是由它派生出来的 Unicode 映射,那些映射放在 standard_fonts.js 的工厂里。
recoverGlyphName:从非标准名字反推标准名
有些生成器会把字形名写成 uni0041 这种形式,而不是 A。recoverGlyphName 负责把这类名字还原:
function recoverGlyphName(name, glyphsUnicodeMap) {
if (glyphsUnicodeMap[name] !== undefined) {
return name;
}
// The glyph name is non-standard, trying to recover.
const unicode = getUnicodeForGlyph(name, glyphsUnicodeMap);
if (unicode !== -1) {
for (const key in glyphsUnicodeMap) {
if (glyphsUnicodeMap[key] === unicode) {
return key;
}
}
}
info('Unable to recover a standard glyph name for: ' + name);
return name;
}三步逻辑很清楚:
- 名字已经在标准字形表里,原样返回,这是绝大多数情况,只花一次属性查找。
- 不在表里,交给
getUnicodeForGlyph尝试按uniXXXX或uXXXX{XX}解析出码位。 - 解析出码位后,反向遍历
glyphsUnicodeMap,找第一个 Unicode 值相同的键,作为标准名返回。
第三步是线性扫描,看起来贵,但触发条件很窄:只有 /Differences 里出现非标准名时才会走到这里。glyphsUnicodeMap 来自 glyphlist.js 的 getGlyphsUnicode,同样是 getLookupTableFactory 的产物,整个 Worker 只构建一次,反向扫描的是同一份共享数据。
如果三步都失败,函数返回原名,而不是空值。调用方会继续拿这个名字查表:type1FontGlyphMapping 用 glyphNames.indexOf,fonts.js 的 cmap 分支用 glyphsUnicodeMap;前者查不到就落到 .notdef,后者查不到就跳过这个字形。返回原名让两条路径共享同一种降级方式,不需要额外的错误类型。
设计要点:字形名恢复只在「映射已经确定要查表」的时候发生,而不是在解析字体字典时批量清洗所有名字。批量清洗会让每个字体都付出全表扫描的成本,而实际坏名字通常只出现在少数
/Differences条目上;把恢复放到查表失败的那一次,成本与问题规模成正比。
type1FontGlyphMapping:char code 到 glyph id
这是文件里最长、也是被复用最多的函数:Type1 字体与简单 CFF 字体都用它建立字符映射。它接收三个输入:字体属性、字体程序自带的内置编码、按 glyph id 排列的字形名数组,输出一张 char code 到 glyph id 的表。
function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
const charCodeToGlyphId = Object.create(null);
let glyphId, charCode, baseEncoding;
const isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
if (properties.isInternalFont) {
baseEncoding = builtInEncoding;
for (charCode = 0; charCode < baseEncoding.length; charCode++) {
glyphId = glyphNames.indexOf(baseEncoding[charCode]);
charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
}
} else if (properties.baseEncodingName) {
// If a valid base encoding name was used, the mapping is initialized with
// that.
baseEncoding = getEncoding(properties.baseEncodingName);
for (charCode = 0; charCode < baseEncoding.length; charCode++) {
glyphId = glyphNames.indexOf(baseEncoding[charCode]);
charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
}
} else if (isSymbolicFont) {
// For a symbolic font the encoding should be the fonts built-in encoding.
for (charCode in builtInEncoding) {
charCodeToGlyphId[charCode] = builtInEncoding[charCode];
}
} else {
// For non-symbolic fonts that don't have a base encoding the standard
// encoding should be used.
baseEncoding = StandardEncoding;
// ...
}
// ...
return charCodeToGlyphId;
}四个分支的选择顺序本身就是一条优先级规则:
| 判断顺序 | 条件 | 基准编码 | 为什么放在这一层 |
|---|---|---|---|
| 1 | properties.isInternalFont | 字体程序自带编码 | 标准 14 字体的内部编码最可信 |
| 2 | properties.baseEncodingName | getEncoding 查到的表 | /Encoding 明确指定时优先于内置编码 |
| 3 | FontFlags.Symbolic | 内置编码,且直接复制 glyph id | 符号字体的 code 本身就是 glyph id |
| 4 | 其它 | StandardEncoding | 规范要求非符号字体默认用标准编码 |
第一、第二、第四分支做的是同一件事:拿基准编码里的字形名,去 glyphNames 里找下标。找不到就写 0,也就是 .notdef。第三个分支不同,它不做 indexOf,因为符号字体的 builtInEncoding 已经是 char code 到 glyph id 的映射,直接复制即可。
分支选完基准编码后,最后统一合并 /Differences:
// Lastly, merge in the differences.
const differences = properties.differences;
let glyphsUnicodeMap;
if (differences) {
for (charCode in differences) {
const glyphName = differences[charCode];
glyphId = glyphNames.indexOf(glyphName);
if (glyphId === -1) {
glyphsUnicodeMap ??= getGlyphsUnicode();
const standardGlyphName = recoverGlyphName(glyphName, glyphsUnicodeMap);
if (standardGlyphName !== glyphName) {
glyphId = glyphNames.indexOf(standardGlyphName);
}
}
charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
}
}glyphsUnicodeMap ??= getGlyphsUnicode() 用的是「只在第一次需要时构建」的写法:如果所有 /Differences 名字都能直接命中 glyphNames,整个字体根本不会触碰 Unicode 表。这对内嵌子集字体很常见,能省掉约四千项的表初始化。
Differences 覆盖基准编码:同一个 char code 先由基准编码给出名字,再被差异数组改写。这正是 PDF 规范 9.6.6.2 描述的「基础编码 + 差异」模型。
设计要点:映射函数接收
glyphNames而不是自己去解析字体程序,是这一层最重要的解耦。Type1 的/CharStrings、CFF 的 charset、TrueType 的post表都能产出一份「下标是 glyph id 的名字数组」,于是同一套 char code 到 glyph id 的规则可以服务三种字体格式。字体格式差异留在各自的解析器里,公共层只认名字数组。
normalizeFontName:字体名归一化
function normalizeFontName(name) {
return name.replaceAll(/[,_]/g, '-').replaceAll(/\s/g, '');
}两个正则分别处理两类噪声:
| 输入 | 输出 | 处理的问题 |
|---|---|---|
Arial,Bold | Arial-Bold | 有些生成器用逗号分隔字重与样式 |
Arial_Bold_Italic | Arial-Bold-Italic | 下划线被当作连字符使用 |
Times New Roman | TimesNewRoman | 字体名里混入空格 |
ArialMT,Italic | ArialMT-Italic | 逗号与样式后缀同时出现 |
它只做分隔符归一,不做大小写折叠,也不做别名替换。归一后的名字直接作为 standard_fonts.js 里各张表的键,例如 getStandardFontName 与 isKnownFontName 都先调用它再查表。font_substitutions.js 与 xfa_fonts.js 也用它生成替换字体表的键,因此同一个字体名在「标准字体识别」和「字体替换」两条路径上得到相同形状。
为什么不在解析字体字典时一次性归一化并存回 /BaseFont?因为 /BaseFont 还有别的用途:错误信息、字体缓存键、调试输出都希望看到原始名字。归一化是可逆性很差的变换(空格被删掉之后无法还原),只应在查表前临时做,而不是改写原始数据。
getVerticalPresentationForm:竖排呈现形式
竖排文本不能直接把横排字符旋转九十度,标点、括号、省略号在竖排里有专门的呈现形式码位。这张表由 getLookupTableFactory 包装,第一次调用时才构建:
const getVerticalPresentationForm = getLookupTableFactory(t => {
t[0x2013] = 0xfe32; // EN DASH
t[0x2014] = 0xfe31; // EM DASH
t[0x2025] = 0xfe30; // TWO DOT LEADER
t[0x2026] = 0xfe19; // HORIZONTAL ELLIPSIS
t[0x3001] = 0xfe11; // IDEOGRAPHIC COMMA
t[0x3002] = 0xfe12; // IDEOGRAPHIC FULL STOP
// ...
t[0xff5b] = 0xfe37; // FULLWIDTH LEFT CURLY BRACKET
t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET
});表里一共 33 项,覆盖破折号、省略号、中日韩标点、全角括号。使用时只有一处:
if (this.missingFile && this.vertical && fontChar.length === 1) {
const vertical = getVerticalPresentationForm()[fontChar.charCodeAt(0)];
if (vertical) {
fontChar = unicode = String.fromCharCode(vertical);
}
}条件很严格:只有字体文件缺失、竖排、单码位字符三个条件同时成立才替换。内嵌字体自带竖排字形,不需要这张表;多码位字符(例如组合序列)也没有一一对应的呈现形式。替换同时改写 fontChar 与 unicode,保证字形查找和文本提取看到同一个码位。
getLookupTableFactory 的行为也值得单独说明:第一次调用时创建无原型对象、执行初始化函数,然后把初始化函数置空;后续调用直接返回同一个对象。这张表不会进入 cleanup 的清理范围,因为 clearGlobalCaches 只清原语缓存、pattern 缓存与 Unicode 分类缓存;竖排映射是纯静态数据,与具体文档无关,清掉只会让下一次访问重新构建。真正需要随文档清理的是字体对象与 CMap 缓存,那些在 Catalog 与 PartialEvaluator 的清理路径里。
设计要点:这张表是「平台缺失时的补丁」,不是通用的竖排整形器。只在
missingFile时启用,意味着有字体文件时完全交给字体自己的 cmap;表本身也不需要覆盖全部竖排标点,只需要覆盖没有内嵌字体时最常遇到的那批。
compileType3Glyph:位图字形编译成路径
Type3 字体的字形不是轮廓,而是一段内容流,通常包含一个位图掩码。逐字形绘制位图掩码在 Canvas 上很贵,compileType3Glyph 把掩码一次性编译成矢量路径,之后按普通填充路径绘制。
入口先做两道保险:
// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
function compileType3Glyph({ data: img, width, height }) {
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
return null;
}
const POINT_TO_PROCESS_LIMIT = 1000;
// ...宽或高超过 1000 直接放弃,返回 null。这不是正确性限制,而是成本上限:编译算法要在 (width + 1) x (height + 1) 的点网格上做边界跟踪,大图会让点数量与遍历时间失控。函数内部还有 POINT_TO_PROCESS_LIMIT,边界点超过 1000 也返回 null,把「太复杂」与「太大」分开限制。
第一步是把按位打包的掩码展开成每像素一字节:
// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of img) {
let mask = 128;
while (mask > 0) {
data[pos++] = elem & mask ? 0 : 255;
mask >>= 1;
}
}lineSize 把每行按 8 位对齐;每个字节从最高位开始拆成 8 个像素,掩码位为 1 表示「不填充」,写成 0,否则写 255。展开后的 data 是后续边界判断的唯一输入。
第二步是在像素之间找「感兴趣的点」。点的坐标系比像素多一行一列,每个点根据周围四个像素的填充情况得到一个标志:
| 标志 | 含义 | 处理方式 |
|---|---|---|
| 1 | 右上与右下不同 | 作为轮廓起点或转向 |
| 2 | 左上与左下不同 | 作为轮廓起点或转向 |
| 4 | 左下与右下不同 | 作为轮廓起点或转向 |
| 8 | 左上与右上不同 | 作为轮廓起点或转向 |
| 5 | 十字交叉 | 特殊转向规则 |
| 10 | 另一种十字交叉 | 特殊转向规则 |
源码用一张 POINT_TYPES 查表把「四个邻接像素的组合」直接映射成点标志,避免逐点写条件分支。随后用 steps 数组按标志选择前进方向,沿边界走一圈,把每个转角写进 pathBuf:
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const pathBuf = [];
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
.scaleSelf(1 / width, -1 / height)
.translateSelf(0, -height);
// ...
const p0 = p;
let type = points[p];
do {
const step = steps[type];
do {
p += step;
} while (!points[p]);
const pp = points[p];
if (pp !== 5 && pp !== 10) {
// set new direction
type = pp;
// delete mark
points[p] = 0;
} else {
// type is 5 or 10, ie, a crossing
type = pp & ((0x33 * type) >> 4);
points[p] &= (type >> 2) | (type << 2);
}
x = p % width1;
y = (p / width1) | 0;
pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f);
if (!points[p]) {
--count;
}
} while (p0 !== p);
--i;内层 do-while 沿当前方向一直走到下一个有点的位置;pp 不是交叉点时直接把它当作新方向,并把该点清零,表示已经走过。遇到 5 或 10 时不能简单转向:交叉点有两条可能的出路,源码用位运算根据来向选出一条,同时改写该点的标志,让下一次从另一侧进入时走另一条边。count 只在点被清零时递减,循环到回到起点 p0 结束,--i 用来让外层重新扫描当前行,处理同一行里剩下的轮廓。
坐标变换用 DOMMatrix 完成:scaleSelf 把 x 缩放到 1 / width、把 y 缩放到 -1 / height 以翻转 y 轴,紧接着的 translateSelf(0, -height) 把平移写进已经缩放的坐标系。最终 y 等于 height 时映射到 0、y 等于 0 时映射到 1,像素坐标落在 [0, 1] 的字形空间。这一步必须与 Type3 字体的 FontMatrix 约定一致,否则字形会上下颠倒或缩放错误。
返回值直接就是 operator 参数:
return [OPS.rawFillPath, [new Float32Array(pathBuf)], new Float32Array([0, 0, width, height])];三项分别是「填充路径操作」「路径缓冲区」「包围盒」。调用方在 evaluator.js 里把它塞进 OPS.constructPath:
args = compileType3Glyph(imgData);
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
}
warn('Cannot compile Type3 glyph.');失败时并没有抛错,而是回退到 OPS.paintImageMaskXObject,用原始位图掩码绘制。这条回退路径保证:编译只是优化,不是正确性前提;任何尺寸或复杂度超限的字形仍然能显示,只是慢一些。
设计要点:
compileType3Glyph把「能不能优化」和「怎么优化」都封装在一个纯函数里,输入是掩码与宽高,输出是 operator 参数或null。调用方只需要判断返回值是否为空,不需要知道点跟踪、交叉转向、坐标变换中的任何细节;优化失败也不会污染 operator 列表的其余部分。
总结
fonts_utils.js 是字体层的共享基础设施:它不解析字体程序,也不决定字体类型,只提供各分支都需要的标志、名字表与算法。fonts.js 决定「这是什么字体」,type1_font.js 与 cff_font.js 解析字体程序,font_renderer.js 生成轮廓,而它们共同依赖这里的定义。
它在整条链路中的位置
| 阶段 | 模块 | 输入 | 输出 |
|---|---|---|---|
| 资源读取 | evaluator.js | /Font、/Encoding、/Flags | 字体属性对象 |
| 格式分派 | fonts.js | 字体属性、字体文件 | Font 子类 |
| 映射构建 | fonts_utils.js | 属性、内置编码、glyph 名字数组 | char code 到 glyph id 的表 |
| 轮廓生成 | type1_font.js、cff_font.js | 字体程序 | 字形轮廓与宽度 |
| 绘制 | font_renderer.js、evaluator | 字形、SEAC_ANALYSIS_ENABLED | 路径或位图 operator |
用一句话串起来:evaluator 读出字体属性,fonts.js 选出字体分支,fonts_utils.js 提供分支之间共享的映射规则与编译算法,最后 font_renderer 把 glyph id 变成可绘制的轮廓。
五条设计原则
- 只放跨分支的公共物:
FontFlags、字形序、字符映射被 Type1、CFF、TrueType 共同使用,所以放在fonts_utils.js;某一类字体独有的解析逻辑留在自己的文件里。 - 表按需构建,常量直接共享:
MacStandardGlyphOrdering是固定数组,模块加载即得;getVerticalPresentationForm与getGlyphsUnicode用工厂延迟构建,避免打开文档就付出全表初始化成本。 - 映射方向必须明确:
encodings.js的表是「char code 到名字」,MacStandardGlyphOrdering是「glyph id 到名字」。同名数组在不同方向下含义完全不同,函数签名用glyphNames这样的名字把方向写进调用契约。 - 优化失败要能退回原路径:
compileType3Glyph用尺寸与点数上限主动放弃,返回null,调用方回退到位图掩码绘制。优化只影响速度,不影响能否显示。 - 平台差异在解析阶段消化:
SEAC_ANALYSIS_ENABLED把seac合成拆成基字与重音,避免把 Linux 与 Windows 的渲染器差异留到绘制阶段;代价是解析器要理解字形合成,收益是渲染路径统一。
理解 fonts_utils.js 之后,再看 font.js 里 Type1 与 CFF 如何生成 glyphNames、fonts.js 如何把 /Differences 与基准编码合成最终映射,就会清楚它们共享的是同一套字符映射语义,而不是各自临时拼出来的表。下一篇进入 encodings.js,看那张「char code 到字形名」的基准表本身是怎么组织的。
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 源码解读: 共享工具层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 源码解读: 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日