我一直用的是以前积攒的一份 fontconfig 配置,主要功能就是设置对于无衬线字体优先用 Roboto 显示英文字体,然后回退到 Noto Sans CJK SC 显示中文字体,因为 Roboto 比 Noto 的英文字好看,以及对等宽字体优先用 Monaco。虽然大部分都是网上抄来的,我自己并不太懂,但是这个配置一直工作的还可以。直到我开启了 RIME 内置的 emoji 输入法,发现 emoji 显示成了空白。

一开始没觉得是什么很难解决的问题,在字体列表末尾加上 Noto Color Emoji 不就行了?不过事情要是真的这么简单,也就没必要写个博客记下来了。忘记当时怎么查的了,总之是搜到 一个 firefox 的 bug,提 bug 的人表示自己一直是开着 hintfull 和 antialias 的,关掉这个 Noto Color Emoji 才能显示。于是我看了一眼我的配置,也有这么一段:

<!-- 针对所有字体的默认设置,力求显示效果最好。 -->
<match target="font">
  <!-- 禁用内嵌点阵。 -->
  <edit name="embeddedbitmap" mode="assign">
    <bool>false</bool>
  </edit>
  <!-- 禁用合成粗体。 -->
  <edit name="embolden" mode="assign">
    <bool>false</bool>
  </edit>
  <!-- 设置显示器为 RGB 排列。 -->
  <edit name="rgba" mode="assign">
    <const>rgb</const>
  </edit>
  <!-- 开启轻度微调。 -->
  <edit name="hinting" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="autohint" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="hintstyle" mode="assign">
    <const>hintslight</const>
  </edit>
  <!-- 开启抗锯齿。 -->
  <edit name="antialias" mode="assign">
    <bool>true</bool>
  </edit>
</match>

那我觉得针对 Noto Color Emoji 关掉这几个选项就行了,正好这个 bug 下面就有人提供了配置:

<!-- Noto Color Emoji 不支持开启抗锯齿和微调,所以在全局开启之后还得给它关掉。 -->
<match target="font">
  <test name="family" qual="any">
    <string>Noto Color Emoji</string>
  </test>
  <edit name="scalable" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="embeddedbitmap" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="hinting" mode="assign">
    <bool>false</bool>
  </edit>
  <edit name="antialias" mode="assign">
    <bool>false</bool>
  </edit>
</match>

重新登录,输入法里面的 emoji 都显示出来了,我以为事情到这里就结束了,没想到噩梦才刚刚开始……

某一天用 firefox 打开 twitter,发现里面字体都是毛毛糙糙的,我觉得很离奇,明明我只是对 Noto Color Emoji 关了抗锯齿,为什么 twitter 的字体也受到影响了?而且其他页面的字体都是正常的,后来我又打开过一个微软的文档页面,也出现了类似的毛病。我研究了一下发现这两个页面都用了自定义的 webfont。可是 webfont 和 Noto Color Emoji 有什么关系?人类摸不着头发。

我查了很多博客文章,不过它们都关注在正常的字体顺序上,还没有人研究 webfont 有什么问题。于是我只能自己研究,不过我不能保证我对 fontconfig 的理解完全正确,这东西看起来非常复杂,不过我还是大致搞清楚了一些事情。引用我看到的 一篇英文博客 的标题:

当我凝视着 fontconfig 时,fontconfig 也在凝视着我。

首先我找到了这个叫 fontconfig 几个常见的坑 的文章,里面信息量挺大的,不过说实话没有解决掉我的问题——我明明只对匹配到 Noto Color Emoji 时候关掉了抗锯齿,为什么 webfont 也受到影响了呢?这篇文章主要介绍怎么调整字体列表顺序,不过说实话我没有遇到那么多障碍,简单的写法就工作了。同一个作者还写了 Color Emoji in openSUSE 的文章,不过总感觉是交待到一半,翻了文章列表没有后续了。而且这篇文章关心的也是字体 fallback 时候某些字符选到错误的字体的问题,和我这个不太相关。

然后还有 Linux fontconfig 的字体匹配 这篇文章,它介绍了浏览器是怎么和 fontconfig 合作匹配字体的,但是对于 webfont 也是没有提到。包括它的后续文章 用 fontconfig 治理 Linux 中的字体 也没提到和 Noto Color Emoji 相关的调整(可能和它用的 Twemoji 有关系)。

