改进了字体回退

凯蒂·亨佩纽斯 (Katie Hempenius)
Katie Hempenius

摘要

本文深入探讨了字体回退以及 size-adjustascent-overridedescent-overrideline-gap-override API。这些 API 让您可以使用本地字体创建与网页字体尺寸高度或完全匹配的回退字体。这样可以减少或消除由字体交换导致的布局偏移。

如果您希望跳过阅读本文,可以使用下面的一些工具来立即开始使用这些 API:

框架工具

  • @next/font:从 Next 13 开始,next/font 会自动使用字体指标替换值,并使用 size-adjust 提供匹配的字体回退。
  • @nuxtjs/fontaine:从 Nuxt 3 开始,您可以使用 nuxt/fontaine 自动生成匹配的字体回退,并将其插入 Nuxt 应用使用的样式表。

非框架工具

  • Fontaine:Fontaine 是一个库,可自动生成和插入使用字体指标替换值的字体回退。
  • 代码库包含由 Google Fonts 托管的所有字体的字体指标替换项。您可以将这些值复制并粘贴到您的样式表中。

背景

后备字体是一种字体,在主字体尚未加载或缺少呈现网页内容所需的字形时使用。例如,以下 CSS 表明应将 sans-serif 字体系列用作 "Roboto" 的后备字体。

font-family: "Roboto" , sans-serif;

回退字体可用于更快地呈现文本(即使用 font-display: swap)。因此,网页内容在早期易于阅读,而且使用起来越低,但一直以来,这也以布局不稳定为代价:在将回退字体换成网页字体时,通常会发生布局偏移。不过,下面讨论的新 API 可以减少或消除这一问题,只需创建与对应网页字体占用相同空间的回退字体即可。

改进了字体回退

生成“改进”的字体回退的方法有两种。更简单的方法仅使用字体指标替换 API。更复杂(但更强大)的方法同时使用字体指标替换 API 和 size-adjust。本文介绍了这两种方法。

字体指标替换的运作方式

简介

字体指标替换提供了一种替换字体的上层、下沉和行间距的方法:

  • 上升测量的是字体的字形超出基线的最远距离。
  • 下降用于衡量字体字形在基线以下的最远距离。
  • 行间距(也称为“前导”)用于衡量连续文本行之间的距离。

描绘字体的上浮、下落和线条间隙的示意图。

字体指标替换可用于替换后备字体的上半部分、下行间距和线间距,以与网页字体的上半部分、下半部分和线间距匹配。因此,网页字体和调整后的后备字体将始终具有相同的垂直尺寸。

字体指标替换值用于样式表中,如下所示:

body {
    font-family: Poppins, "fallback for poppins";
}

@font-face {
    font-family: "fallback for poppins";
    src: local("Times New Roman");
    ascent-override: 105%;
    descent-override: 35%;
    line-gap-override: 10%;
}

本文开头列出的工具可以生成正确的字体指标替换值。不过,您也可以自行计算这些值。

计算字体指标替换值

以下公式为给定网页字体生成字体指标替换值。字体指标替换值应以百分比(例如 105%)而非小数的形式编写。

ascent-override = ascent/unitsPerEm
descent-override = descent/unitsPerEm
line-gap-override = line-gap/unitsPerEm

例如,下面是 Poppins 字体的字体指标替换项:

/*
Poppins font metrics:
ascent = 1050
descent = 350
line-gap = 100
UPM: 1000
*/

ascent-override: 105%;  /* = 1050/1000 */
descent-override: 35%;  /* = 350/1000 */
line-gap-override: 10%; /* = 100/1000 */

ascentdescentline-gapunitsPerEm 的值都来自网页字体的元数据。本文的下一部分将介绍如何获取这些值。

读取字体表格

字体的元数据(具体而言是其字体表)包含计算其字体指标替换值所需的所有信息。

FontForge 中“字体信息”对话框的屏幕截图。对话框中会显示“Typo Ascent”“Typo Descent”和“Typo Line Gap”等字体指标。
使用 FontForge 查看字体元数据

