木匣子

Web/Game/Programming/Life etc.

修复跨平台 Webfonts 渲染不一致的问题

Issue

最近开发的几个 Web 项目都遇到了同样的问题,在 Windows 上与设计稿 pixel-perfect 的网页,在 macOS 或 iPhone 上测试的时候却出现字体偏移现象,如下图所示:

font-rendereing-os-different-os

与这个 span 相关的的 css 大致如下:

.gs-header-menu__mega-toggle span {
    font-family: Larsseit;
    font-weight: 700;
    font-size: 1.6rem; // 16px in this case
    line-height: 1.33;
}

仔细观察发现 span 框的大小是完全一样的,其高度为 \(16 * 1.33 \approx 21\)。但里面的字体图元却渲染得不一样,即使把 line-height 设置成 1,也无法解决这一问题,在 macOS 中渲染出来的字体仍然偏上。这一问题对 CSS 来说已经超纲了。虽然我不是特别强迫症,但是这样的问题却非常吸引我,非解决不可。

于是我做了一些研究,找到了一篇非常硬核的文章:Deep dive CSS: font metrics, line-height and vertical-align,文中作者介绍了字体是如何被渲染到网页上,以及字体文件中的各种参数是如何影响渲染。

文章中提到了一个跨平台的开源软件:Font Forge。我们可以用它来直接编辑字体文件,让我们有机会深入字体文件,一探究竟。

Solution

首先我们要找到对应的字体文件。字体是设计师指定的,由客户在字体商店购买授权后发给我们的。为了方便在项目中使用,我将其 @font-face 重新编排,只要根据 font-family, font-weightfont-style 就能找到对应的字体文件:

@font-face {
    font-family: 'Larsseit';
    font-weight: 700;
    font-style: normal;
    src: url('#{$fonts-directory-path}/39A063_4_0.eot');
    src: url('#{$fonts-directory-path}/39A063_4_0.eot?#iefix') format('embedded-opentype'),
    url('#{$fonts-directory-path}/39A063_4_0.woff2') format('woff2'),
    url('#{$fonts-directory-path}/39A063_4_0.woff') format('woff'),
    url('#{$fonts-directory-path}/39A063_4_0.ttf') format('truetype'),
    url('#{$fonts-directory-path}/39A063_4_0.svg#wf') format('svg');    
}

商业字体通常提供了多种不同的格式,在兼容不同浏览器的同时作了一些优化。例如 woff2 可以在最新的浏览器上使用,文件小,加载快。其它格式可以参考这个页面:不台平台的 Webfonts 格式支持情况

客户提供的字体文件多达 60 个(1款字体 * 12个字型 * 5种格式)。由于字体的问题只出现在 macOS 和 iOS 上,所以我们只需要处理 woff2 格式的 12 个字形即可。

针对上面这个字体 39A063_4_0.woff2 我们用 Font Forge 编辑它。在 Element > Font Info... > General 面板中可以看到如下参数:

General Font Info

其中 Em Size: 1000 表示其字体的设计尺寸为 1000 单位。Ascent: 766 是一个辅助线高度,表示在基线上 766 单位,设计字体时顶部不要超过它,(例如带注音的字母ÉÅÑ)。Descent: 234 是另一个辅助线高度,表示在基线下 234 单位,设计设计时底部不要超过它,(例如有尾部的字母gjpqy)。如果对 em 这个单位有兴趣的话,可以阅读一下这篇文章

以上三个参数是供字体设计师在设计的时候使用,并不影响渲染,甚至在设计字体的时候,对某些非常张扬的字母,还可以不遵守这些辅助线。所以设计师在设计完字体的时候,还要另外设置另外的在渲染时使用的参数。这些参数在另一个页面:OS/2 > Metrics

OS/2 Metrics

对于如何设置这些个参数,业界也没有一个统一的说法。比较主流的实践有这两个,一个来自 Glyphs App,另一个来自 Google Fonts。我作为 Web 前端,自然是站队后者。

以 Google Fonts 的规范为例,需要调整参数如下:

  • Win Ascent/Win Descent:将其设置为字形中的最高和高低线,防止 Windows 裁剪字型。
  • Really use Typo metrics:启用该选项,引导 Windows 使用 Typo * 来控制字体的基线和行高。
  • Typo Ascent/Typo Descent:设置 Ascent 为 Cap Height 与 Ascender Height 中较大的一个。设置 Descent 为 Ascent - EmSize。
  • HHead Ascent/HHead Descent:保持与 Typo * 一致,确保 macOS 与 Windows 有相同的渲染效果。

依照这个规范,对比我们现在使用的这个字体。之所以在 Windows 和 macOS 上有不同的渲染效果是因为没有启用 Really use Typo metrics,导致 Windows 上使用的是 Win * 的基线,而 macOS 使用的是 HHead * 的基线。所以只要将 Really use Typo metrics 钩上,保存并导出字体即可。如果原本字体中的 Ascent/Descent 设置不符合要求,可以自行按上面的标准进行微调。

Example

以下是使用这种方法修复 URWDIN 字体的示例:

Windows

URDIN-windows
在 Windows 上修复前字体使用的是 Win * 基线,整体偏下。line-height: 1 时溢出 span 框;
修复后使用 Typo * 基线,与 macOS 表现一致。

macOS

URDIN-macos
在 macOS 上修复前后使用的都是 HHea * 基线,没有区别。
与修复后的 Windows 示例一致。

但是如果要将带注音的字母ÉÅÑ也包括进去,则 Typo * 以及 HHea * 的 Ascent 就需要再往上调整了。(不过我们这个项目无须考虑这个问题。)


在研究的过程中,我还发现有一些在线工具可以帮你修复类似的问题,例如 Font Squirrel Generator。但是这些工具会将商业字体加入黑名单来避免不必要的版权纠纷。如果使用的是开源的字体,没有版权限制的话,到是可以试试。通常开源字体有社区维护,被广大用户使用。经过反馈和修改,几轮迭代后质量反而更好,例如思源系列