Linux 上的中文字体设置一直是一个令人头疼的问题,本文旨在分享个人经验。

全局设置

GNOME(GNOME Tweaks)、KDE 等桌面环境都提供了图形化的字体设置工具,它们能完成大部分字体配置工作了。

如需手动配置,参见 https://wiki.archlinux.org/title/Font_configuration,本文不再赘述,仅介绍个人所用配置关键片段:

漏网之鱼

KDE 的字体配置会创建 $XDG_CONFIG_HOME/fontconfig/fonts.conf,不过仅用于配置抗锯齿和 Hinting;默认字体的设置在 GTK 和 Qt 的设置里面。因此我将我的字体配置放在 $XDG_CONFIG_HOME/fontconfig/conf.d/00-prefer.conf,用于捕捉漏网之鱼(比如 GTK 4 的等宽字体,KDE 会自动设置 dconf 键 org.gnome.desktop.interface:monospace-font-name,但 GTK 4 却不会使用它)。

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
47
48
49
50
51
52
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'urn:fontconfig:fonts.dtd'>
<fontconfig>
<!-- 针对 Wine 的特殊设置,必须放在其他设置前面,见下文。 -->
<match>
<test name="prgname">
<string>wine</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" binding="same" mode="prepend">
<string>Noto Sans CJK SC</string>
</edit>
</match>
<alias binding="same">
<family>sans-serif</family>
<prefer>
<family>Roboto</family>
<family>Noto Sans</family>
<family>Noto Sans CJK SC</family>
</prefer>
</alias>
<alias binding="same">
<family>serif</family>
<prefer>
<family>Noto Serif</family>
<family>Noto Serif CJK SC</family>
</prefer>
</alias>
<alias binding="same">
<family>monospace</family>
<prefer>
<family>Iosevka</family>
</prefer>
</alias>
<alias binding="same">
<family>system-ui</family>
<prefer>
<family>Roboto</family>
<family>Noto Sans</family>
<family>Noto Sans CJK SC</family>
</prefer>
</alias>
<!-- Nerd Font 配置,见下文。 -->
<alias binding="same">
<family>Iosevka Term</family>
<accept>
<family>IosevkaTerm Nerd Font Mono</family>
</accept>
</alias>
</fontconfig>

Emoji

Qt 5 的字体 Fallback 只支持 255 个字体,如果字体过多(比如安装了全套 Noto 字体时),可能会导致 Emoji 无法正确显示,创建 /etc/fonts/conf.d/70-emoji.conf 并写入以下内容以解决此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<test name="family"><string>serif</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>

<match target="pattern">
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>

<match target="pattern">
<test name="family"><string>monospace</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>
</fontconfig>

Qt 6 无此问题。

设置 Noto CJK 为全局默认

在某次更新前,Arch 安装 noto-fonts-cjk 会自动设置默认,这个配置文件在 /etc/fonts/conf.d/70-noto-cjk.conf,后来为了尊重用户默认值,这个配置被移除了。如果你和我一样使用 Noto CJK 为默认 CJK 字体,手动创建该文件并写入以下内容:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<test name="lang">
<string>ja</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Serif CJK JP</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>ko</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Serif CJK KR</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-cn</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Serif CJK SC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-tw</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Serif CJK TC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-hk</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Serif CJK HK</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>ja</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans CJK JP</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>ko</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans CJK KR</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-cn</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans CJK SC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-tw</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans CJK TC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-hk</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans CJK HK</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>ja</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans Mono CJK JP</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>ko</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans Mono CJK KR</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-cn</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-tw</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>

<match target="pattern">
<test name="lang">
<string>zh-hk</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="same">
<string>Noto Sans Mono CJK HK</string>
</edit>
</match>
</fontconfig>

Nerd Fonts