以下是一些可用于读取字体元数据的工具:

  • fontkit 是专为 Node.js 构建的字体引擎。此代码段展示了如何使用 fontkit 计算字体指标替换值。
  • Capsize 是一个字体大小和布局库。Capsize 提供了一个 API,用于获取有关各种字体指标的信息。
  • fontdrop.info 是一个网站,可让您通过浏览器查看字体表格和其他字体相关信息。
  • Font Forge 是一款热门的桌面字体编辑器。如需查看 ascentdescentline-gap:打开 Font Info 对话框,选择 OS/2 菜单,然后选择 Metrics 标签页。如需查看 UPM,请打开 Font Info 对话框,然后选择 General 菜单。

了解字体表

您可能会注意到,“上升”之类的概念由多个指标引用。例如,有 hheaAscenttypoAscentwinAscent 指标。这是由于不同的操作系统采用不同的字体渲染方法造成的:OSX 设备上的程序通常使用 hhea* 字体指标,而 Windows 设备上的程序通常使用 typo*(也称为 sTypo*)或 win* 字体指标。

根据字体、浏览器和操作系统,字体将使用 hheatypowin 指标来呈现。

Mac Windows
Chromium 使用“hhea”表中的指标。 如果设置了“USE_TYPO_METRICS”,则使用“拼写错误”表格中的指标,否则使用“成功”表格中的指标。
Firefox 如果设置了“USE_TYPO_METRICS”,则会使用“拼写错误”表中的指标,否则使用“hhea”表中的指标。 如果设置了“USE_TYPO_METRICS”,则使用“拼写错误”表格中的指标,否则使用“成功”表格中的指标。
Safari 使用“hhea”表中的指标。 如果设置了“USE_TYPO_METRICS”,则使用“拼写错误”表格中的指标,否则使用“成功”表格中的指标。

如需详细了解字体指标在不同操作系统中的运作原理,请参阅这篇有关垂直指标的文章

跨设备兼容性

对于绝大多数字体(例如,Google Fonts 托管的约 90% 的字体),字体指标替换项可以安全地使用,而无需知道用户的操作系统:换言之,对于这些字体,无论 hheatypo 还是 win 指标适用,ascent-overridedescent-overridelinegap-override 的值都将完全相同。此代码库提供了关于此字体和不适用于哪些字体的信息。

如果您使用的字体需要为 OSX 和 Windows 设备使用一组单独的字体指标替换值,则只有在您能够根据用户的操作系统更改样式表的情况下,才建议使用字体指标替换值和 size-adjust

使用字体指标替换值

由于字体指标替换项是使用来自网页字体元数据(而非后备字体)的测量结果计算得出的,因此无论使用哪种字体作为后备字体,替换项都保持不变。例如:

body {
  font-family: "Poppins", "fallback for Poppins", "another fallback for Poppins";
}

@font-face {
  font-family: "fallback for Poppins";
  src: local("Arial");
  ascent-override: 105%;
  descent-override: 35%;
  line-gap-override: 10%;
}

@font-face {
  font-family: "another fallback for Poppins";
  src: local("Roboto");
  ascent-override: 105%;
  descent-override: 35%;
  line-gap-override: 10%;
}

大小调整功能的运作方式

简介

size-adjust CSS 描述符会按比例缩放字体字形的宽度和高度。例如,size-adjust: 200% 会将字体字形缩放为原始大小的两倍;size-adjust: 50% 会将字体字形缩放至其原始大小的一半。

显示使用 'size-adjust: 50%' 和 'size-adjust: 200%' 结果的示意图。

size-adjust 本身就可用于改进字体回退的应用有限:在大多数情况下,回退字体需要略微缩小或加宽(而不是按比例缩放),才能与网页字体相匹配。不过,结合使用 size-adjust 与字体指标替换项可让任意两种字体在水平和垂直方向上相互匹配。

size-adjust 在样式表中的用法如下:

@font-face {
  font-family: "fallback for poppins";
  src: local("Arial");
  size-adjust: 60.85099821%;
  ascent-override: 164.3358416%;
  descent-override: 57.51754455%;
  line-gap-override: 16.43358416%;
}

由于 size-adjust 的计算方式(详见下一部分),size-adjust 的值(以及相应的字体指标替换值)会因所使用的后备字体而异:

