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

前言
前面几篇已经把字体层的公共底座铺开:fonts_utils.js 提供 FontFlags、MacStandardGlyphOrdering 与 normalizeFontName,encodings.js 定义了几套内置编码,glyphlist.js 给出字形名到 Unicode 的对照,cmap.js 负责复合字体的字符码到 CID 或 Unicode 的翻译。这些模块解决的是「怎么编码」「怎么查字形名」「怎么解析 CMap」。但还有一类问题没有回答:当字体根本没有嵌入文件时,名字该怎么认、替代字体该怎么选、字形编号该怎么猜。
src/core/standard_fonts.js(1092 行)就是为这类问题准备的数据层。它几乎不含算法,主体是九张由工厂函数懒加载的查表,加上两个查询函数。它不解析字体文件,也不构造 Font 对象;它只把「哪些名字属于标准字体」「某个名字应该退化成谁」「某个 CID 大概率对应哪个 Unicode」这些领域知识集中起来。
用一句话概括:它是字体层的标准字体字典,把字体名归一化后的查表结果交给 evaluator.js 与 fonts.js,让没有嵌入文件的 PDF 仍能选出一个可用的替代字体并尽量保住字形。
把它称作「字典」而不是「模块」是有意的:字典的职责是收录与检索,不负责使用。真正决定用哪个字体、画哪个字形的,是调用它的解析器与字体构造器。
模块总览
standard_fonts.js 可以切成九块,每一块都是一张独立的表或一个极薄的查询函数:
| 区块 | 关键常量或函数 | 职责 |
|---|---|---|
| 标准字体表 | getStdFontMap | 标准 14 字体,以及 Arial、Times New Roman 等常见别名 |
| 文件名映射 | getFontNameToFileMap | 标准字体名到内置字体文件的对应关系 |
| 非标准字体表 | getNonStdFontMap | Calibri 一类未嵌入字体的退化目标 |
| 衬线名集合 | getSerifFonts | 用名字判断衬线倾向,弥补不可信的 /Flags |
| 符号名集合 | getSymbolsFonts | Symbol、Dingbats、Wingdings 一族的名字 |
| Mac 序字形表 | getGlyphMapForMacOrderedFonts | 按 Macintosh 标准字形序生成 CID 到 Unicode 的基表 |
| 标准字形表 | getGlyphMapForStandardFonts | Ghostscript 一类无映射 CID 字体的常用 CID 到 Unicode |
| 补充字形表 | 三个 getSupplementalGlyphMapFor... | Trebuchet、ArialBlack、Calibri 的稀疏覆盖 |
| 查询与导出 | getStandardFontName、isKnownFontName | 归一化名字后查表,并提供「是否认识这个字体名」的判断 |
如果按项数看,这九张表的分工更直观:
| 表 | 项数 | 键的类型 | 值的类型 | 是否生成式 |
|---|---|---|---|---|
getStdFontMap | 72 | 字体名字符串 | 标准字体名或别名 | 否 |
getFontNameToFileMap | 18 | 标准字体名 | 字体文件名 | 否 |
getNonStdFontMap | 50 | 非标准字体名 | 替代字体名 | 否 |
getSerifFonts | 134 | 族名 | 布尔值 | 否 |
getSymbolsFonts | 6 | 族名 | 布尔值 | 否 |
getGlyphMapForMacOrderedFonts | 最多 256 | glyph id 数字 | Unicode 码位 | 是 |
getGlyphMapForStandardFonts | 496 | CID 数字 | Unicode 码位 | 否 |
getSupplementalGlyphMapForTrebuchetMS | 75 | CID 数字 | Unicode 码位 | 否 |
getSupplementalGlyphMapForArialBlack | 3 | CID 数字 | Unicode 码位 | 否 |
getSupplementalGlyphMapForCalibri | 117 | CID 数字 | Unicode 码位 | 否 |
名字表与字形表的规模差异说明了两件事。名字表的键是有限的厂商命名,几十到一百多项就能覆盖主流情况;字形表的键是字体内部的槽位,理论上可以到几千,PDF.js 只收录真实样本里验证过的对应关系。生成式的 Mac 序表是唯一例外,它从 258 个标准字形名反查 Unicode,用循环换体积。
它在整条链路里的位置可以画成这样:
// 字体字典 /BaseFont、/Subtype、/FontDescriptor
// → evaluator.js 读取字体字典、决定是否使用系统字体
// → standard_fonts.js 本篇:名字表、替代表、字形映射表
// → fonts.js Font 家族:fallbackToSystemFont、toFontChar
// → font_substitutions.js 真正把名字落到系统字体或内置字体
// → canvas / 文本层 绘制字形、提取 Unicode版本说明:本篇严格按当前仓库这一版
standard_fonts.js讲解。本版导出 12 个成员,其中getGlyphMapForMacOrderedFonts与getSupplementalGlyphMapForTrebuchetMS是围绕 TrebuchetMS 字形序单独拆出的两表;fonts.js的 Trebuchet 分支会先套 Mac 序基表,再叠补充表,源码注释标注了 issue 21713。旧资料若只讲getGlyphMapForStandardFonts一张表,就会漏掉这条分支。另外,所有表都由getLookupTableFactory懒构造,但源码没有调用Object.freeze,只读是模块契约,不是运行时保证。
依赖方向
文件头的导入只有三个,全部指向底层公共模块:
import { MacStandardGlyphOrdering, normalizeFontName } from './fonts_utils.js';
import { getGlyphsUnicode } from './glyphlist.js';
import { getLookupTableFactory } from './core_utils.js';| 依赖 | 提供的能力 | 在本文件的使用点 |
|---|---|---|
fonts_utils | MacStandardGlyphOrdering、归一化 | Mac 序表的生成循环、两个查询函数 |
glyphlist | getGlyphsUnicode | 把 Mac 字形名翻成 Unicode 码位 |
core_utils | getLookupTableFactory | 九张表的懒加载包装 |
它不导入 fonts.js、evaluator.js、cmap.js,也不导入任何流或解析器。这条单向依赖让它可以被 Worker 内的多个模块安全引用,而不会把字体实现细节带进数据层。反过来,evaluator.js 与 fonts.js 都从它导入,font_substitutions.js 则通过参数接收已经算好的 standardFontName,不直接依赖本文件。
懒加载工厂与数据形状
九张表全部通过 getLookupTableFactory 创建。这个工厂不在本文件里,而在 core_utils.js,实现只有十行左右:
function getLookupTableFactory(initializer, useArray = false) {
let lookup;
return function () {
if (initializer) {
lookup = useArray ? [] : Object.create(null);
initializer(lookup);
initializer = null;
}
return lookup;
};
}它返回的不是表,而是一个取表函数。第一次调用时创建容器、执行初始化器,然后把 initializer 置空;之后的调用直接返回同一个 lookup。这是一种「首次访问才构造,此后永久复用」的单例模式,与 Catalog 里的 shadow 目标相同,只是作用域在模块级。
这里有几个容易忽略的细节:
| 细节 | 具体表现 | 影响 |
|---|---|---|
| 容器类型 | 默认 Object.create(null),数组模式才用 [] | 表没有原型链,toString 之类名字不会假命中 |
| 构造时机 | 第一次调用 getter | 打开文档时不构造,真正遇到字体时才构造 |
| 构造次数 | 初始化器执行一次后置空 | 同一模块实例内永远只有一份表 |
| 可变性 | 返回原对象,没有包装或冻结 | 调用方理论上能改表,实际只读 |
| 数字键 | t[2] = 10 最终变成字符串键 "2" | 查询用数字或字符串都可以,遍历时需还原 |
Object.create(null) 的选择与 primitives.js 的 NameCache 一致:字体名来自 PDF 内容,可能是 constructor、__proto__ 这类字符串。普通对象字面量会继承 Object.prototype,用 in 判断或直接下标都可能拿到非表项;无原型对象把这种风险降到零。注意 getSerifFonts 的判断在 evaluator.js 里用了 in 运算符,这正是无原型容器才能安全支撑的写法。
数字键的行为值得单独说明。getGlyphMapForStandardFonts 的写法是 t[2] = 10、t[2424] = 7821,JavaScript 会把数字键转成字符串属性。之后 applyStandardFontGlyphMap 里的 for (const charCode in glyphMap) 拿到的是字符串,所以下游必须写 map[+charCode] = glyphMap[charCode],用一元加号转回数字下标。这不是随手写的转换,而是对象表与数组表之间的必要桥接。
表的生命周期
这些表的生命周期与文档无关。它们在模块第一次被求值时登记初始化器,在第一次查询时构造,之后一直留在 Worker 的模块作用域里。打开第二份 PDF 不会重新构造,关闭第一份 PDF 也不会释放。这一点与 Catalog 里的 fontCache、standardFontDataCache 形成对比:
| 缓存或表 | 作用域 | 何时构造 | 何时清理 |
|---|---|---|---|
getStdFontMap 等九张表 | 模块级 | 首次调用 getter | 不清理,随 Worker 存活 |
Catalog.fontCache | 文档级 | 首次遇到字体 | 文档 cleanup |
Catalog.standardFontDataCache | 文档级 | 首次取回字体文件 | 文档 cleanup |
getFontSubstitution 的系统缓存 | 文档级 | 首次需要系统替代字体 | 文档 cleanup |
这个区分是合理的:九张表是不可变的领域知识,不随文档变化,也没有持有大块二进制;字体对象与取回的文件字节才是文档级资源,必须按文档回收。把领域知识做成模块级单例,避免了每开一份文档就重建一遍映射表。
表不是冻结对象
getLookupTableFactory 返回的是初始化后的原对象,没有 Object.freeze,也没有 Object.seal。也就是说,任何拿到表的调用方都能写入新键。源码依赖两层约束来维持只读:
- 初始化器执行完就置空,工厂不会再重新构造,也不会在构造后主动改写;
- 所有表常量都不直接导出,外部只能通过 getter 拿到引用,模块内部没有暴露写入接口。
这是契约式只读,不是运行时只读。它的好处是构造路径极短,没有冻结成本;代价是误写不会被立刻发现。对像 getGlyphMapForStandardFonts 这种 496 项的表,冻结一遍要遍历所有键,而它只在 Worker 内部被少数路径读取,PDF.js 选择了前者。
设计要点:懒加载工厂把「数据定义」和「数据实例化」分开。模块加载时只登记初始化器,不分配九张表的内存;真正需要某张表时才构造。字体数据在多数文档里只会用到其中两三张,这种按需构造避免了启动阶段无意义的分配,也让未被用到的表永远不进入堆。
标准 14 字体与别名表
getStdFontMap 是整个文件最核心的一张表。它先列出 PDF 规范里的标准 14 字体,再追加一大批商业字体的别名:
const getStdFontMap = getLookupTableFactory(function (t) {
// The standard 14 fonts:
t["Times-Roman"] = "Times-Roman";
t.Helvetica = "Helvetica";
t.Courier = "Courier";
t.Symbol = "Symbol";
t["Times-Bold"] = "Times-Bold";
t["Helvetica-Bold"] = "Helvetica-Bold";
t["Courier-Bold"] = "Courier-Bold";
t.ZapfDingbats = "ZapfDingbats";
t["Times-Italic"] = "Times-Italic";
t["Helvetica-Oblique"] = "Helvetica-Oblique";
t["Courier-Oblique"] = "Courier-Oblique";
t["Times-BoldItalic"] = "Times-BoldItalic";
t["Helvetica-BoldOblique"] = "Helvetica-BoldOblique";
t["Courier-BoldOblique"] = "Courier-BoldOblique";标准 14 字体是 PDF 规范里不需要嵌入文件也必须被查看器支持的字体集合,实际是 Times、Helvetica、Courier 三族各四种字形,再加 Symbol 与 ZapfDingbats 两个符号字体,共 14 个。表里的值大多与键相同,这不是冗余:它让「标准字体查询」有一个统一的返回形态,命中的键总能拿到一个可用的标准名。
| 族 | 常规 | 粗体 | 斜体 | 粗斜体 |
|---|---|---|---|---|
| Times | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| Helvetica | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| Courier | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| 符号 | Symbol | 无 | 无 | 无 |
| 符号 | ZapfDingbats | 无 | 无 | 无 |
表中的命名并不完全对称:Helvetica 与 Courier 用 Oblique 表示斜体,Times 用 Italic;Courier 的粗斜体叫 BoldOblique,而 Arial 的别名里又同时出现 BoldItalic 与 BoldOblique 两种拼写。这些不对称不是笔误,而是各家字体厂商的历史命名,表必须按真实名字登记,不能强行统一。
真正的价值在后面的「Extra mappings」。表一共 72 项,除 14 个标准名之外,其余都是别名归一化:
// Extra mappings
t.ArialNarrow = 'Helvetica';
t['ArialNarrow-Bold'] = 'Helvetica-Bold';
t['ArialNarrow-BoldItalic'] = 'Helvetica-BoldOblique';
t['ArialNarrow-Italic'] = 'Helvetica-Oblique';
t.ArialBlack = 'Helvetica';
t['ArialBlack-Bold'] = 'Helvetica-Bold';
t['ArialBlack-BoldItalic'] = 'Helvetica-BoldOblique';
t['ArialBlack-Italic'] = 'Helvetica-Oblique';
t['Arial-Black'] = 'Helvetica';
t['Arial-Black-Bold'] = 'Helvetica-Bold';
t['Arial-Black-BoldItalic'] = 'Helvetica-BoldOblique';
t['Arial-Black-Italic'] = 'Helvetica-Oblique';
t.Arial = 'Helvetica';
t['Arial-Bold'] = 'Helvetica-Bold';
t['Arial-BoldItalic'] = 'Helvetica-BoldOblique';
t['Arial-Italic'] = 'Helvetica-Oblique';
t.ArialMT = 'Helvetica';
t['Arial-BoldItalicMT'] = 'Helvetica-BoldOblique';
t['Arial-BoldMT'] = 'Helvetica-Bold';
t['Arial-ItalicMT'] = 'Helvetica-Oblique';这段别名不是简单的「Arial 等于 Helvetica」。它同时处理了四类差异:
| 差异类型 | 例子 | 处理方式 |
|---|---|---|
| 族名不同 | Arial、ArialMT、ArialUnicodeMS | 统一落到 Helvetica |
| 字重后缀不同 | Bold、BoldMT、Black | 统一落到 Helvetica-Bold |
| 斜体命名不同 | Italic、Oblique、BoldItalic | 统一落到 Helvetica-Oblique 系列 |
| 连字符差异 | Arial-Black 与 ArialBlack | 两个拼写都登记 |
这些别名覆盖了 Arial、ArialNarrow、ArialBlack、ArialUnicodeMS、CourierNew、CourierNewPS、TimesNewRoman、TimesNewRomanPS、HelveticaLTStd 等常见商业字体。它们的共同点是:PDF 里写了名字,却没有嵌入字形数据;查看器必须把它们当成某个标准字体来画。
| 原始族 | 常规落点 | 粗体落点 | 斜体落点 | 粗斜体落点 |
|---|---|---|---|---|
| Arial | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialNarrow | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialBlack | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialUnicodeMS | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| TimesNewRoman | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| TimesNewRomanPS | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| CourierNew | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| CourierNewPS | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| HelveticaLTStd | Helvetica | Helvetica-Bold | 无 | 无 |
这张表也解释了为什么 getStdFontMap 有 72 项而不是 14 项:同一个族在 PDF 里可能出现带 MT、PS、PSMT、UnicodeMS、Narrow、Black 等多种后缀,每一种都要单独登记。漏掉一个拼写,字体就会落回原名,既拿不到内置文件,也拿不到标准度量。
Symbol 一族还有一个特殊处理:Symbol-Bold、Symbol-BoldItalic、Symbol-Italic 全部映射回 Symbol。符号字体没有真正的字重与字形变化,强行保留样式名只会让下游找不到对应文件。
别名表不是穷举
这张表按「实际遇到过的 PDF」积累,不是按字体厂商目录穷举。72 项里,14 项是标准字体,58 项是别名;别名又集中在 Arial、Times New Roman、Courier New、Helvetica 四个族。它覆盖的是桌面出版与 Office 导出里最常见的名字,不保证覆盖所有商业字体。
| 分组 | 项数 | 代表 |
|---|---|---|
| 标准 14 | 14 | Times-Roman、Helvetica、Symbol |
| Arial 族别名 | 27 | Arial、ArialMT、ArialUnicodeMS |
| Times 族别名 | 15 | TimesNewRoman、TimesNewRomanPSMT |
| Courier 族别名 | 10 | CourierNew、CourierNewPSMT |
| Helvetica 族别名 | 3 | Helvetica-Italic、HelveticaLTStd-Bold |
| Symbol 样式别名 | 3 | Symbol-Bold、Symbol-Italic |
遇到表里没有的名字,getStandardFontName 返回 undefined,字体走系统替代或默认回退。这不是缺陷,而是有意的边界:把所有字体名都猜一个替代目标,会让错误更难发现;只登记验证过的名字,未知字体至少能保留原名,交给平台字体匹配。
normalizeFontName 在查表之前先做一次清洗:
function normalizeFontName(name) {
return name.replaceAll(/[,_]/g, '-').replaceAll(/\s/g, '');
}逗号与下划线都变成连字符,所有空白被删除。于是 Times New Roman 归一成 TimesNewRoman,正好命中表项;Times,New_Roman 归一成 Times-New-Roman,与前者不是同一个键,表里也没有这一项。注意它不转小写,所以表里的键必须与 PDF 里的实际大小写一致。这也是表里同时出现 ArialMT、Arial-BoldMT、TimesNewRomanPSMT-Bold 的原因:归一化只能抹平空白与两种分隔符,抹不平厂商的命名习惯。
设计要点:
getStdFontMap同时承担两种语义。对标准 14 字体,它是「这是规范保证可用的字体」;对 Arial 一族,它是「这虽然不在标准 14 里,但可以直接借 Helvetica 的字形与度量」。两种语义共用一张表,换来的是一个查询入口;代价是isStandardFont会把 Arial 也算成标准字体,下游必须用isNarrow、isMappedToStandardFont等额外判断来修正宽度重算。
标准字体文件名映射
getFontNameToFileMap 只有 18 项,把标准字体名映射到随 PDF.js 分发的字体文件:
// Contains mapping for standard fonts and xfa fonts.
const getFontNameToFileMap = getLookupTableFactory(function (t) {
t.Courier = 'FoxitFixed.pfb';
t['Courier-Bold'] = 'FoxitFixedBold.pfb';
t['Courier-BoldOblique'] = 'FoxitFixedBoldItalic.pfb';
t['Courier-Oblique'] = 'FoxitFixedItalic.pfb';
t.Helvetica = 'LiberationSans-Regular.ttf';
t['Helvetica-Bold'] = 'LiberationSans-Bold.ttf';
t['Helvetica-BoldOblique'] = 'LiberationSans-BoldItalic.ttf';
t['Helvetica-Oblique'] = 'LiberationSans-Italic.ttf';
t['Times-Roman'] = 'FoxitSerif.pfb';
t['Times-Bold'] = 'FoxitSerifBold.pfb';
t['Times-BoldItalic'] = 'FoxitSerifBoldItalic.pfb';
t['Times-Italic'] = 'FoxitSerifItalic.pfb';
t.Symbol = 'FoxitSymbol.pfb';
t.ZapfDingbats = 'FoxitDingbats.pfb';
t['LiberationSans-Regular'] = 'LiberationSans-Regular.ttf';
t['LiberationSans-Bold'] = 'LiberationSans-Bold.ttf';
t['LiberationSans-Italic'] = 'LiberationSans-Italic.ttf';
t['LiberationSans-BoldItalic'] = 'LiberationSans-BoldItalic.ttf';
});这张表有两个明显特征。第一,Courier、Times、Symbol、ZapfDingbats 用 Foxit 的 .pfb 文件,Helvetica 族用 Liberation Sans 的 .ttf 文件。第二,末尾额外登记了四个 LiberationSans-* 名字,对应 XFA 表单里可能直接写出的字体名。注释里的「standard fonts and xfa fonts」说的就是这件事。
它的消费点在 evaluator.js 的 fetchStandardFontData:
const standardFontNameToFileName = getFontNameToFileMap(),
filename = standardFontNameToFileName[name];
let data;
try {
if (this.options.useWorkerFetch) {
data = await fetchBinaryData(
`${this.options.standardFontDataUrl}${filename}`
);
} else {上游只有在 useSystemFonts 关闭、且名字不是 Symbol 或 ZapfDingbats 时才会走这里。也就是说,这张表服务于「用自己的字体文件渲染标准字体」这条路径;开启系统字体时,名字会交给 font_substitutions.js 去匹配本机字体,不需要内置文件。文件名不是路径,真正的目录由 standardFontDataUrl 决定,Worker 只负责拼出完整 URL 并取回字节。
| 表项来源 | 例子 | 文件类型 | 典型用途 |
|---|---|---|---|
| 标准 14 的衬线族 | Times-Roman | .pfb | 内置 Type1 轮廓 |
| 标准 14 的无衬线 | Helvetica | .ttf | 内置 TrueType 轮廓 |
| 符号字体 | Symbol、ZapfDingbats | .pfb | 特殊字符集 |
| XFA 备用名 | LiberationSans-Regular | .ttf | XFA 字体名直接命中 |
取回与缓存
fetchStandardFontData 的完整入口更能说明这张表在链路里的角色:
async fetchStandardFontData(name) {
const cachedData = this.standardFontDataCache.get(name);
if (cachedData) {
return new Stream(cachedData);
}
if (
this.options.useSystemFonts &&
name !== "Symbol" &&
name !== "ZapfDingbats"
) {
return null;
}
const standardFontNameToFileName = getFontNameToFileMap(),
filename = standardFontNameToFileName[name];
let data;缓存键是标准字体名,不是文件名。同一个 Helvetica 被多页引用时只取回一次字节;取回后存进 Catalog.standardFontDataCache,文档清理时统一释放。name 不在表里时,filename 是 undefined,后续请求会失败并返回 null,字体随后落到系统字体或默认替代路径。
这里还有一个容易忽略的条件:useSystemFonts 打开时,只有 Symbol 与 ZapfDingbats 仍然使用内置文件。原因是系统字体里通常没有这两个符号字体,而它们的字形又不能靠普通文本字体替代;其余标准字体交给系统字体匹配,省掉一次网络请求。
| 条件 | 是否取内置文件 | 原因 |
|---|---|---|
useSystemFonts 为假 | 是 | 没有系统字体可用,必须用内置文件 |
useSystemFonts 为真,名字是 Symbol | 是 | 系统字体通常没有符号字形 |
useSystemFonts 为真,名字是 ZapfDingbats | 是 | 同上 |
useSystemFonts 为真,其它标准名 | 否 | 交给 font_substitutions.js 匹配本机字体 |
XFA 的四个 LiberationSans 名字
表末的四个 LiberationSans-* 项不是标准 14 字体,而是 XFA 表单的备用名:
t['LiberationSans-Regular'] = 'LiberationSans-Regular.ttf';
t['LiberationSans-Bold'] = 'LiberationSans-Bold.ttf';
t['LiberationSans-Italic'] = 'LiberationSans-Italic.ttf';
t['LiberationSans-BoldItalic'] = 'LiberationSans-BoldItalic.ttf';XFA 表单由 XML 描述,字体名可能直接写成 LiberationSans-Regular,而不是 PDF 里的 /BaseFont 名字。xfa_fonts.js 在构造字体字典时会把这类名字带进来,fetchStandardFontData 再用这张表取回对应文件。如果没有这四项,XFA 表单里的默认无衬线字体就只能落到系统字体或默认回退,排版会与设计稿偏离。
这四个名字与 Helvetica 指向同一批 .ttf 文件,但键必须单独登记,因为 getFontNameToFileMap 是精确匹配,不做别名推导。Helvetica 的替代关系在 getStdFontMap 里,文件映射表只负责「名字到文件」这一层,不负责「名字到名字」。
非标准字体与替代链
getNonStdFontMap 收录 50 项,处理的是「不是标准 14,但经常被当成标准字体写在 PDF 里」的名字:
/**
* Holds the map of the non-standard fonts that might be included as
* a standard fonts without glyph data.
*/
const getNonStdFontMap = getLookupTableFactory(function (t) {
t.Calibri = "Helvetica";
t["Calibri-Bold"] = "Helvetica-Bold";
t["Calibri-BoldItalic"] = "Helvetica-BoldOblique";
t["Calibri-Italic"] = "Helvetica-Oblique";
t.CenturyGothic = "Helvetica";
t["CenturyGothic-Bold"] = "Helvetica-Bold";
t["CenturyGothic-BoldItalic"] = "Helvetica-BoldOblique";
t["CenturyGothic-Italic"] = "Helvetica-Oblique";
t.ComicSansMS = "Comic Sans MS";
t["ComicSansMS-Bold"] = "Comic Sans MS-Bold";
t["ComicSansMS-BoldItalic"] = "Comic Sans MS-BoldItalic";
t["ComicSansMS-Italic"] = "Comic Sans MS-Italic";
t.GillSansMT = "Helvetica";
t["GillSansMT-Bold"] = "Helvetica-Bold";
t["GillSansMT-BoldItalic"] = "Helvetica-BoldOblique";
t["GillSansMT-Italic"] = "Helvetica-Oblique";
t.Impact = "Helvetica";这张表与 getStdFontMap 的差别不在形式,而在结果是否仍然是标准字体。Calibri、CenturyGothic、GillSansMT、Impact、TrebuchetMS 都退化成 Helvetica 族;ComicSansMS 退化成带空格的 Comic Sans MS 族;LucidaConsole 退化成 Courier 族;MS Gothic、MS Mincho、MS PGothic、MS PMincho 退化成同名的日文字体族。后两类的结果不一定能命中 getStdFontMap,因此下游会把它们视为「被映射到标准字体」而非「本身是标准字体」。
| 原始族 | 替代目标 | 目标是否标准 | 说明 |
|---|---|---|---|
| Calibri | Helvetica 族 | 是 | Office 默认字体,常被无嵌入引用 |
| CenturyGothic | Helvetica 族 | 是 | 几何无衬线,形状接近 Helvetica |
| GillSansMT | Helvetica 族 | 是 | 英式无衬线,用 Helvetica 兜底 |
| Impact | Helvetica | 是 | 仅常规字重,没有斜体与粗体变体 |
| TrebuchetMS | Helvetica 族 | 是 | 此处只做名字替换,字形表另有分支 |
| ComicSansMS | Comic Sans MS 族 | 否 | 保留原名,交给系统字体匹配 |
| LucidaConsole | Courier 族 | 是 | 等宽控制台字体,落到 Courier |
| MS Gothic 系列 | MS Gothic 等同名族 | 否 | 日文 Windows 字体,保留族名 |
| MS Mincho 系列 | MS Mincho 等同名族 | 否 | 同上 |
| NuptialScript | Times-Italic | 是 | 手写体无法替代,退化为斜体衬线 |
这张表的 50 项里,MS Gothic 与 MS Mincho 系列共 16 项,是最大的一组;Calibri、CenturyGothic、GillSansMT、TrebuchetMS 四个四字形族合计 16 项;ItcSymbol 6 项、LucidaConsole 4 项、ComicSansMS 4 项;剩下的是 Impact、LucidaSans-Demi、NuptialScript、SegoeUISymbol 这类单项。把四种字重全部登记,是为了让替代目标也保留对应的字重,而不是把粗体正文画成常规体。
fonts.js 的 fallbackToSystemFont 把这条链写得很清楚:
const { name, type } = this;
let fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap(),
nonStdFontMap = getNonStdFontMap();
const isStandardFont = !!stdFontMap[fontName];
const isMappedToStandardFont = !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;查找顺序是标准表优先、非标准表其次、最后保留原名。isStandardFont 只看标准表;isMappedToStandardFont 要求非标准表的结果还能在标准表里再命中一次。这个双重条件是必要的:Calibri 映射到 Helvetica,而 Helvetica 在标准表里,所以它为真;ComicSansMS 映射到 Comic Sans MS,后者不在标准表里,所以它为假。两者后续的宽度重算与 toFontChar 构造路径不同。
| 名字 | 标准表命中 | 非标准表结果 | 实际使用的名字 | 是否视为标准 |
|---|---|---|---|---|
Helvetica | 是 | 无 | Helvetica | 是 |
Arial | 是 | 无 | Helvetica | 是 |
Calibri | 否 | Helvetica | Helvetica | 是(间接) |
ComicSansMS | 否 | Comic Sans MS | Comic Sans MS | 否 |
MS-Gothic | 否 | MS Gothic | MS Gothic | 否 |
SomeUnknownFont | 否 | 无 | SomeUnknownFont | 否 |
设计要点:替代链把「识别」与「替换」分成两步。
getStdFontMap回答「这个名字是不是标准字体」,getNonStdFontMap回答「这个名字应该换成谁」。两步分开之后,isMappedToStandardFont可以只对真正落到标准字体上的替代返回真,避免把 Comic Sans MS 也当成标准字体去做度量修正。
衬线名与符号名集合
两张布尔集合表不负责替换,只负责给字体打标签。
衬线名集合
getSerifFonts 有 134 项,全部映射为 true:
const getSerifFonts = getLookupTableFactory(function (t) {
t["Adobe Jenson"] = true;
t["Adobe Text"] = true;
t.Albertus = true;
t.Aldus = true;
t.Alexandria = true;
t.Algerian = true;
t["American Typewriter"] = true;
t.Antiqua = true;
t.Apex = true;
t.Arno = true;
t.Aster = true;
t.Aurora = true;
t.Baskerville = true;
t.Bell = true;
t.Bembo = true;
t["Bembo Schoolbook"] = true;
t.Benguiat = true;
t["Berkeley Old Style"] = true;
t["Bernhard Modern"] = true;
t["Berthold City"] = true;
t.Bodoni = true;
t["Bauer Bodoni"] = true;
t["Book Antiqua"] = true;
t.Bookman = true;
t["Bordeaux Roman"] = true;
t["Californian FB"] = true;
t.Calisto = true;
t.Calvert = true;
t.Capitals = true;
t.Cambria = true;
t.Cartier = true;
t.Caslon = true;中段与末尾同样密集:
t.Garamond = true;
t.Gentium = true;
t.Georgia = true;
t.Gloucester = true;
t['Goudy Old Style'] = true;
t['Goudy Schoolbook'] = true;
t['Goudy Pro Font'] = true;
t.Granjon = true;
t['Guardian Egyptian'] = true;
t.Heather = true;
t.Hercules = true;
t['High Tower Text'] = true;
t.Hiroshige = true;
t['Hoefler Text'] = true;
t['Humana Serif'] = true;
t.Imprint = true;
t['Ionic No. 5'] = true;
t.Janson = true;
t.Joanna = true;
t.Korinna = true;
t.Lexicon = true;
t.LiberationSerif = true;
t['Liberation Serif'] = true;
t['Linux Libertine'] = true;
t.Literaturnaya = true;
t.Lucida = true;
t['Lucida Bright'] = true;
t.Melior = true;表里既有 Garamond、Baskerville、Bodoni、Caslon 这些传统衬线体,也有 Cambria、Constantia、Georgia、Times、LiberationSerif、FreeSerif 这些现代或自由字体,末尾还有 Bitstream Vera、Vera Serif、XITS 这类技术排版字体。它的判断单位是去掉样式后缀的族名,不是完整字体名。
| 类别 | 代表项 | 特征 |
|---|---|---|
| 旧式衬线 | Garamond、Caslon、Janson、Bembo | 笔画对比低,斜轴,传统书籍排版 |
| 过渡与现代 | Baskerville、Bodoni、Bauer Bodoni | 笔画对比高,竖轴,标题常用 |
| 埃及体 | Rockwell、Guardian Egyptian、Clarendon | 粗衬线,近似 slab serif |
| 现代系统体 | Georgia、Cambria、Constantia | 屏幕阅读优化,Windows 常见 |
| 自由字体 | LiberationSerif、FreeSerif、Gentium | 开源许可,常作为 Times 替代 |
| 技术排版 | Computer Modern、STIX、XITS | 数学与学术排版 |
由于集合只存族名,Times-BoldItalic、TimesNewRomanPSMT 这类完整名不会直接命中,必须先经过归一化与替代表查一次,再 split 掉样式后缀。这也是 fonts.js 那段循环比 evaluator.js 的 isSerifFont 更长的原因:它要同时处理子集前缀、厂商别名与样式后缀。
evaluator.js 的 isSerifFont 展示了这个用法:
isSerifFont(baseFontName) {
// Simulating descriptor flags attribute
const fontNameWoStyle = baseFontName.split("-", 1)[0];
return fontNameWoStyle in getSerifFonts() || /serif/i.test(fontNameWoStyle);
}split("-", 1)[0] 只取连字符之前的第一段,所以 Times-BoldItalic 会变成 Times;后面的正则再兜底识别名字里带 serif 的情况。fonts.js 里的判断更宽松一些,会先按 + 拆子集前缀,再归一化,经过标准表与非标准表替换,最后才 split 并查集合:
let isSerifFont = !!(properties.flags & FontFlags.Serif);
// Fallback to checking the font name, in order to improve text-selection,
// since the /Flags-entry is often wrong (fixes issue13845.pdf).
if (!isSerifFont && !properties.isSimulatedFlags) {
const stdFontMap = getStdFontMap(),
nonStdFontMap = getNonStdFontMap(),
serifFonts = getSerifFonts();
for (const namePart of name.split('+')) {
let fontName = normalizeFontName(namePart);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
fontName = fontName.split('-', 1)[0];
if (serifFonts[fontName]) {
isSerifFont = true;
break;
}
}
}这段代码的注释说明了动机:/Flags 里的 Serif 位经常是错的,如果只看标志位,文本层会选错字体族,选区与复制都受影响。名字查表是一个成本极低的二次确认,只在标志位没有声明衬线时才执行。
| 判断路径 | 输入 | 依据 | 结果用途 |
|---|---|---|---|
/Flags 的 Serif 位 | 字体描述符 | 生成器写入的位 | 首选判断 |
getSerifFonts 名字查表 | 去样式后的族名 | 本文件的 134 项集合 | 修正错误的标志位 |
serif 正则 | 族名 | 名字里是否含 serif | 兜底识别 |
getBaseFontMetrics 回退 | 无度量数据 | 衬线则 Times,否则 Helvetica | 决定默认宽度表 |
符号名集合
getSymbolsFonts 只有 6 项,却覆盖了 PDF 里最常见的符号字体家族:
const getSymbolsFonts = getLookupTableFactory(function (t) {
t.Dingbats = true;
t.Symbol = true;
t.ZapfDingbats = true;
t.Wingdings = true;
t['Wingdings-Bold'] = true;
t['Wingdings-Regular'] = true;
});它的使用点有两处。第一处是 evaluator.js 构造模拟字体描述符时,用 getSymbolsFonts()[fontNameWoStyle] 决定 FontFlags.Symbolic 还是 FontFlags.Nonsymbolic:
const flags =
(this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) |
(metrics.monospace ? FontFlags.FixedPitch : 0) |
(getSymbolsFonts()[fontNameWoStyle] ? FontFlags.Symbolic : FontFlags.Nonsymbolic);第二处是 fonts.js 的字形映射分流:名字匹配 Symbol 或 Dingbats 时,不再走普通编码,而是使用专门的符号编码表:
} else if (/Symbol/i.test(fontName)) {
const isCidKeyed =
this.composite && this.cidEncoding.startsWith("Identity-");
this.toFontChar = buildToFontChar(
isCidKeyed ? getSymbolGlyphIdEncoding() : SymbolSetEncoding,
getGlyphsUnicode(),
this.differences
);
} else if (/Dingbats/i.test(fontName)) {
this.toFontChar = buildToFontChar(
ZapfDingbatsEncoding,
getDingbatsGlyphsUnicode(),
this.differences
);
}这里正则匹配的是替代后的 fontName,不是原始名字。Symbol-Bold 先被标准表归一成 Symbol,再进入这个分支;Wingdings 虽然不在正则里,但因为它在符号集合中而获得 Symbolic 标志,后续走系统字体或符号路径。集合表与正则各管一段,并不重复。
注意集合里没有 Wingdings 2、Webdings 这类名字。它的角色是「已知的符号字体提示」,不是穷举所有符号字体;未命中的符号字体仍然可以靠 /Flags 的 Symbolic 位与 /Encoding 被识别,只是少了一条按名字兜底的路径。
设计要点:布尔集合表把「名字到类别」的知识独立出来,不参与替换。替换表改变字体名,类别表只回答是或否。两者混在一起会让
Calibri这种「替换后变成无衬线」与Times这种「本身就是衬线」难以区分,后续的度量与文本层判断就失去了稳定输入。
字形映射:Mac 序与标准字体
字体没有嵌入文件时,PDF 里的字符码往往只是 CID 或 glyph id,缺少可信的 CIDToGIDMap。这时 PDF.js 需要用一张经验表猜出「这个编号大概对应哪个 Unicode 字形」。本文件提供两张基表,三张补充表。
Mac 标准字形序基表
getGlyphMapForMacOrderedFonts 不是手写表,而是从 MacStandardGlyphOrdering 生成的:
// Base glyph map for fonts whose glyph ids follow the standard Macintosh
// ordering implied by a `post` table of format 1. Fonts such as TrebuchetMS can
// use part of this ordering with supplemental overrides.
const getGlyphMapForMacOrderedFonts = getLookupTableFactory(function (t) {
const glyphsUnicode = getGlyphsUnicode();
t[2] = 10;
for (let gid = 3; gid < MacStandardGlyphOrdering.length; gid++) {
const unicode = glyphsUnicode[MacStandardGlyphOrdering[gid]];
if (unicode !== undefined) {
t[gid] = unicode;
}
}
});MacStandardGlyphOrdering 是 258 个按 glyph id 排列的标准 Mac 字形名,开头几项是:
const MacStandardGlyphOrdering = [
".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl",
"numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft",
"parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "colon", "semicolon", "less", "equal", "greater", "question", "at",第 0 到第 2 项分别是 .notdef、.null、nonmarkingreturn。循环从 3 开始,因为前三项没有可用的 Unicode;t[2] = 10 单独把 nonmarkingreturn 映射成换行符,这是一条历史约定,避免文本层把回车位当成缺失字形。space 在 gid 3,对应 Unicode 32;exclam 在 gid 4,对应 33;此后基本按 ASCII 顺序推进,直到 gid 97 的 asciitilde。
前三项的处理值得单独记住:.notdef 是缺字形的占位,.null 是空字形,nonmarkingreturn 是不留墨的回车位。它们都不该出现在可见文本里,但 nonmarkingreturn 需要保留换行语义,所以源码给了它一个码位 10,而没有像前两项那样跳过。如果把它也跳过,某些按 Mac 序生成的字体在文本提取时会丢掉段落分隔。
循环最多写入 255 项,加上单独的 t[2] 一共不超过 256 项,实际数量取决于 glyphsUnicode 是否收录对应名字。它生成的是完整基表,适合字形编号遵循 Mac post 表格式 1 的字体。fonts.js 只在名字匹配 Trebuchet 时使用它,因为多数无嵌入的标准字体并不遵循这套序,直接套用会把字符映射到错误的字形。
标准字体字形表
getGlyphMapForStandardFonts 是本文件最长的一张表,共 496 项,键是 CID,值是 Unicode:
// Glyph map for well-known standard fonts. Sometimes Ghostscript uses CID
// fonts, but does not embed the CID to GID mapping. The mapping is incomplete
// for all glyphs, but common for some set of the standard fonts.
const getGlyphMapForStandardFonts = getLookupTableFactory(function (t) {
t[2] = 10;
t[3] = 32;
t[4] = 33;
t[5] = 34;
t[6] = 35;
t[7] = 36;
t[8] = 37;
t[9] = 38;
t[10] = 39;
t[11] = 40;
t[12] = 41;
t[13] = 42;
t[14] = 43;
t[15] = 44;
t[16] = 45;
t[17] = 46;
t[18] = 47;
t[19] = 48;
t[20] = 49;
t[21] = 50;
t[22] = 51;
t[23] = 52;
t[24] = 53;
t[25] = 54;
t[26] = 55;
t[27] = 56;
t[28] = 57;
t[29] = 58;
t[30] = 894;
t[31] = 60;前 30 项几乎是连续的 ASCII 区:CID 3 到 29 对应空格到冒号,CID 30 跳到 894,是「因此」符号,随后 31 到 97 继续对应小于号到波浪号。这个缺口说明它不是按字符码直排的表,而是按某套 CID 字体的实际字形槽位整理出来的。
中段覆盖 Latin-1 补充字符与常见标点,后段则伸向更广的字符集:
t[130] = 8224;
t[131] = 176;
t[132] = 162;
t[133] = 163;
t[134] = 167;
t[135] = 8226;
t[136] = 182;
t[137] = 223;
t[138] = 174;
t[139] = 169;
t[140] = 8482;
t[141] = 180;
t[142] = 168;
t[143] = 8800;
t[144] = 198;
t[145] = 216;
t[146] = 8734;这些值是 Unicode 码位,例如 8224 是剑号、8226 是项目符号、8482 是商标符号、8800 是不等于、8734 是无穷。表里还能看到希腊字母、西里尔字母、希伯来字母、阿拉伯字母的区段:
t[493] = 1039;
t[494] = 1040;
t[570] = 1040;
t[571] = 1041;
t[672] = 1488;
t[673] = 1489;
t[759] = 1617;
t[761] = 1776;
t[2424] = 7821;
t[2425] = 7822;
t[3282] = 11799;
t[3379] = 42785;
t[3416] = 8377;t[570] 到 t[633] 是西里尔大写字母区,t[672] 起是希伯来字母,t[759]、t[761] 一带有阿拉伯字符,t[2424] 一带则是更晚的补充区。表中的键并不连续,末尾甚至出现 3416 这样的高位 CID,说明它是从真实字体样本里积累出来的经验表,而不是按规范完整生成的映射。
| CID 区段 | 典型 Unicode 内容 | 说明 |
|---|---|---|
| 2 到 97 | 控制符、基本拉丁、ASCII 标点 | 最常用的一段,覆盖空格到波浪号 |
| 98 到 171 | Latin-1 补充与常见标点 | 含重音字母、货币、版权、商标 |
| 179 到 229 | 引号、破折号、数学符号 | 弯引号、省略号、不等号、无穷 |
| 305 到 337 | 希腊字母与制表符号 | 含求和、乘积、积分、方框绘制 |
| 493 到 633 | 西里尔字母 | 大写与小写区段 |
| 672 到 711 | 希伯来字母 | 连续排列的 Alef 到 Tav |
| 759 到 805 | 阿拉伯附加符号与展示形式 | 含 shadda 一类附加符号、连写形式 |
| 2424 到 3416 | 拉丁扩展、数学与货币补充 | 真实样本里出现的高位 CID,稀疏分布 |
这张分布表说明:它不是「CID 减某个常数就是 Unicode」的线性映射,而是每个区段各自成段、段间留空。PDF.js 没有尝试补全空隙,因为空隙对应的字形在真实样本里没有可靠证据;补错一个键,文本层就会输出错误字符,比缺失更难排查。
四层映射的叠加顺序
fonts.js 在 fallbackToSystemFont 里最终构造的不是一张表,而是四层叠加后的结果:
| 层 | 来源 | 作用 | 是否覆盖前层 |
|---|---|---|---|
| 1 | getGlyphMapForStandardFonts 或 Mac 序基表 | 给出 CID 到 Unicode 的初始猜测 | 初始层 |
| 2 | Trebuchet、ArialBlack、Calibri 补充表 | 修正已知的错位键 | 是 |
| 3 | /CIDToGIDMap | 用字体自带的映射修正下标 | 是 |
| 4 | /ToUnicode | 用文档自带的 Unicode 映射修正 | 是 |
第三层的代码体现得很直接:
if (cidToGidMap) {
for (const charCode in map) {
const cid = map[charCode];
if (cidToGidMap[cid] !== undefined) {
map[+charCode] = cidToGidMap[cid];
}
}
}它遍历的是已经叠加过基表与补充表的 map,把每个 CID 对应的值再通过 cidToGidMap 翻译一次。第四层则用 toUnicode.forEach 把 charCode 到 unicodeCharCode 的映射直接写入。层数越多,结果越接近真实字体;但只有第一、二层来自本文件,后两层来自 PDF 自身。
设计要点:这张表刻意保持「不完整」。完整映射需要为每个可能的 CID 找到正确字形,而真实 PDF 只用到其中一小部分;把所有已知样本的对应关系攒成稀疏表,既能覆盖常见字体,又把体积压在不到五百项。
fallbackToSystemFont在表之后还会用cidToGidMap与ToUnicode覆盖,说明它只是兜底起点,不是最终真相。
下游如何把它变成数组
fonts.js 的 applyStandardFontGlyphMap 负责把对象表摊平成数组:
function applyStandardFontGlyphMap(map, glyphMap) {
for (const charCode in glyphMap) {
map[+charCode] = glyphMap[charCode];
}
}map 是以下标为 CID 的数组,glyphMap 是字符串键的对象。+charCode 把 "2424" 转回 2424,写入对应下标。多次调用时后写的覆盖先写的,这正是补充表能够修正基表的机制。
在 fallbackToSystemFont 里,基表与补充表的组合方式按字体名分流:
if (/Trebuchet/i.test(name)) {
applyStandardFontGlyphMap(map, getGlyphMapForMacOrderedFonts());
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForTrebuchetMS());
} else {
applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts());
if (/Arial-?Black/i.test(name)) {
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForArialBlack());
} else if (/Calibri/i.test(name)) {
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForCalibri());
}
}Trebuchet 走完全不同的基表,ArialBlack 与 Calibri 则在标准基表上打补丁。这个分流顺序不能颠倒:先铺大表,再叠小表,小表才有修正效果;如果把补充表放在前面,基表会把同样的键覆盖回去。
补充字形表:Trebuchet、ArialBlack、Calibri
三张补充表的规模差异很大,但结构完全相同:只登记需要修正的键。
TrebuchetMS 补充表
TrebuchetMS 的注释说明了它的策略:
// TrebuchetMS largely follows the standard Macintosh glyph ordering through
// glyph id 235, with the exceptions below. Additional mappings for the affected
// Latin glyph repertoire follow.
const getSupplementalGlyphMapForTrebuchetMS = getLookupTableFactory(
function (t) {
// A few of the standard Macintosh glyph slots hold a different, but
// similar looking, glyph in TrebuchetMS.
t[151] = 956;
t[159] = 937;
t[168] = 916;
t[189] = 8364;
t[195] = 8729;
t[218] = 713;
t[236] = 222;
t[237] = 254;
t[238] = 8722;
t[239] = 185;
t[240] = 178;
t[241] = 179;前六项是「同一个槽位放着形近但不同的字形」,例如 956 是希腊字母 mu,937 是 Omega,916 是 Delta,8364 是欧元符号,8729 是圆点运算符,713 是上横线。从 236 开始则是 Mac 序之外的扩展区,覆盖 thorn、minus、上标数字、分数、钩形符号等拉丁字符。
这张表共 75 项,使用方式是「先 Mac 序基表,再本表覆盖」。注释里的「largely follows」是关键:Trebuchet 与 Mac 序大部分一致,所以可以复用基表;不一致的部分才需要逐项修正。fonts.js 的源码注释直接给出过一个真实后果:如果误用标准字体基表,š 会被映射成 ž(issue 21713)。
ArialBlack 补充表
getSupplementalGlyphMapForArialBlack 只有 3 项:
// The glyph map for ArialBlack differs slightly from the glyph map used for
// other well-known standard fonts. Hence we use this (incomplete) CID to GID
// mapping to adjust the glyph map for non-embedded ArialBlack fonts.
const getSupplementalGlyphMapForArialBlack = getLookupTableFactory(function (t) {
t[227] = 322;
t[264] = 261;
t[291] = 346;
});三项分别修正 227、264、291 三个 CID:值 322 是 U+0142 的 ł,261 是 U+0105 的 ą,346 是 U+015A 的 Ś。表小到可以一眼看完,却足以让 ArialBlack 的几个关键拉丁扩展字形落到正确位置。它说明补充表不需要成体系,只需要覆盖已知的错位点。
Calibri 补充表
getSupplementalGlyphMapForCalibri 有 117 项,是最大的补充表:
// The glyph map for Calibri (a Windows font) differs from the glyph map used
// in the standard fonts. Hence we use this (incomplete) CID to GID mapping to
// adjust the glyph map for non-embedded Calibri fonts.
const getSupplementalGlyphMapForCalibri = getLookupTableFactory(function (t) {
t[1] = 32;
t[4] = 65;
t[5] = 192;
t[6] = 193;
t[9] = 196;
t[17] = 66;
t[18] = 67;
t[21] = 268;
t[24] = 68;
t[28] = 69;
t[29] = 200;
t[30] = 201;
t[32] = 282;
t[38] = 70;
t[39] = 71;
t[44] = 72;
t[47] = 73;
t[48] = 204;
t[49] = 205;
t[58] = 74;
t[60] = 75;
t[62] = 76;
t[68] = 77;
t[69] = 78;
t[75] = 79;
t[76] = 210;
t[80] = 214;
t[87] = 80;
t[89] = 81;
t[90] = 82;
t[92] = 344;它的 CID 编号从 1 开始,而不像标准表那样从 2 或 3 开始;字母 A 落在 4,B 落在 17,C 落在 18。这说明 Calibri 有一套自己的 CID 排序,无法靠标准表加少量补丁解决,只能用一张较大的表覆盖整个基本拉丁区。后半段还覆盖小写字母与数字、标点:
t[258] = 97;
t[259] = 224;
t[260] = 225;
t[263] = 228;
t[268] = 261;
t[271] = 98;
t[272] = 99;
t[273] = 263;
t[275] = 269;
t[282] = 100;
t[853] = 44;
t[855] = 58;
t[856] = 46;
t[876] = 47;
t[878] = 45;
t[882] = 45;
t[894] = 40;
t[895] = 41;
t[896] = 91;
t[897] = 93;
t[923] = 64;
t[940] = 163;
t[1004] = 48;
t[1005] = 49;
t[1006] = 50;小写 a 在 258,逗号在 853,数字 0 在 1004。键位跨度极大,说明 Calibri 的后段槽位还混入了标点与数字;这张表只登记有把握的 117 项,其余仍交给 cidToGidMap 与 ToUnicode 修正。
三张补充表放在一起,可以看出一条共同的设计线索:
| 字体 | 基表 | 补充项数 | 触发条件 | 修正范围 |
|---|---|---|---|---|
| TrebuchetMS | Mac 标准字形序 | 75 | 名字匹配 Trebuchet | 形近字符与扩展拉丁区 |
| ArialBlack | 标准字体字形表 | 3 | 名字匹配 Arial-?Black | 三个错位字母 |
| Calibri | 标准字体字形表 | 117 | 名字匹配 Calibri | 基本拉丁、数字与标点 |
分流条件为什么用正则
三个分支都匹配原始 name,不是已经替换过的 fontName。这一点很关键:TrebuchetMS 在 getNonStdFontMap 里会被替换成 Helvetica,如果分支看的是替换后的名字,就永远进不了 Trebuchet 的 Mac 序基表。原始名保留了厂商信息,替换名只负责选择可用字体,两者用途不同。
| 判断对象 | 来源 | 是否保留厂商信息 | 用途 |
|---|---|---|---|
name | 字体字典原始名 | 是 | 选择字形映射基表与补充表 |
fontName | 归一化并替换后 | 部分保留 | 选择内置文件或系统字体 |
loadedName | 最后 split 的族名 | 否 | CSS 字体名与内部标识 |
分支的正则都写得比较宽松。/Trebuchet/i 只要求名字里出现 Trebuchet,能覆盖 TrebuchetMS、Trebuchet MS-Bold 以及带子集前缀的形式;/Arial-?Black/i 同时接受 ArialBlack 与 Arial-Black,对应标准表里登记过的两种拼写;/Calibri/i 覆盖四个字重。宽松匹配的代价是可能误伤同名前缀的其它字体,但字体名空间里以这些字符串开头的商业字体极少,实际风险可以接受。
三个分支的优先级也值得注意。ArialBlack 与 Calibri 是互斥的 if / else if,同一时刻只会应用一张补充表;Trebuchet 则单独走外层 if,不会与它们同时命中。这个顺序与 getStdFontMap 里的别名不冲突,因为别名只改变 fontName,不影响原始 name 的正则判断。
设计要点:补充表不是补丁集合,而是可叠加的覆盖层。基表负责大多数键,补充表只负责已知差异;叠加顺序固定为「基表在前、补充表在后」。这种结构让新增一个字体特例只需要加一张小表,不必复制或修改整张基表,也让每张表的来源与适用范围保持可追溯。
查询函数、导出与调用面
文件末尾只有两个函数,逻辑都很薄:
function getStandardFontName(name) {
const fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap();
return stdFontMap[fontName];
}
function isKnownFontName(name) {
const fontName = normalizeFontName(name);
return !!(
getStdFontMap()[fontName] ||
getNonStdFontMap()[fontName] ||
getSerifFonts()[fontName] ||
getSymbolsFonts()[fontName]
);
}getStandardFontName 只查标准表,返回标准名或 undefined。它不查非标准表,因为调用方要的是「这个名字能否直接对应一个内置标准字体」。evaluator.js 用它拿到 standardFontName,再交给 fetchStandardFontData 或 getFontSubstitution。
isKnownFontName 是「四表并集」:标准表、非标准表、衬线集合、符号集合,任意一张命中就返回真。它不判断替代结果,只判断名字是否在知识库里。evaluator.js 用它在字体名与描述符名不一致时决定是否信任描述符:
if (
fontNameStr &&
baseFontStr &&
(baseFontStr.startsWith(fontNameStr) ||
(!isKnownFontName(fontNameStr) && isKnownFontName(baseFontStr)))
) {
fontName = null;
}
fontName ||= baseFont;这段判断处理两类真实情况。第一类是 fontNameStr 为 Arial、baseFontStr 为 Arial,Bold:描述符名是基础名的前缀,说明两者是同一个字体,采用哪个都可以。第二类是 fontNameStr 为 wg09np、baseFontStr 为 Wingdings-Regular:描述符名是生成器随手写的内部名,基础名才是可读的族名;此时 isKnownFontName 给出「内部名不认识、基础名认识」的结论,于是改用基础名。若两个名字都不认识,就不做替换,保留原始名字,避免把未知字体误判成已知。
归一化的边界
normalizeFontName 只做三种替换,因此查表时会出现命中与不命中的明确分界:
| 输入 | 归一化结果 | 是否命中标准表 | 原因 |
|---|---|---|---|
Times New Roman | TimesNewRoman | 是 | 空白被删除,正好等于表项 |
Times,New_Roman | Times-New-Roman | 否 | 逗号与下划线变成连字符,表里没有 |
Arial-BoldMT | Arial-BoldMT | 是 | 无空白,连字符保留 |
Arial Bold MT | ArialBoldMT | 否 | 空白删除后与表项不一致 |
Symbol-Bold | Symbol-Bold | 是 | 表里单独登记了该样式别名 |
ABCDEF+Times-Roman | ABCDEF+Times-Roman | 否 | 子集前缀不在表里,需要先剥离 |
最后一行说明了为什么 fonts.js 要先 name.split("+") 再查表。子集前缀是六位大写字母加一个加号,PDF.js 不把它当成字体名的一部分;直接拿完整名去查,永远命中不了。查询函数本身不负责剥离前缀,因为它不知道调用方处于哪条解析路径。
两个查询函数为什么不合并
getStandardFontName 与 isKnownFontName 看起来可以合成一个函数:先查四张表,再返回标准名或布尔值。源码没有这样做,因为两者的返回语义完全不同。
getStandardFontName 返回的是可继续传递的标准身份。调用方拿到它之后,会去取内置文件、去查度量、去交给系统替代模块;返回值必须是标准表里的名字,不能是 true 或原始名。isKnownFontName 返回的是信任判断。调用方只拿它做条件分支,不需要知道命中哪张表,也不需要标准名。
| 维度 | getStandardFontName | isKnownFontName |
|---|---|---|
| 查询范围 | 仅标准表 | 标准表、非标准表、衬线集合、符号集合 |
| 返回值 | 标准名字符串或 undefined | 布尔值 |
| 典型用途 | 取内置文件、查度量、系统替代 | 判断描述符名是否可信 |
| 未命中含义 | 不是可直接使用的标准字体 | 完全陌生,四类知识都没有记录 |
合并之后,调用方要么额外判断返回值是不是字符串,要么在需要标准名时再查一次标准表,反而多了一步。拆成两个小函数,代价只是一次重复的归一化,换来的是每个调用点都能直接使用结果。
导出清单把十二个成员分成三类:
export {
getFontNameToFileMap,
getGlyphMapForMacOrderedFonts,
getGlyphMapForStandardFonts,
getNonStdFontMap,
getSerifFonts,
getStandardFontName,
getStdFontMap,
getSupplementalGlyphMapForArialBlack,
getSupplementalGlyphMapForCalibri,
getSupplementalGlyphMapForTrebuchetMS,
getSymbolsFonts,
isKnownFontName,
};| 类别 | 成员 | 调用方 |
|---|---|---|
| 名字表 | getStdFontMap、getNonStdFontMap、getSerifFonts、getSymbolsFonts、getFontNameToFileMap | evaluator.js、fonts.js |
| 字形表 | getGlyphMapForMacOrderedFonts、getGlyphMapForStandardFonts、三张补充表 | fonts.js |
| 查询函数 | getStandardFontName、isKnownFontName | evaluator.js |
从调用面看,这个文件没有任何状态,也不接收 PDF 对象。所有函数都是「传名字、查表、返回结果」或「取表、由调用方叠加」。这让它可以被 Worker 内的多个字体路径共享,也让它容易测试:不需要构造 Dict,不需要流,只需要字符串。
设计要点:查询函数只做归一化与查表,不把替代链、宽度度量、字形映射混在一起。
getStandardFontName不知道 Calibri 会退化成 Helvetica,isKnownFontName不知道 Trebuchet 需要 Mac 序基表。职责边界清晰,调用方才能在不同阶段选择不同的表,而不是被一个「万能查询」绑死。
与相邻模块的契约
这个文件不持有状态,也不发起异步操作,因此它与相邻模块的契约可以用「输入什么、输出什么」完整描述。
对上游的依赖
| 上游模块 | 导入项 | 契约 |
|---|---|---|
fonts_utils | MacStandardGlyphOrdering | 提供 258 项的标准 Mac 字形名数组 |
fonts_utils | normalizeFontName | 提供空白删除与分隔符统一的纯函数 |
glyphlist | getGlyphsUnicode | 提供字形名到 Unicode 码位的懒加载表 |
core_utils | getLookupTableFactory | 提供「首次构造、之后复用」的包装器 |
四个依赖都是纯数据或纯函数,没有一个会读取 PDF、访问网络或修改全局状态。这让 standard_fonts.js 可以在任何线程、任何时机被调用,不依赖 XRef、PDFManager 或 PartialEvaluator 的生命周期。
对下游的输出
| 下游模块 | 使用的成员 | 使用方式 |
|---|---|---|
evaluator.js 字体名解析 | getStandardFontName、isKnownFontName | 决定标准名、判断描述符名是否可信 |
evaluator.js 度量 | getStdFontMap、getSerifFonts | 选择标准度量表,衬线则回退 Times |
evaluator.js 文件取回 | getFontNameToFileMap | 标准名到 .pfb 或 .ttf 文件名 |
evaluator.js 标志位 | getSymbolsFonts | 决定 Symbolic 标志 |
fonts.js 回退 | getStdFontMap、getNonStdFontMap、getSerifFonts | 计算 isStandardFont、fontName、衬线倾向 |
fonts.js 字形映射 | 五张字形表 | 构造 toFontChar 与 ToUnicodeMap |
getStandardFontName 在 evaluator.js 里出现多处,覆盖简单字体、复合字体与 CFF 字体分支;下面这段是简单字体分支:
const standardFontName = getStandardFontName(baseFontName);
let file = null;
if (standardFontName) {
file = await this.fetchStandardFontData(standardFontName);
properties.isInternalFont = !!file;
}
if (!properties.isInternalFont && this.options.useSystemFonts) {
properties.systemFontInfo = getFontSubstitution(
this.systemFontCache,
this.idFactory,
this.options.standardFontDataUrl,
baseFontName,
standardFontName,
type,
);
}这段代码把三条路径串了起来:先查标准名,再用标准名取内置文件;取不到且允许系统字体时,把原始名与标准名同时交给替代模块。getFontSubstitution 需要两者:原始名用于在替代表里匹配具体族,标准名用于再兜底一次标准字体。这个接口设计说明 standard_fonts.js 的输出不是最终决定,而是下游决策的一项输入。
getBaseFontMetrics 则展示了度量路径:
let fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap();
fontName = stdFontMap[fontName] || fontName;
const Metrics = getMetrics();
const glyphWidths =
Metrics[fontName] ?? Metrics[this.isSerifFont(name) ? 'Times-Roman' : 'Helvetica'];先归一化,再查标准表得到标准名,然后去 metrics.js 取宽度表;取不到时按衬线与否回退到 Times-Roman 或 Helvetica。这里没有用 getNonStdFontMap,因为度量表只认标准字体;非标准名会保持原名,随后落到默认回退。这个细节与 fonts.js 的 fallbackToSystemFont 不同,后者会用非标准表,因为它要的是可绘制的字体名,而不是标准度量。
不负责的事情
为了避免把职责扩大,这里也列出它明确不做的几件事:
| 不做的事 | 实际负责的模块 | 原因 |
|---|---|---|
读取 /BaseFont 与 /Flags | evaluator.js | 本文件不接触 PDF 对象 |
| 解析字体文件 | type1_font.js、cff_font.js、fonts.js | 本文件只提供映射数据 |
| 决定系统字体 | font_substitutions.js | 需要平台与字体列表信息 |
| 缓存取回的字体字节 | Catalog.standardFontDataCache | 文档级资源必须按文档释放 |
| 生成字形轮廓 | font_renderer.js、glyph.js | 本文件不涉及绘制 |
这条边界让本文件可以保持「纯数据加两个查询函数」的形态。任何需要 PDF 上下文、异步 IO 或平台信息的能力,都被推到相邻模块;数据层只回答领域问题。
缺失值怎么流动
本文件没有抛异常,也没有 null 检查。所有「不认识」都表现为 undefined 或 false,由调用方决定下一步:
| 调用 | 未命中时的返回值 | 调用方的处理 |
|---|---|---|
getStdFontMap()[name] | undefined | 继续查非标准表或保留原名 |
getNonStdFontMap()[name] | undefined | 保留原名 |
getSerifFonts()[name] | undefined | 视为非衬线 |
getSymbolsFonts()[name] | undefined | 视为非符号字体 |
getFontNameToFileMap()[name] | undefined | 拼出的 URL 缺文件名,取回失败后返回 null |
getStandardFontName(name) | undefined | 不取内置文件,直接进入系统字体分支 |
isKnownFontName(name) | false | 不信任该名字,保留原始名 |
这套约定与 Catalog 的容错思路一致:数据层不判断「这是不是错误」,只返回「有没有」。undefined 与空字符串、null 严格区分;表里没有的值不会用默认值填充,因为默认值会掩盖真实的名字拼写问题。
getFontNameToFileMap 的未命中尤其要注意。filename 是 undefined 时,fetchBinaryData 会拿到一个以 undefined 结尾的 URL,请求失败后 fetchStandardFontData 捕获异常并返回 null。字体不会因此报错,只会走 isInternalFont 为假的路径。这是有意的降级:一个未知字体的文件名缺失,不应该让整页渲染失败。
总结
standard_fonts.js 是 PDF.js 字体层的数据字典:向下依赖 fonts_utils 的归一化与 Mac 字形序、glyphlist 的字形名表;向上给 evaluator.js 提供名字识别与内置字体文件名,给 fonts.js 提供无嵌入字体的字形映射基表与补充表。它不解析字体文件,也不绘制字形,只把领域知识整理成懒加载的查表。
它在整条链路中的位置
| 阶段 | 模块 | 输入 | 输出 |
|---|---|---|---|
| 字典读取 | evaluator.js | /BaseFont、/Flags、/Encoding | 归一化后的字体名、字体类型 |
| 名字识别 | standard_fonts.js | 字体名 | 标准名、替代名、衬线或符号标签 |
| 文件选择 | getFontNameToFileMap | 标准名 | .pfb 或 .ttf 文件名 |
| 系统替代 | font_substitutions.js | 标准名与非标准名 | 本机字体信息或内置字体信息 |
| 字形映射 | fonts.js | CID、cidToGidMap、基表与补充表 | toFontChar、ToUnicodeMap |
| 绘制与提取 | canvas 与文本层 | 字形与 Unicode | 页面图像、可选中文本 |
用一句话串起来:evaluator.js 把字体字典变成名字,standard_fonts.js 把名字变成标准身份与替代目标,fonts.js 再把身份变成可绘制的字形映射。
数据规模的实际意义
把九张表加在一起,常驻内存的项数大约是一千二百项:名字表 280 项,字形表 691 项,外加生成式 Mac 序表最多 256 项。这个量级对 Worker 来说很小,却足以覆盖绝大多数无嵌入字体文档。
| 数据类别 | 项数范围 | 增长方式 | 维护成本 |
|---|---|---|---|
| 字体名字 | 280 左右 | 遇到新厂商名就追加 | 低,单行即可 |
| 字形映射 | 690 左右 | 从真实样本里补键 | 中,需要验证 Unicode 正确性 |
| 生成式 Mac 序 | 256 以内 | 由字形序与字形名表自动生成 | 极低,跟随上游表变化 |
数据规模也解释了为什么这些表可以完全静态。它们不需要数据库、不需要运行时下载、不需要按文档更新;打包进 Worker 脚本后,任何时候都能同步查询。对于需要快速打开文档的查看器,这比在解析阶段临时构建映射表要稳定得多。
一次典型查询的完整路径
假设 PDF 里写了一个未嵌入的 Arial,Bold,并且关闭了系统字体。链路大致如下:
// /BaseFont = "Arial,Bold",没有嵌入字体文件
const baseFontName = 'Arial,Bold';
const standardFontName = getStandardFontName(baseFontName);
// normalizeFontName("Arial,Bold") → "Arial-Bold"
// getStdFontMap()["Arial-Bold"] → "Helvetica-Bold"
const fileName = getFontNameToFileMap()[standardFontName];
// → "LiberationSans-Bold.ttf"
// evaluator 取回文件后,fonts.js 按名字构造 Font
// 若它是 CIDFontType2 + Identity 编码且没有完整 CIDToGIDMap:
applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts());这条路径里有三次查表,分别回答三个问题:这个名字是不是标准字体、对应的内置文件叫什么、没有字形映射时 CID 大概对应哪些 Unicode。三次查表都不依赖 PDF 对象,只依赖字符串与几张大表;真正的异步只发生在取回字体文件的那一步。
如果 baseFontName 是 Calibri,第一步会失败,因为 Calibri 不在标准表里;evaluator.js 随后把原始名交给 getFontSubstitution,由 font_substitutions.js 决定系统替代;如果关闭系统字体,字体最终走 fallbackToSystemFont,由非标准表把名字换成 Helvetica,再套标准字形表。路径不同,但每一步仍然只查本文件的一张表。
遇到问题时查哪张表
| 问题 | 应查的表 | 返回形态 |
|---|---|---|
| 这个名字是不是标准字体 | getStdFontMap | 标准名或 undefined |
| 未嵌入的字体应该换成谁 | getNonStdFontMap | 替代名或 undefined |
| 这个字体名有没有衬线倾向 | getSerifFonts | 布尔值 |
| 这个字体名是不是符号字体 | getSymbolsFonts | 布尔值 |
| 标准字体对应哪个内置文件 | getFontNameToFileMap | 文件名或 undefined |
| CID 大概对应哪个 Unicode | getGlyphMapForStandardFonts | 对象表 |
| 字形是否遵循 Mac 标准序 | getGlyphMapForMacOrderedFonts | 对象表 |
| 是否是 Trebuchet、ArialBlack、Calibri | 三张 getSupplementalGlyphMapFor... | 覆盖表 |
这张索引也适合当作读源码时的导航:先确认你要回答哪一类问题,再去找对应的工厂函数,不要在一张表里找另一张表的答案。
补充一句:表之间没有隐式优先级。标准表与非标准表的先后顺序由调用方决定,基表与补充表的先后顺序由 fonts.js 决定,衬线集合与符号集合则只提供布尔判断。把顺序写在调用方,而不是写进数据,意味着同一张表可以在不同路径里以不同方式组合。
六条设计原则
- 数据与算法分离:1092 行里绝大多数是表项,真正的逻辑只有工厂包装、归一化查表和两个查询函数。字体知识可以独立审阅、独立扩充,不需要读懂解析器。
- 懒加载而不是启动构造:九张表都由
getLookupTableFactory在首次调用时构造,之后复用。打开文档不会为未使用的字体支付内存与构造成本。 - 归一化只抹平分隔符:
normalizeFontName把逗号、下划线、空白统一成连字符,但保留大小写。表项必须按厂商实际拼写登记,查询结果才稳定。 - 标准身份与替代目标分离:
getStdFontMap回答「是不是标准字体」,getNonStdFontMap回答「应该换成谁」。两者分开,isMappedToStandardFont才能准确判断替代后是否仍落在标准字体上。 - 字形映射采用基表加覆盖层:Mac 序或标准表提供大多数键,Trebuchet、ArialBlack、Calibri 的补充表只修正已知差异,叠加顺序固定为基表在前、补充表在后。
- 映射表刻意不完整:
getGlyphMapForStandardFonts只有 496 项,Calibri 补充表只有 117 项。它们是无嵌入字体时的兜底起点,最终仍由cidToGidMap、ToUnicode与真实字体文件修正。
理解 standard_fonts.js 之后,再看 fonts.js 的 fallbackToSystemFont、evaluator.js 的 getBaseFontMetrics,以及 font_substitutions.js 的字体匹配,就会清楚它们共享的是同一批名字与映射,而不是各自维护一套字体知识。下一篇会继续留在字体层,进入真正构造 Font 家族的 fonts.js,看这些表如何变成 toFontChar、toUnicode 与字形宽度。
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 源码解读: 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日