改进了字体回退

Katie Hempenius
Katie Hempenius

摘要

本文将深入探讨字体回退和 size-adjustascent-overridedescent-overrideline-gap-override API。借助这些 API,您可以使用本地字体创建与 Web 字体的尺寸大致或完全匹配的后备字体。这可以减少或消除由字体切换导致的布局偏移。

如果您不想阅读本文,可以使用以下工具立即开始使用这些 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 可以创建与其 Web 字体对应项占用相同空间的后备字体,从而减少或消除此问题。

改进了字体回退

您可以通过以下两种方法生成“经过改进”的字体后备选项。较简单的方法仅使用字体指标替换 API。更复杂(但更强大)的方法同时使用了字体指标替换项 API 和 size-adjust。本文介绍了这两种方法。

字体指标替换项的运作方式

简介

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

  • 上行距离用于衡量字体的字形超出基准线的最远距离。
  • 下沉测量字体字形延伸到基线以下的最远距离。
  • 行间距(也称为“前导”)用于衡量连续两行文本之间的距离。

一张图表,描绘了字体的上行、下行和行间距。

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

字体测量值替换项在样式表中使用,如下所示:

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%;
}

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

计算字体指标替换值

以下等式可用于为给定 Web 字体生成字体指标替换项。字体指标替换项的值应写为百分比(例如 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”,则使用“typo”表中的指标,否则使用“win”表中的指标。
Firefox 如果设置了“USE_TYPO_METRICS”,则使用“typo”表中的指标,否则使用“hhea”表中的指标。 如果已设置“USE_TYPO_METRICS”,则使用“typo”表中的指标;否则,使用“win”表中的指标。
Safari 使用“hhea”表中的指标。 如果已设置“USE_TYPO_METRICS”,则使用“typo”表中的指标;否则,使用“win”表中的指标。

如需详细了解字体指标在不同操作系统中的运作方式,请参阅这篇介绍纵向指标的文章

跨设备兼容性

对于大多数字体(例如,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 的运作方式

简介

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

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

size-adjust 本身在改进字体后备选项方面应用有限:在大多数情况下,后备字体需要略微缩窄或展宽(而不是按比例缩放),才能与 Web 字体匹配。不过,将 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)

大多数输入(即上行、下行和行距)都可以直接从 Web 字体的元数据中读取。不过,avgCharacterWidth 需要近似估算。

近似平均字符宽度

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

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

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

不过,对所有字符进行均等加权可能会降低常用字母(例如 e)的宽度,并使不常用字母(例如 z)的宽度超支。

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

显示英语字母频率的图表。
英语字母出现频率

选择方法

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

  • 如果您刚开始优化字体后备方案,不妨单独使用字体指标替换项。虽然这是两种方法中最简单的一种,但通常效果足以显著减少与字体相关的布局偏移幅度。

  • 另一方面,如果您想提高精确率,并且愿意做更多工作和测试,那么使用 size-adjust 是一个不错的方法。如果实现得当,此方法可以有效消除与字体相关的布局偏移。

选择后备字体

本文介绍的技术依赖于使用字体指标替换项和 size-adjust 来转换广泛使用的本地字体,而不是尝试找到与网页字体非常接近的本地字体。在选择本地字体时,请务必注意,很少有字体能够广泛应用于本地,并且不会有一种字体在所有设备上都存在。

Arial 是 Sans Serif 字体的推荐回退字体,Times New Roman 是 Serif 字体的推荐回退字体。不过,这两种字体都不适用于 Android(Android 上唯一的系统字体是 Roboto)。

以下示例使用了三种回退字体来确保广泛的设备覆盖率:一种定位到 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 的体验有任何反馈,欢迎与我们联系。