Nerd Fonts 将符号字体嵌入到编程字体中,很适合用于花里胡哨的终端主题。但 Nerd Fonts 也有它的问题:Nerd Fonts 会分开 patch 每个字体变种,生成多个 TTF 而非单个 TTC,导致总大小急剧变大(作为对比,ttc-iosevka 的总大小是 446 MiB,而 ttf-iosevka-nerd 的总大小是 1036 MiB);并且对于字形过多的字体(如更纱黑体),由于 TTF 的 65536 个总字形的限制,patch 将会失败;此外,Nerd Fonts 有自己的更新节奏,不一定跟得上更新较快的字体,如 Iosevka 的更新节奏(虽然字体更不更新其实都差不多)。

一个解决方案是使用单独的 Symbols Nerd Font (Mono),或者只下载一个字重的 patched 字体作为符号字体(建议使用后者,因为 Nerd Font Symbols 不一定匹配你的字体宽度),并让普通的字体 Fallback 到符号字体上,如下所示:

1
2
3
4
5
6
<alias binding="same">
<family>Iosevka Term</family>
<accept>
<family>IosevkaTerm Nerd Font Mono</family>
</accept>
</alias>

应用特定设置

Wine

Proton 自带思源黑体魔改的假字体替换宋体、微软雅黑等,但原版 Wine 并不自带中文字体,可按照以下方式设置字体。

注意 Wine 只会在查找不存在的字体的时候调用 Fontconfig,Wine 自带的字体始终优先于 Fontconfig。

Fontconfig

如果你在 Fontconfig 的全局配置中将 sans-serif 的首位设置为非中文字体,Wine 将无法正常显示中文,解决方法是增加针对 Wine 的特殊设置,必须放在其他设置前面。

1
2
3
4
5
6
7
8
9
10
11
<match>
<test name="prgname">
<string>wine</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" binding="same" mode="prepend">
<string>Noto Sans CJK SC</string>
</edit>
</match>

这只会修复大部分字体,还有一小部分字体需要通过注册表替换来修复。(当然,你也可以全部使用注册表来控制 Wine 字体)

注册表

Wine 提供了以下三种方式替换字体,其中前两种方式是 Windows 也存在的方法,第三种是 Wine 独有的;此外,第三种只会在对应字体不存在的时候替换,而前两种始终会替换。

1
2
3
4
5
6
7
8
9
10
REGEDIT4

[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes]
"要替换的字体"="替换成的字体"

[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink]
"要替换的字体"="字体文件名(如NotoSansCJK-Regular.ttc,不是字体名)"

[HKEY_CURRENT_USER\Software\Wine\Fonts\Replacements]
"要替换的字体"="替换成的字体"

通常使用 FontSubstitutes 或者 Replacements,SystemLink 较为不常用。

替换 Tahoma 即可修复剩余的大部分字体;如果你未使用 Fontconfig 法,也替换 MS Shell Dlg 和 MS Shell Dlg 2。将以下内容保存到 font.reg,并用 wine regedit font.reg 导入即可:

1
2
3
4
5
6
REGEDIT4

[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes]
"MS Shell Dlg"="Noto Sans CJK SC"
"MS Shell Dlg 2"="Noto Sans CJK SC"
"Tahoma"="Noto Sans CJK SC"

注:如果仍然有问题,尝试替换其他字体,一个字体可能有多个名字,注意全部替换,如宋体(宋体、新宋体、SimSun、NSimSun)、微软雅黑(微软雅黑、Microsoft YaHei、Microsoft YaHei UI)。

字体发虚?

如果你通过以上两种方式完成了字体替换,你会发现另一个问题,Wine 的字体在灰度抗锯齿下看起来发虚。

我并未找到较好的解决方法,winetricks fontsmooth=rgb 也不生效,只能全局换成 RGB 抗锯齿。

不过也并不是一定要那么“全局”:Wine 使用 Xft,因此只需要将 Xft 的抗锯齿设置为 RGB 即可,Fontconfig 可以保持灰度抗锯齿。

尽管这确实会影响 Wine 之外的程序,但使用 Xft 配置而非 Fontconfig 配置的程序已经很少了。

参见 https://wiki.archlinux.org/title/X_resources 设置 Xresources。

1
Xft.rgba: rgb

禁用点阵字体