然后我尝试按照文章里的办法对 firefox 进行一个 fontconfig 的调试,FC_DEBUG=4 firefox --new-instance --private-window https://twitter.com/ 直接吐了 12 万行输出,我实在是不知道从哪里看了。翻了一下午胡乱修改了好几次配置也没找出来哪里有问题。就在我眼花的时候偶然看到一条有用的,fontconfig 吐出来一个匹配结果,family 是 Roboto, Noto Sans CJK SC, Noto Color Emoji, sans-serif… 等等一长串,style 是 。这个就很有趣了,因为我看 firefox 里面,twitter 用的 webfont 叫 TwitterChirp,它的 style 也是这么个 (我怀疑是个 bug),那这条可能就是相关的了。然后我发现这个匹配结果下面 antialias 是 false,但理论上来说这条又不是最先匹配到 Noto Color Emoji,怎么会应用 Noto Color Emoji 的设置呢?我猜测是因为默认 test 语句是 qual="any" 含义似乎是只要匹配到的有一个满足就应用,正常匹配一个字体应该是不会返回这么一长串 family 的(我用 fc-match 一般只返回一个符合条件的 family),虽然我不知道为什么 webfont 会返回这么一长串结果,总之我改成 qual="all" 也就是必须所有结果都是 Noto Color Emoji 时候才应用就好了对吧!然后重新登录,emoji 能显示,我以为已经胜利了!结果事情证明是半场开香槟……

当我以为完全没问题,正打算用 atom 整理一下这份配置的时候,突然发现不对劲了……Atom里面的中文字体失去了抗锯齿……你可以想像我当时心情有多绝望。具体为什么还是同样的我不知道,不过我猜测恰好是和 Linux fontconfig 的字体匹配 里面提到的 chromium 查询字体的奇葩逻辑有关系……可能只是它某次查询刚好漏掉了 Noto Sans CJK SC 匹配到了 Noto Color Emoji 然后就给了个 antialias false,然后再通过什么奇怪的 UI 字体查询找出了中文字体……反正总之我的心好累,我实在不知道该怎么解决了,我只想用个 emoji,我甚至还给 Noto Color Emoji 提了请求支持抗锯齿的 issue……然后我决定要不换成 Twemoji 算了。

Twemoji 倒也不是开箱即用,它不能禁用内嵌点阵字体,不过好歹这个不像抗锯齿那么要命,就算影响了其他字体大概也许能够忽略吧……于是我改成了这个。不过这时候我突然灵光一闪有了新主意。

按照前面那几篇博客和 这一篇手册翻译 的讲解,fontconfig match 的 target 分为三个阶段,第一是 scan,扫描字体文件,构建一个集合。然后是 pattern,按照规则构建一个字体列表。最后是 font,意味着已经挑出了需要的字体列表。所以调整字体回退顺序一般都放在 pattern 阶段。而网上大部分都把对于 freetype2 微调的选项放在了 font 阶段,不过我想放在 scan 阶段是不是也可以?这样扫描到 Noto Color Emoji 的时候就对它设置选项,也就不存在 family 列表可能有很多项的问题了。测试了一下发现基本一切正常,于是就把所有 freetype2 微调选项移到了 scan 阶段。(我不是 fontconfig 的专家,这一段要是有错误还希望指正。)

下面是我现在的配置,首先我配置文件放的位置是 /etc/fonts/local.conf,因为 /etc/fonts/fonts.conf 是发行版默认的设置,用来加载其他配置文件所以不能改,然后我想对所有用户都生效,所以没有放在我的家目录,Arch Linux 包含一个 /etc/fonts/conf.d/51-local.conf 文件,/etc/fonts/fonts.conf 会加载它,然后它再加载 /etc/fonts/local.conf

首先是 XML 开头必须要有的那些东西:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>

然后接下来就是针对所有字体的 freetype2 微调选项,按照上面介绍的放在 scan 阶段:

  <!-- 针对所有字体的默认设置,力求显示效果最好。 -->
  <match target="scan">
    <!-- 禁用内嵌点阵。 -->
    <edit name="embeddedbitmap" mode="assign">
      <bool>false</bool>
    </edit>
    <!-- 禁用合成粗体。 -->
    <edit name="embolden" mode="assign">
      <bool>false</bool>
    </edit>
    <!-- 设置显示器为 RGB 排列。 -->
    <edit name="rgba" mode="assign">
      <const>rgb</const>
    </edit>
    <!-- 开启轻度微调。 -->
    <edit name="hinting" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="autohint" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="hintstyle" mode="assign">
      <const>hintslight</const>
    </edit>
    <!-- 开启抗锯齿。 -->
    <edit name="antialias" mode="assign">
      <bool>true</bool>
    </edit>
  </match>

内嵌点阵的效果通常没有矢量绘制的效果好,合成粗体也只是个临时方案,微调什么的见仁见智了就。

