本文参考:
用 fontconfig 治理 Linux 中的字体
Linux字体美化实战(Fontconfig配置)
Linux 上的字体配置与故障排除

利用 fontconfig 对 linux 下的字体进行配置。分享我自己的配置方案,尽量把各类问题处理好。

字体的分类

字体的数量可以说是成千上万,但一般在电脑上显示的基本为以下这三类

  1. monospace [等宽]

等宽字体是指字符宽度相同的字体,用于需要字符严格对齐的场合,例如控制台和源代码。与此相对,字符宽度各不相同的字体称为比例字体(其余四类字体都是)。不过,对于中文字体而言,并不存在等宽与比例的差别,因为所有中文字都是等宽的。中文字体中的“等宽”指的是字体的西文部分是等宽的,2个字母对应1个汉字。

  1. sans-serif [无衬线]

是指笔画末端没有修饰(衬线)的字体,通常用于屏幕显示。中文的黑体与圆体就属于此类字体。

  1. serif [有衬线]

是指笔画末端有修饰(衬线)的字体,通常用于打印。中文的宋体与仿宋就属于此类字体。

我们要做的字体配置主要就是针对上面这三类字体。

选字体

有了目标,下面就是选一个自己喜欢的字体了。不过,对于中文字体,目前免费商用的中文字体越来越多了,除了 google 主导的 Noto 系列字体,以及 Adobe 主导的思源系列字体以外。还有阿里巴巴普惠体、OPPO Sans、HarmonyOS Sans 和 MiSans。

对于编程字体,可以选择的余地就多多了,像是 Source Code Pro,Consolas,Menlo 等等。我最终也是选择了广受好评的 Fira Code 。

小结一下,我的选择是:

  • 系统UI:MiSans
  • 无衬线:西文 DejaVu Sans,中文 MiSans
  • 衬线:西文 DejaVu Serif,中文 方正书宋
  • 等宽:西文 Fira Code,中文 方正中等线

需要注意的是,方正书宋,方正中等线都是个人免费使用,但是不能商用,需要自行去方正官网拿到授权。而 Misans 则是小米的免费字体。

关于 emoji,我选择了 Debian 系统默认会装上的 Noto emoji 。

另外,少不了人见人爱的图标字体 Nerd Fonts。我是下载使用了 FiraCode Nerd Fonts。

这里就有小伙伴开始好奇了,如何让西文和中文使用不同的字体呢?

在 Windows 下,我们可以选择合成字体,即将各类字体打包到一起。例如更纱黑体就是由思源黑体和西文字体 Iosevka 整合而来的。这种字体的好处就是方便,直接选择使用即可。但是缺点也是显而易见,就是打包太麻烦了,引入 Iosevka 要打一次包,想要支持 Nerd Fonts,又要打一次包。如果是别人帮你提供好的合成字体那还好说, 从网上下载、从软件仓库安装就完事了,自己打包的话真的工作量巨大。

而在 Liunx 下,我们只需要配置 fontconfig 就好了,无论想怎么搭配都可以实现,听起来是不是特别酷😎。可惜的是,有一些程序对 fontconfig 支持并不完善,这就达不到我们想要的效果。(说的就是你,Chrome😠)

fontconfig

在我们开始正式配置前,还是有必要了解一些基本的知识。这里我就简单介绍一下,如果想要深入了解的话可以看看双猫大佬金步国大佬的文章,里面详细介绍了 Linux fontconfig 工作原理。

字体的属性

字体有很多属性,常用的有字族(family)、倾斜(slant)、字重(weight)。后两者合一起叫样式(style)。

字族就是它的名字啦。一个字体文件,可以提供多个字体族名 (family)。比如 Debian 用户安装 fonts-noto 后,系统端增加了 NotoSans-Regular.ttf 等字体文件,文件会提供一系列字体名,它们是一个意思。我们可以运行 fontconfig 提供的命令行工具 fc-list 去查看系统上已安装的字体已经它们对应的字体族名。

倾斜就是斜不斜,英文叫 Roman、Italic 或者 Oblique、Italic 是专门的斜体写法(更接近手写样式), Oblique 是把常规写法倾斜一下完事。

字重就更简单了,就是笔划的粗细。常见的有 Regular、Normal、Medium、Bold、Semibold、Black、Thin、Light、Extralight 等。

