整形与回退

整形与回退

最后更新 2026-11-02

  • 整形
  • shaping
  • 回退
  • fallback
  • 豆腐块
  • 复杂文字
EN中文
本页目录

排版有一段常被忽略的中间层:文本里是一串码位,屏幕上是一串字形,两者之间不是一对一。把码位序列变成字形序列的过程叫整形(shaping)。

整形做了什么

最少要做四件事:

  1. 映射:查 cmap,把码位换成字形;
  2. 替换:按 GSUB 的规则换字形(连字、上下文形式、本地化形式);
  3. 定位:按 GPOS 的规则挪位置(字偶距、标记附着、连写);
  4. 排序:双向文字(阿拉伯文、希伯来文)要按方向重排。

对拉丁文字,前三步大多时候什么也不做——所以整形这个词听起来很玄。真正离不开它的是阿拉伯文、印度系文字、以及带大量连写的书写系统。

复杂文字为什么需要它

阿拉伯文的字母有词首、词中、词尾、独立四种形态,选哪一种取决于它在词里的位置。这不是一个字符换一个字形能表达的,必须看上下文。

所以同一串码位,在不同语言环境下排出来可能完全不同——这也是 GSUB / GPOS 里 locl 与上下文替换存在的原因。

回退:找不到就换字体

整形之前,得先决定用哪个字体。浏览器的顺序大致是:

  1. font-family 列表里第一个装了 / 已加载的字体;
  2. 如果这个字体没有某个字符(cmap 里没有,或映射到 .notdef),只把这一个字符交给列表里下一个有它的字体;
  3. 列表走完,用系统默认字体;
  4. 系统也没有,画豆腐块。

关键点:回退是逐字符发生的。所以一行里可能来自三个字体,行高、基线、粗细都可能不一致——中文与西文混排时的跳就是这么来的。

这个字是谁画的

这是最容易被误解的一点:你没法在 CSS 里指定这个字必须用那个字体画。能指定的只有候选列表,实际用哪个由浏览器按上面的规则决定。

所以:

  • 正常显示不等于用的就是选中的那个字体;
  • 一个字体有 cmap 映射,但轮廓为空,浏览器仍然会回退——你看到的不是它画的;
  • 本站的字符抽屉会明确区分在字体里与用这个字体渲染出来是什么样。

豆腐块与空轮廓

  • 豆腐块:所有字体都没有这个字符时,浏览器画的方框(U+FFFD 是替换字符,又是另一回事);
  • 空轮廓:字体声明了映射,但字形里什么也没有。看起来和缺字一样,原因不同。

判断方法:查 cmap(本站的覆盖率就是这么算的)。但覆盖率只能告诉你有映射,告诉不了画得出来——这是它已知的局限。

在本站怎么观察

  • 本机字体:取到字节后用 FontFace 注册,渲染就是这个字体画的,可以直接对比;
  • 库里的字体:未载入时是页面字体画的(看起来像,但不是);载入后才是真的;
  • 字符抽屉里的默认字体 / 这个字体两张对照图,就是把这件事摆出来给你看。

延伸阅读

  • OpenType 规范:整形(shaping)与 GSUB / GPOS
  • HarfBuzz 文档:一个广泛使用的整形引擎
  • 本站:GSUB / GPOS、字符属性