然后对于 Twemoji 要打开内嵌点阵:

  <!-- 当然另一个简单方案是换成 Twemoji,不过它不能关这个。 -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Twemoji</string>
    </test>
    <edit name="embeddedbitmap" mode="assign">
      <bool>true</bool>
    </edit>
  </match>

对于 Noto Color Emoji 也是打开内嵌点阵关闭微调和抗锯齿:

  <!-- Noto Color Emoji 不支持开启抗锯齿和微调,所以在全局开启之后还得给它关掉。 -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Noto Color Emoji</string>
    </test>
    <edit name="scalable" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="embeddedbitmap" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="hinting" mode="assign">
      <bool>false</bool>
    </edit>
    <edit name="antialias" mode="assign">
      <bool>false</bool>
    </edit>
  </match>

我个人还有一个需求,Monaco 作为一个等宽字体竟然支持连字,导致某次我看别人的代码时候 fi 连在一起让我以为他没有对齐缩进,结果人家说是我的问题,十分尴尬,于是我在这里关掉了它。不过有些程序比如 firefox 是不支持这个的,解决方法是用 FontForge 编辑字体文件删掉连字相关的数据再导出……

  <!-- 我真不理解一个等宽字体要连字功能干嘛?故意变得不等宽? -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Monaco</string>
    </test>
    <edit name="fontfeatures" mode="append">
      <string>liga off</string>
      <string>dlig off</string>
    </edit>
  </match>

然后就是常见的默认字体代称(sans-serif,serif,monospace,emoji)回退列表了,我是按照 Roboto/Roboto Slab/Monaco -> Noto Sans/Serif (Mono) CJK SC -> Noto Color Emoji 的顺序回退的:

  <!-- 下面这一系列等同于 alias。 -->
  <!-- 默认无衬线字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>sans</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <match target="pattern">
    <test name="family" qual="any">
      <string>sans-serif</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认有衬线字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>serif</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto Slab</string>
      <string>Noto Serif CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认等宽字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>monospace</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Monaco</string>
      <string>Noto Sans Mono CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认 emoji 字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>emoji</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Noto Color Emoji</string>
    </edit>
  </match>

这里的写法和 <alias> 是等价的。设置完这个记得把所有软件的自定义字体设置里面无衬线设置为 sans-serif,有衬线设置为 serif,等宽设置为 monospace,这样才会遵守这里的回退顺序。

然后很多网站写的都是 Apple Color Emoji,我们这里自然是没有的,要换掉:

  <!-- 替换 Apple Color Emoji 字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>Apple Color Emoji</string>
    </test>
    <edit name="family" mode="assign" binding="same">
      <string>Noto Color Emoji</string>
    </edit>
  </match>

然后是个非常弱智的部分,B 站直播首页的 CSS 写死字体回退顺序为 Arial,Microsoft YaHei, "Microsoft Sans Serif",Microsoft SanSerf,微软雅黑,你没看错,这个不合格的前端连最后要加 sans-serif 保证兼容性都不知道,而且还把 Microsoft Sans Serif 拼错了,如果我是他的老板,我真想一拳打在他头上告诉他这个世界不是只有微软字体,然后再把他开除掉。但现在我需要加条规则让他匹配到我想要的字体,虽然这样在一些需要微软雅黑的 office 软件里面可能有问题,不过反正我的工作不需要用垃圾 office 哈哈哈哈哈哈哈哈哈哈:

  <!--
    B 站直播首页的前端认为这个世界上只有微软字体(Arial,Microsoft YaHei,
    "Microsoft Sans Serif",Microsoft SanSerf,微软雅黑),
    所以我不得不开着这几个规则。如果我是他的老板,我就会开除掉这个不合格的前端。
    这里不能换成 sans-serif,因为它们只是插入用的锚点而不是子列表,
    除非我们把这条抬到最前面。
  -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>Microsoft YaHei</string>
    </test>
    <edit name="family" mode="append" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
    </edit>
  </match>

  <match target="pattern">
    <test name="family" qual="any">
      <string>Microsoft Sans Serif</string>
    </test>
    <edit name="family" mode="append" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
    </edit>
  </match>

剩下还有一些冗长的用来在不同语言下选择不同的 Noto Sans CJK 变体的设置,因为 Noto Sans CJK 是一个多语种集合字体,然后中日韩对一些汉字有不同的变体,所以需要这一段,不过我就不贴在这里了,完整的文件可以在文章末尾下载。

最后收个尾:

</fontconfig>

完整的文件在这里:local.conf

😼

Alynx Zhou

A Coder & Dreamer

既然看了喵写的文章,不打算投喂一下再走吗?哼!