2019-12-01-fun-with-fonts-on-the-web.zh.md (7550B)
1 +++ 2 title = "字体配置万维网篇" 3 date = 2019-12-01 4 slug = "fun-with-fonts-on-the-web" 5 draft = false 6 +++ 7 8 用《字体配置浏览器篇》作为标题或许更为准确,不过现在的标题听起来更吸引人一些。渲染文本 [不是一件简单的事](https://gankra.github.io/blah/text-hates-you/) ,如果还要考虑书写系统之间的巨大差异(这大概得怪巴别塔)无异于雪上加霜。运行双语博客会导致字体选择的麻烦加倍,这里是我遇到的一些问题的汇总。 9 10 11 ## 空格侵略者 {#空格侵略者} 12 13 大多数浏览器会将 HTML 中的连续文本合并为一行,并在链接处加上空格。所以 14 15 ```html 16 <html>Line one and 17 line two.</html> 18 ``` 19 20 会被渲染为 21 22 ```text 23 Line one and line two. 24 ``` 25 26 这种一刀切的方法显然不适用与字符之间不带分隔的 CJK 语言。解决方案是为页面(或页面上的任何特定元素)指定 `lang` 属性,如下所示: 27 28 ```html 29 <html lang="zh">第一行和 30 第二行。</html> 31 ``` 32 33 如果你的浏览器足够聪明(例如 Firefox),渲染的结果就不会有额外的空格。但是,所有基于 Blink 的浏览器仍然顽固地将多余的空格塞进去,所以我只能像野蛮人那样继续写一段一行的源文件。尽管不是万能的解决方案,但是指定 `lang` 属性仍然具有启用特定于某种语言的CSS规则的额外好处,这稍后会派上用场。 34 35 36 ## 引号归来 {#引号归来} 37 38 如 [之前的日志](https://www.shimmy1996.com/zh/posts/2018-06-24-fun-with-fonts-in-emacs/) 所说, CJK 字体会将引号显示为全角字符,不同于拉丁字体。只要网页不尝试混搭字体,这就不会成为问题:只需使用特定于语言的字体栈就行。 39 40 ```css 41 body:lang(en) { 42 font-family: "Oxygen Sans", sans-serif; 43 } 44 45 body:lang(zh) { 46 font-family: "Noto Sans SC", sans-serif; 47 } 48 ``` 49 50 再加上匹配的 `lang` 属性,所有问题就都解决了。 Firefox 甚至允许为每种语言指定默认字体,所以仅使用后备字体(例如 `sans-serif` 或 `serif` )也可行,不一定要费心编写特定于语言的CSS。 51 52 那么,如果我想用 Oxygen Sans 来渲染拉丁字符,并用 Noto Sans SC 来渲染 CJK 字符怎么办?虽然看似没有问题,但像这样指定字体堆栈, 53 54 ```css 55 body:lang(zh) { 56 font-family: "Oxygen Sans", "Noto Sans SC", sans-serif; 57 } 58 ``` 59 60 会导致引号被 Oxygen Sans 渲染、显示为半角字符。我的解决方案是通过 `unicode-range` 定义一个涵盖了引号的替代字体, 61 62 ```css 63 @font-face { 64 font-family: "Noto Sans SC Override"; 65 unicode-range: U+2018-2019, U+201C-201D; 66 src: local("NotoSansCJKsc-Regular"); 67 } 68 ``` 69 70 并修改字体栈为 71 72 ```css 73 body:lang(zh) { 74 font-family: "Noto Sans SC Override", "Oxygen Sans", "Noto Sans SC", sans-serif; 75 } 76 ``` 77 78 这样我们就可以享受全角引号了! 79 80 81 ## 字体忍者 {#字体忍者} 82 83 字体文件通常都不小,对于 CJK 字体来说更是如此:刚才提到的 Noto Sans SC 的大小 [超过8MB](https://github.com/googlefonts/noto-cjk/blob/master/NotoSansSC-Regular.otf) 。尽管我已经下定主意要从自己的服务器上提供所有文件,考虑到我网站上的平均 HTML 文件大小更接近 8KB,这显得有些过头了。那么那些网络字体服务如何处理这一问题呢? 84 85 大多数网络字体服务的工作方式是在网站的样式表里添加一堆 [`@font-face` ](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face)定义,以从专用服务器上提取字体文件。为了减少所提供的文件大小, Google Fonts 会将字体文件大卸八块,并在 `@font-face` 里声明每一块所对应的 `unicode-range` (这正是它们处理 [CJK 字体](https://fonts.googleapis.com/css?family=Noto+Sans+SC) 的方式)。他们还将字体文件压缩为 WOFF2 以进一步缩减文件大小。而 [Adobe Fonts](https://fonts.adobe.com/) (以前称为 Typekit )似乎有一些 JavaScript 奇技淫巧,可以动态确定要从字体文件加载的字形。 86 87 博采众家之长,得益于这是一个静态站点,我们可以简单地统计所有用到的字符,并提供一个只包含这些字符的字体文件。所要用到的工具主要是 pyftsubset (属于 [fonttools](https://pypi.org/project/fonttools/) 下的一个组件)和 GNU AWK 。将字体压缩为 WOFF2 还需要 Brotli 压缩库。在 Arch Linux 下,获取这些程序需要安装 [python-fonttools](https://www.archlinux.org/packages/community/any/python-fonttools/) 、 [gawk](https://www.archlinux.org/packages/core/x86%5F64/gawk/) 、 [brotli](https://www.archlinux.org/packages/community/x86%5F64/brotli/) 和 [python-brotli](https://www.archlinux.org/packages/community/x86%5F64/python-brotli/) 。 88 89 收集生成的HTML文件中的所有使用的字形可以使用这条 shell 命令: 90 91 ```sh 92 find . -type f -name "*.html" -printf "%h/%f " | xargs -l awk 'BEGIN{FS="";ORS=""} {for(i=1;i<=NF;i++){chars[$(i)]=$(i);}} END{for(c in chars){print c;} }' > glyphs.txt 93 ``` 94 95 你可能需要 `export LANG=en_US.UTF-8` (或者其他 UTF-8 语言环境)以便正确处理某些字形。有了字形清单,我们就可以提取字体文件的有用部分并进行压缩: 96 97 ```sh 98 pyftsubset NotoSansSC-Regular.otf --text-file=glyphs.txt --flavor=woff2 --output-file=NotoSansSC-Regular.woff2 99 ``` 100 101 指定 `--no-hinting` 和 `--desubroutinize` 可以进一步减小生成文件的大小,但会降低字体的美观程度。拉丁字体也可以使用类似的技术来瘦身,例如只提取包含 ASCII 字符的部分(或将范围设为 `U+0000-00FF` 以涵盖 Extended ASCII 字符): 102 103 ```sh 104 pyftsubset Oxygen-Sans.ttf --unicodes="U+0000-007F" --flavor=woff2 --output-file=Oxygen-Sans.woff2 105 ``` 106 107 大部分字体管理器都可以用来检查最后生成文件中可用的字形,也可以使用这一 [在线检查器](http://torinak.com/font/lsfont.html) (不支持 WOFF2,但是可以先试着转为其他格式后查看,例如 WOFF)。 108 109 我还考虑过将字形按受欢迎程度划分为更多块。获取按出现次数排序的字形列表可以使用以下命令: 110 111 ```sh 112 find . -type f -name "*.html" -printf "%h/%f " | xargs -l awk 'BEGIN{FS=""} {for(i=1;i<=NF;i++){chars[$(i)]++;}} END{for(c in chars){printf "%06d %s\n", chars[c], c;}}' | sort -r > glyph-by-freq.txt 113 ``` 114 115 结果显示我的博客用到了大约 1000 个不同的汉字,其中大约 400 个出现了10次以上。由于上一步中获得的字体文件大小已经足够好,我没有继续进行拆分。 116 117 118 ## 孔中窥见真理之貌(好像没有啥不对) {#孔中窥见真理之貌-好像没有啥不对} 119 120 我最终将字体文件的总大小减少到了 250KB 左右,但这仍然比 HTML 文件大好几个数量级。虽然看到我的网站在所有设备和屏幕上都保持一致很让人开心,但是与页面大小增加上百倍的代价相比,我觉得这点好处不成比例。 121 122 费劲心思指定字体或许并不值得。如果你希望看到我眼中本站的样子的话,以下是我的常用字体: 123 124 - 比例拉丁字体: [Oxygen Sans](https://github.com/KDE/oxygen-fonts) 。注意 KDE 版本与 [Google Fonts 版本](https://fonts.google.com/specimen/Oxygen) 有一些微妙的区别,我更喜欢前者。 125 - 比例 CJK 字体: [Noto Sans CJK](https://www.google.com/get/noto/help/cjk/) ,即思源黑体。 126 - 等宽字体: [Iosevka](https://typeof.net/Iosevka/) ,确切地说是 ss09 样式。