如果你和我一样尝试过使用 /etc/fonts/conf.d/70-no-bitmaps.conf 来禁用 Wine 的点阵字体的话,你会发现这样行不通,原因如上文所说,Wine 在有自带字体的情况下根本不管 Fontconfig,而罪魁祸首就是 Wine 自带的 Tahoma 字体 /usr/share/wine/fonts/tahoma.ttf,这个字体内嵌了点阵字形。

因此要禁用 Wine 的点阵字体,要么修改 tahoma.ttf 删除里面的点阵字形,要么用 FontSubstitutes 注册表替换掉这个字体。

原生起源引擎游戏

Valve 已修复第一方起源引擎游戏的中文字体,但对于某些第三方起源引擎游戏和 Mod,如 Portal Stories: Mel、Garry’s Mod 等,可按照如下方式二选一修复字体。

引擎替换法

如果出现字体问题的是 Mod,可以用原版游戏的引擎替换 Mod 的引擎,比如用 Portal 2 的引擎替换 Aperture Tag 的引擎,方法参见下文

注意这不适用于部分 Mod,尤其是重度魔改引擎的 Mod,这可能会导致游戏崩溃。

Fontconfig 法

找一个 $XDG_CONFIG_HOME/fontconfig 以外的目录创建 fonts.conf,比如 /home/你的用户名/.config/fontconfig-source/fonts.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
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'urn:fontconfig:fonts.dtd'>
<fontconfig>
<dir>/usr/share/fonts</dir>
<dir>/usr/local/share/fonts</dir>
<dir prefix="xdg">fonts</dir>
<dir>/home/你的用户名/.config/fontconfig-source</dir>
<alias binding="same">
<family>Helvetica</family>
<accept>
<family>文泉驿微米黑</family>
</accept>
</alias>
<alias binding="same">
<family>Verdana</family>
<accept>
<family>文泉驿微米黑</family>
</accept>
</alias>
<alias binding="same">
<family>WenQuanYi Zen Hei</family>
<accept>
<family>文泉驿微米黑</family>
</accept>
</alias>
<alias binding="same">
<family>Sans</family>
<accept>
<family>文泉驿微米黑</family>
</accept>
</alias>
<!-- PanoramaUI -->
<alias binding="same">
<family>sans-serif</family>
<accept>
<family>Noto Sans CJK SC</family>
</accept>
</alias>
</fontconfig>

下载文泉驿微米黑,一并放入该目录中。

这个文件的作用是将常见起源引擎的 VGUI 字体替换为文泉驿微米黑,不建议使用思源黑体,因为旧版起源引擎似乎无法加载;PanoramaUI(出现于 Strata 起源的 Mod 中,如 Portal Revolution 和 Portal 2: Community Edition)的字体可以使用思源黑体。

然后为对应游戏设置启动参数 FONTCONFIG_FILE=/home/你的用户名/.config/fontconfig-source/fonts.conf %COMMAND% 即可替换字体。

如果不想创建单独的配置文件,也可以修改全局配置,根据进程名替换字体,语法如下:

1
2
3
4
5
6
7
8
9
10
11
<match>
<test name="prgname">
<string>进程名</string>
</test>
<test name="family">
<string>源字体名</string>
</test>
<edit name="family" binding="same" mode="append">
<string>目标字体名</string>
</edit>
</match>

以下是游戏、进程名和字体名的对应表:

游戏进程名字体名
Half-Life 2hl2_linux“WenQuanYi Zen Hei”
Portalhl2_linuxHelvetica
Portal 2portal2_linuxHelvetica
Garry’s Modhl2_linux(32位)、gmod(64位)Helvetica(VGUI 非英文)、Verdana(VGUI 英文)、Sans(CEF)
Portal RevolutionrevolutionHelvetica(VGUI)、sans-serif(PanoramaUI)

原生 Unity 引擎游戏

部分原生 Unity 引擎游戏不自带字体,而是使用系统字体,如《瘟疫公司:进化》使用系统安装的 Microsoft YaHei,即使在 Linux 下也如此。

更致命的问题是 Unity 引擎似乎不直接或间接使用 Fontconfig,因此适用于起源引擎的 Fontconfig 法对 Unity 引擎无效。

