自从我的博客改用我新作的主题以来,有一个问题就一直萦绕在我的心头……
WebFont 缺字时怎么办:修复缺字回退与基线错位
发现了吗,看这个副标题,「钊钊」两个字,明显比其他的字大一圈!
而且不单单这一篇,其实许多文章,都出现了这样的问题。
这是为什么呢?
高级的 SF Pro SC
这个新主题是我一比一复刻了 Apple Newsroom 的样式制作而成。为了完全一致地达到 Apple 的文字排印效果,我使用了 Apple 为网站特制优化的中文字体「SF Pro SC」。
这款字体主要是对苹果目前系统字体「苹方-简」的修改版:缩小了汉字的大小,调整了标点符号的宽度,使得中英文混合排版以及网页排版更加美观。为了更加详细了解这个字体,并且方便下文的理解,推荐读者可以先阅读李瑞东老师的这篇博文:SF Pro SC 是什么字体?
然而,追求苹果那样优雅的高级感却是有代价的。这款字(对于非苹果官方)在使用上却有着一个很大的问题,就是字符数太少。
也许 4587 个中文字符型看着很多,但是……很遗憾不知道苹果是怎么选取这些字的范围的。常用的「姨」「驴」字都没有包含在内,而「蠟」「豔」「弢」这种奇怪的生僻字又是包含在内的。
这就带来了问题:博客写作中,太多的字,并不被包含在这个字体内。而一旦缺字触发回退(fallback),又因为 SF Pro SC 的字形被人为缩小过,回退后使用系统字体(在我的博客中,优先的回退字体是真正的「苹方-简」)就会显得大一圈。
我其实一开始就知道这个问题,不过一直懒着拖着没有想去解决。偶尔跳几个字大一点?……算了,好像也不是不能忍。
直到后来给主题加了副标题显示,看到自己朋友的名字显示出来这么奇怪……唉(这个「唉」也是字体里缺的)!还是想想怎么改善吧。
「补字」的拉锯战
我真正的需求:
- 保持视觉一致:回退是必然,但是应该保证回退时,字的大小和基线不能突变。
- 不想逐字 patch: 维护成本太高了,累死我。
- 不动原始字体:我不能去改这个「SF Pro SC」,本来用了就不礼貌了,再改更不礼貌了。而且苹果可能未来会更新,把字符数量提上去。
- 中文环境兼容性:中文字体家族本身差异很大,要考虑跨平台。
基于这些想法,我逐一尝试了下面的思路。
size-adjust:把回退缩小
似乎幸运的是,CSS 提供了size-adjust,可以对某个字体整体缩放包装成新字体。WOW,难道这个问题这么方便就能被我解决了?我只需要回退到其他的系统无衬线体,然后给它加个缩放不就好了?
遵照 Apple Newsroom 的样式,我原先的字体回退是:
1 | p { |
下面,我希望让字体优先回退到我包装的「缩小版」字体上。姑且把这个字体叫做「PingFang SC local」吧。将它写进正确的回退位置(注意在真正的 “PingFang SC” 前面)。
1 | p { |
在 CSS 里搞一个自定义字体:
1 | @font-face { |
见证奇迹的时刻!
奇迹发生了……一半。
字确实缩小了,但是,同样很明显的是,这几个字在往下掉。这看着真是难受!
我问问 GPT,它说可以用ascent-override/descent-override/line-gap-override这几个@font-face属性来解决。但我实际试了几次,发现根本没用。
CSS 微移:vertical-align / transform
怎么搞都搞不定,我想不开了,在这篇祝我生日快乐2023的文章里,把每一个会回退的字,都用<span class="font-fix">给包上了,然后写 CSS:
1 | .font-fix { |
嗯,看上去是对齐了,但其实这样会导致上下两行行高不一致。所以后来又换了:
1 | .font-fix { |
(注意,span 标签的 transform 不可用于 inline 对象的,所以先改成 inline-block。另外,这样一来,@font-face 的缩放也可以直接在这里用 scale 完成。)
这样确实齐平了(原谅我没有截图)。不失为一种……差强人意的办法。这是有效的,但是:
- 要把每个字揪出来单独用标签合上,逐字处理,麻烦,写新文章改老文章,都麻烦;
- 如果改成用 js 去检测,js 也不好写,对性能也不好;
- 不适合副标题,副标题是用 front matter 生成控制的,手动写 HTML 标签最多用于 Markdown 内部;
- 如果用鼠标滑动选中能看出来这个字的高度位置被上移过。
总之,挺麻烦的,再加上如果想要跨平台,Windows 上也没有苹方。我试着用「微软雅黑UI」作为回退方案,但是微软雅黑真是太丑了,而且只有 Light,Regular,Bold 三种字重,和我主题使用的 Regular,Medium,Semibold 都不相符,看起来很不协调,所以,最终不得不放弃这个方案。
改个字体用用
我一开始不是没想过这个选择——改一份字体出来用作网站字体。实在是想想就觉得麻烦!
但是现在不得不回到这个选择…… 淦!
其实这个方案是很不划算,中文字体普遍很大,连字形最简单的黑体至少也要 5MB 以上,这对网页加载速度来说是很不好的——太晚加载出字体显示会造成 Layout Shift,这在绝对会拉低网页评分。而且手动改了字体,只能托管在自己的服务器,就无法享受 Google Font 中直接引用的 CDN 加速和分包的优势。
经过简单的选择,用了思源黑体(现在也可以是 Noto Sans)作为改版的来源。理由如下:
- 看起来比较顺眼,各个字重的粗细和苹方比较一致;
- 开源的,放心改,不怕;
- 可变字体,我要三个字重不用拿 3 份字体文件,稍稍对网站性能好一点;
- 字符数量绝对够,字重够。要是哪天我想用到超级生僻字或者 Ultra Light 这种字重也能满足我。
那么,在 Google Fonts 中下载 Noto Sans SC,得到NotoSansSC-VariableFont_wght.ttf。
说起来,用 Font Creator 来批量修改字符好像比较方便。不过我没有,我用 Glyphs。那么,用 Glyphs 打开 Noto Sans SC,稍微修改一下不兼容的母版,测试一下可以正常导出可变字体的 woff2 字体文件。
下一步很关键。我们要进行缩放和字的量度(metrics)的调整,来达到字的「变小」。注意,如果直接把字缩小,只会造成每个字符字面率变小,排出来每个字之间都有空隙;因此,缩小字符型后,要缩小字符的宽度,这样才能密排。以及,不要同时改变字的高度,这样字排出来依然是大的。原理我不用讲了吧。
原本 Noto Sans SC 的字宽是 1000,依照 SF Pro SC 的数据,应当缩小为 887。学习一下 Glyphs 官方的脚本教程,让 GPT 辅助来写这个「缩放+改变字宽」的脚本:
1 | # MenuTitle: 对所有母版重做:缩放88.7% |
一些细节:
- 操作所有的「母版」;
- 只对宽度值为 1000 的字符型进行操作。嗯,姑且认为宽度为 1000 的都是中文或者中文相关字符,反正都是要缩小的;而拉丁和数字等变宽字符是不用的;
- 缩放的变换中心点坐标是 (500,0),注意基线的 y 坐标值是 0,而非下降部的高度。
使用此时导出的字体作为PingFang SC local,查看效果。
1 | @font-face { |
嗯,大致是可以的,感觉回退的字稍微比别的字高了一点。再做一下高度的移动:
1 | # MenuTitle: 对所有母版重做:上移30 |
最后参照 SF Pro SC 的数据修改量度数据。
大功告成。得到的 NotoSansSCVF.woff2 大小大约在 8MB 左右,作为回退字体,不分包也可以接受。就让这部分回退的字稍微等一等再加载出来吧。嗯,尽管有些字的风格和苹方那个不是很相符,也就这样吧。
现在,你正在看的这篇文章中这些字,应该都已经看起来正常的显示了。
觉得本文不错?
本文采用 CC BY-NC-SA 4.0 协议共享,欢迎分享与转载。