通用字族名

很多时候,程序并不在乎用户具体使用的是哪款字体,像很多网站的 CSS 那样把各个平台的常见字体全部列出来太傻了,又容易出问题。所以,人们发明了“通用字族名”,也就是 sans-serif (sans)、serif 和 monospace (mono) 这些。它们不是真实存在的字体,而是分别指示程序去使用无衬线、衬线、等宽字体。那么桌面程序又是如何知道具体使用哪些字体呢?它只需要去查询 fontconfig 就行了。由于它们必定要经过 fontconfig 的查询流程后才能使用字体,所以我们可以通过 fontconfig 的配置去精准控制程序使用的字体。

如何调试

传入环境变量FC_DEBUG=4即可,例如:

1
FC_DEBUG=4 firefox

fontconfig 就会打印调试信息,其中可以看到:

1
2
3
4
5
6
7
8
9
FcConfigSubstitute editPattern has 8 elts (size 16)
family: "sans-serif"(w)
pixelsize: 26(f)(s)
antialias: True(w)
hintstyle: 1(i)(w)
rgba: 5(i)(w)
lang: "zh-CN"(w)
lcdfilter: 1(i)(w)
prgname: "firefox"(s)

除了启动一个程序来看它字体的调用日志,我们也可以手动调用。例如,我想看 monospace 在系统里被修改成了什么字体,就可以执行:

1
FC_DEBUG=4 fc-match 'monospace'

打印出的调试信息会很长,我们主要看几个部分:

第一部分,Add Rule,指已添加的配置文件规则。这里面也包含了家目录下的配置文件,可以找来看看被解析成了什么。

第二部分,在 Add Rule 之后,迎来了最关键的、我们应当关心的 FcConfigSubstitute Pattern,它包含了 font pattern。(s) 和 (w) 分别代表强弱绑定;prgname 代表程序名,此时就是 fc-match。至于 lang,由于没有对 fc-match 指定语言,所以默认是 en。

接下来有很多条 FcConfigSubstitute editPattern,代表对 font pattern 的替换操作。但是必须当规则匹配的时候,也就是 Rule Set 不是 No match 的情况下,才执行 FcConfigSubstitute editPattern。那么,又应该怎么看 FcConfigSubstitute editPattern 呢?主要看 family,因为 family 代表着字体匹配顺序。它就是配置文件中的操作。

最后应该关心 FcConfigSubstitute donePattern,这是 fontconfig 执行完字体替换后的结果。

配置文件

整个配置文件由如下几个部分依次拼接而成:

  • 目录设置(<dir>,<cachedir>,<include>)
  • 杂项设置(<config>)
  • 扫描阶段(<match target="scan">)
  • 匹配阶段(<alias>, <match target="pattern">)
  • 渲染阶段(<match target="font">)

想要实现合成字体的效果,一个最简单的思路,本文也基于该思路:不让程序使用某个具体的字体,而是使用通用字体族名 (Generic Font Family)。比如,让程序使用 sans-serif,也就是默认的无衬线字体。

我们要关心第四个部分,即匹配阶段,使用 fontconfig 配置如下:

1
2
3
4
5
6
7
8
9
10
<match target="pattern">
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>MiSans</string>
<string>DejaVu Sans Book</string>
<string>Noto Color Emoji</string>
</edit>
</match>

这种 font stack 的方式,即可让程序按照以下顺序渲染字体:

Misans —> DejaVu Sans Book -> Noto Color Emoji

这里的 <test> 就是条件判断,mode="prepend" 指在前添加,binding="strong" 则是强绑定

开始配置

我们的思路就是就是修改默认的字族,让其成为我们想要指定的字体。然后将所有程序的字体配置改为通用字体族名:sans-serif,serif,monospace。

单用户使用,则配置文件在 ~/.config/fontconfig/fonts.conf;多用户使用则配置文件为 /etc/fonts/local.conf 。

设置默认字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- Default system-ui fonts -->
<match target="pattern">
<test name="family">
<string>system-ui</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>sans-serif</string>
</edit>
</match>

<!-- Default sans-serif fonts-->
<match target="pattern">
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>MiSans</string>
<string>DejaVu Sans Book</string>
<string>Noto Color Emoji</string>
</edit>
</match>