因此你只能安装 Microsoft YaHei 等字体,并且 Unity 引擎似乎不认 $XDG_DATA_HOME/fonts,你可能需要将字体安装到 /usr/share/fonts 中。

……至少几年前是这样的。由于 Steam 容器化运行时的出现,将 Microsoft YaHei 字体安装到 /usr/share/fonts 后,再次在 Steam 内启动《瘟疫公司:进化》或者其附带的场景编辑器,字体依然不显示;但在 Steam 外启动场景编辑器可以正常显示中文字体(游戏本体无法脱离 Steam 启动,会卡死),因此怀疑是 Steam 容器的干扰。我尝试过将字体安装到 Steam Linux Runtime (Soldier) 中,但同样无效,怀疑运行时在启动时只会映射某个清单上的文件,清单外的文件,包括自行安装的字体,不会被映射到容器内。

因此现在不仅需要安装字体到 /usr/share/fonts,还要从命令行中去除 Steam 运行时相关参数,才能正确显示字体,对于《瘟疫公司:进化》,你需要将启动参数设置为 ./PlagueIncEvolved.x86_64 # %COMMAND%(是的,Steam 的启动参数是传递给 bash 的,可以用 bash 的一切语法,甚至是使用 # 注释掉原有命令)。

WPS Office

WPS Office 似乎有一套自己的字体 Fallback 逻辑,但是它不太好用:出于一些需求,我向电脑中安装了一些艺术字体,然后 WPS Office 遇到缺失的字体的时候就会 Fallback 到这些艺术字体上(我知道左下角可以打开字体替换菜单,但我给菜单中每个缺失的字体都设置了替换,依然有部分文字 Fallback 到艺术字体上),这个问题对于一部分文档尤其地严重,比如 Pandoc 导出的文档。

解决方法是针对 WPS 禁用这些艺术字体,这个方案并不完美,你没法在 WPS 中使用这些艺术字体了,但至少能解决查看部分文档时可读性低的问题。

不过,Fontconfig 并未提供针对单应用禁用字体的方案(我试过针对 wps 进程修改 pattern,无效),因此你需要创建一份新的配置文件,我将其放在了 /etc/fonts-wps/fonts.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
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<include ignore_missing="yes">/etc/fonts/fonts.conf</include>
<include ignore_missing="yes" prefix="xdg">fontconfig-wps/fonts.conf</include>
<selectfont>
<rejectfont>
<pattern>
<patelt name="family">
<string>HYYeYXSJ</string>
</patelt>
</pattern>
<pattern>
<patelt name="family">
<string>HYZhongYuanB5</string>
</patelt>
</pattern>
<pattern>
<patelt name="family">
<string>其他要禁用的字体名</string>
</patelt>
</pattern>
</rejectfont>
</selectfont>
</fontconfig>

然后修改 /usr/bin/wps/usr/bin/et/usr/bin/wpp/usr/bin/wpspdf,在 #!/bin/bash 后面加入一行 export FONTCONFIG_FILE=/etc/fonts-wps/fonts.conf

附录:部分起源引擎 Mod 无法运行的解决方案

大部分较旧的 Mod

使用 patchelf 或 execstack 移除 bin/engine.sobin/valve_avi.so(不存在可忽略)的 execstack 标志。

1
2
3
4
5
patchelf --clear-execstack engine.so
patchelf --clear-execstack valve_avi.so
#
execstack -c engine.so
execstack -c valve_avi.so

Aperture Tag

Aperture Tag 的引擎由于自身问题,即使移除了 execstack 标志,也会发生段错误,可通过替换引擎来修复。

下载 Portal 2,用 Portal 2/bin 替换 Aperture Tag/bin,将 Portal 2/portal2_linux 复制到 Aperture Tag/portal2_linux.real,删除原有 Aperture Tag/portal2_linux 并用以下脚本替代:

1
2
3
#!/bin/bash
cd "$(dirname "$0")"
LD_LIBRARY_PATH="$PWD/bin/linux32${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" "$PWD/portal2_linux.real" "$@"

这相当于用原版 Portal 2 的引擎替换掉了 Aperture Tag 的引擎,这应该会一并解决字体问题,如果没有,参考上文的 Fontconfig 法。