body {
  font-family: "Poppins", "fallback for Poppins", "another fallback for Poppins";
}

@font-face {
  font-family: poppins-fallback;
  src: local("Arial");
  size-adjust: 60.85099821%;
  ascent-override: 164.3358416%;
  descent-override: 57.51754455%;
  line-gap-override: 16.43358416%;
}

@font-face {
  font-family: poppins-fallback-android;
  src: local("Roboto");
  size-adjust: 55.5193474%:
  ascent-override: 180.1173909%;
  descent-override: 63.04108683%;
  line-gap-override: 18.01173909%;
}

计算“大小调整”和“字体”指标替换值

以下是用于计算 size-adjust 和字体指标替换值的公式:

size-adjust = avgCharacterWidth of web font / avgCharacterWidth of fallback font
ascent-override = web font ascent / (web font UPM * size-adjust)
descent-override = web font descent / (web font UPM * size-adjust)
line-gap-override = web font line-gap / (web font UPM * size-adjust)

其中大多数输入(即上升、下降和线条间隙)都可以直接从网页字体的元数据中读取。不过,avgCharacterWidth 需要近似值。

近似平均字符宽度

一般来说,平均字符宽度只能进行近似计算,但在某些情况下,也可以准确计算平均字符宽度:例如,在使用等宽字体时,或事先知道文本字符串的内容时。

计算 avgCharacterWidth 的一种简单方法是采用所有 [a-z\s] 字符的平均宽度。

 比较各个 Roboto [a-zs] 字形宽度的图表。
Roboto 字形的宽度

但是,为所有字符分配相同权重可能会降低常用字母(例如 e)的宽度,并导致不常用的字母(例如 z)的宽度超重。

要提高准确性,更复杂的方法是考虑字母频率,并改为计算 [a-z\s] 个字符的频率加权平均宽度。如需了解英语文本的字母频率和平均字长度,这篇文章提供了很好的参考参考。

一张显示英语字母频率的图表。
英语的字母频率

选择方法

本文讨论的两种方法各有优缺点:

  • 如果您要开始优化字体回退,单独使用字体指标替换是一种不错的方法。尽管这是两种方法中较简单的一种,但其功能通常足够强大,可以明显降低与字体相关的布局偏移的幅度。

  • 另一方面,如果您希望提高精确率,并且愿意执行更多工作和测试,则整合 size-adjust 是一个不错的使用方法。如果实现正确,此方法可有效消除与字体相关的布局偏移。

选择后备字体

本文中介绍的技术依赖于使用字体指标替换和 size-adjust 来转换广泛可用的本地字体,而不是尝试查找与网页字体非常接近的本地字体。在选择本地字体时,请务必注意,只有极少的字体能够在当地广泛使用,也没有任何单一字体在所有设备上都存在。

建议使用 Arial 作为 Sans Serif 字体的后备字体,而为 Serif 字体推荐使用 Times New Roman 的后备字体。不过,这两种字体都不适用于 Android(Roboto 是 Android 上唯一的系统字体)。

以下示例使用了三种后备字体来确保广泛的设备覆盖范围:一种针对 Windows/Mac 设备的回退字体、一种针对 Android 设备的回退字体,以及一种使用通用字体系列的回退字体。

body {
  font-family: "Poppins", poppins-fallback, poppins-fallback-android, sans-serif;
}

/*
Poppins font metrics:
- ascent = 1050
- descent = 350
- line-gap = 100
- UPM: 1000
AvgCharWidth:
- Poppins: 538.0103768
- Arial: 884.1438804
- Roboto: 969.0502537
*/

@font-face {
  font-family: poppins-fallback;
  src: local("Arial");
  size-adjust: 60.85099821%;
  ascent-override: 164.3358416%;
  descent-override: 57.51754455%;
  line-gap-override: 16.43358416%;
}

@font-face {
  font-family: poppins-fallback-android;
  src: local("Roboto");
  size-adjust: 55.5193474%:
  ascent-override: 180.1173909%;
  descent-override: 63.04108683%;
  line-gap-override: 18.01173909%;
}

征求反馈意见

如果您对使用字体指标替换项和 size-adjust 的体验有任何反馈,请与我们联系。