StreamPDFStreamPDF
返回博客

PDF.js 源码解读: fonts_utils

2026年10月27日 · StreamPDF Team

PDF.js 源码解读: fonts_utils

前言

前面几篇章已经把「文档」这条线走完: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 里的位映射成数值掩码
字形序MacStandardGlyphOrdering258 个按 glyph id 排列的标准 Mac 字形名
字形名工具recoverGlyphName把 uniXXXX 之类非标准名恢复成标准名
字符映射type1FontGlyphMappingType1 与简单 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/utilOPS、DrawOPS、infoType3 编译结果、恢复失败的日志
encodingsgetEncoding、StandardEncodingtype1FontGlyphMapping 的基准编码
glyphlistgetGlyphsUnicode字形名反查与 /Differences 恢复
core_utilsgetLookupTableFactory竖排呈现形式表的懒加载
unicodegetUnicodeForGlyph从 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 的 CFFParserCFF 解析器处理 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 的位位置,而不是连续编号:

成员十进制位位置规范含义
FixedPitch11等宽字体
Serif22有衬线
Symbolic43符号字体,使用内置编码
Script84手写体
Nonsymbolic326非符号字体,使用标准编码
Italic647斜体
AllCap6553617全大写
SmallCap13107218小型大写
ForceBold26214419强制加粗

本版核心路径真正读取的只有四个:FixedPitch、Serif、Symbolic、Nonsymbolic。Italic、Script、AllCap、SmallCap、ForceBold 按规范表定义,但当前代码不拿它们做分支判断。注意 Symbolic 与 Nonsymbolic 在规范里应当互斥,现实中却有大量两者都置位的字体;evaluator.js 对这种情况有专门的回退:TrueType 字体同时置位且存在 /Differences 时,清掉 Symbolic,按非符号字体处理。

位消费位置用途
FixedPitchfonts.js 的 isMonospace、evaluator.js 的宽度推断等宽字体判断
Seriffonts.js 的 isSerifFont、evaluator.js衬线字体回退
Symbolictype1FontGlyphMapping、fonts.js选择内置编码还是标准编码
Nonsymbolicevaluator.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 建立默认编码
MacStandardGlyphOrderingglyph 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;
}

三步逻辑很清楚:

  1. 名字已经在标准字形表里,原样返回,这是绝大多数情况,只花一次属性查找。
  2. 不在表里,交给 getUnicodeForGlyph 尝试按 uniXXXX 或 uXXXX{XX} 解析出码位。
  3. 解析出码位后,反向遍历 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;
}

四个分支的选择顺序本身就是一条优先级规则:

判断顺序条件基准编码为什么放在这一层
1properties.isInternalFont字体程序自带编码标准 14 字体的内部编码最可信
2properties.baseEncodingNamegetEncoding 查到的表/Encoding 明确指定时优先于内置编码
3FontFlags.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,BoldArial-Bold有些生成器用逗号分隔字重与样式
Arial_Bold_ItalicArial-Bold-Italic下划线被当作连字符使用
Times New RomanTimesNewRoman字体名里混入空格
ArialMT,ItalicArialMT-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 变成可绘制的轮廓。

五条设计原则

  1. 只放跨分支的公共物:FontFlags、字形序、字符映射被 Type1、CFF、TrueType 共同使用,所以放在 fonts_utils.js;某一类字体独有的解析逻辑留在自己的文件里。
  2. 表按需构建,常量直接共享:MacStandardGlyphOrdering 是固定数组,模块加载即得;getVerticalPresentationForm 与 getGlyphsUnicode 用工厂延迟构建,避免打开文档就付出全表初始化成本。
  3. 映射方向必须明确:encodings.js 的表是「char code 到名字」,MacStandardGlyphOrdering 是「glyph id 到名字」。同名数组在不同方向下含义完全不同,函数签名用 glyphNames 这样的名字把方向写进调用契约。
  4. 优化失败要能退回原路径:compileType3Glyph 用尺寸与点数上限主动放弃,返回 null,调用方回退到位图掩码绘制。优化只影响速度,不影响能否显示。
  5. 平台差异在解析阶段消化:SEAC_ANALYSIS_ENABLED 把 seac 合成拆成基字与重音,避免把 Linux 与 Windows 的渲染器差异留到绘制阶段;代价是解析器要理解字形合成,收益是渲染路径统一。

理解 fonts_utils.js 之后,再看 font.js 里 Type1 与 CFF 如何生成 glyphNames、fonts.js 如何把 /Differences 与基准编码合成最终映射,就会清楚它们共享的是同一套字符映射语义,而不是各自临时拼出来的表。下一篇进入 encodings.js,看那张「char code 到字形名」的基准表本身是怎么组织的。

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

相关文章