<!-- Default serif fonts-->
<match target="pattern">
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>方正书宋_GBK</string>
<string>FZShuSong</string>
<string>DejaVu Serif Book</string>
<string>Noto Color Emoji</string>
</edit>
</match>

<!-- Default monospace fonts-->
<match target="pattern">
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>FiraCode Nerd Font</string>
<string>方正中等线_GBK</string>
<string>Noto Color Emoji</string>
</edit>
</match>

对 system-ui,sans-serif,serif,monospace 设置优先显示的字体。在这里我让 system-ui 默认为无衬线。注意,system-ui 必须在最前。由于 fontconfig 对 font pattern 的操作是按顺序执行的,所以必须先让 system-ui 能优先以 sans-serif 显示,然后才是对 sans-serif 的操作。

覆盖西文字体

如果去观察 Noto Sans CJK 这个中文字体,会发现它的西文部分的字形其实和 Noto Sans 不一样,虽然它们都以 Noto 自称。中文字体携带的英文字符有可能十分糟糕,特别是 Windows 自带的 SimHei,也就是中易黑体,它的英文相当糟糕。另外,微软雅黑的字重实在是太少了,对于设计师来说很不友好。而各种流行的英文字体支持很多字重。

此处仅在英文状态下将 MiSans 替换为 DejaVu Sans Book字体。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Replace english fonts-->
<match target="pattern">
<test name="lang" compare="contains">
<string>en</string>
</test>
<test name="family" compare="contains">
<string>MiSans</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Sans Book</string>
</edit>
</match>

浏览器字体问题

有些程序,主要是浏览器程序,居然只使用 font pattern 结果中的首个字体,比如 Chrome(以及衍生的Chromium),虽然 Chrome 接受了我们指定的西文字体,但是它忽略了紧接其后的中文字体,即使配置采用了强绑定!然后中文字体又不知道它 fallback 到哪去了,可能会出现你想要的中文字体,也可能不是。我们可以指定程序来渲染。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 替换浏览器字体-->
<match target="pattern">
<test name="prgname" compare="not_eq">
<string>msedge</string>
</test>
<test name="family" compare="contains">
<string>MiSans</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>FiraCode Nerd Font</string>
</edit>
</match>

这里是 msedge 主要是我平时都用 edge 而不是 Chrome。可惜由于 Chromium 在 Linux 上小问题实在是太多了,还是老老实实的用 firefox 吧。

在所有情况下,除了程序名为 msedge 的情况下,优先使用 Fira Code 显示西文,再用 MiSans 显示中文。虽然我不能让 msedge 使用 Fira Code,但它一定能用上 MiSans 显示中文。

替换任意字体

当系统里已经安装了一些不需要的字体,但又不想删除或者屏蔽它怎么办呢?替换掉 font pattern 就可以了。

我这里则是用方正书宋来替换普通的宋体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<match target="pattern">
<test qual="any" name="family">
<string>宋体</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>方正书宋_GBK</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>新宋体</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>方正书宋_GBK</string>
</edit>
</match>

字体渲染参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--rendering options-->
<match target="font">
<edit name="autohint" mode="assign">
<bool>false</bool>
</edit>
<edit name="hinting" mode="assign">
<bool>true</bool>
</edit>
<edit name="hintstyle" mode="assign">
<const>hintslight</const>
</edit>
<edit name="antialias" mode="assign">
<bool>true</bool>
</edit>
<edit name="lcdfilter" mode="assign">
<const>lcddefault</const>
</edit>
<edit name="rgba" mode="assign">
<const>rgb</const>
</edit>
</match>

这里主要设置了一些字体的渲染方式:

autohint:优先使用内嵌微调
hinting:开启微调
hintstyle:微调的程度,轻微
antialias:开启抗锯齿功能
lcdfilter:LCD filter 的风格,默认
rgba:LCD 子像素的排列顺序,rgb
这里就直接抄作业了。

不能解决的问题

Linux 不强迫程序必须使用特定的依赖,而是程序主动选择了约定俗成的依赖。老话重谈,程序可以自由选择完全遵守 fontconfig,也可以选择部分使用 fontconfig 的配置,或者完全不遵守它。这也导致了对一些程序无法实现字体的修改。以及上面提到的 chrome 对 fontconfig 并不是很好,或许面对这种程序,就需要合成字体的出场了。