# 字体的分类
字体的数量可以说是成千上万,但一般在电脑上显示的基本为以下这三类1:
-
monospace [等宽]
等宽字体是指字符宽度相同的字体,用于需要字符严格对齐的场合,例如控制台和源代码。与此相对,字符宽度各不相同的字体称为比例字体(其余四类字体都是)。不过,对于中文字体而言,并不存在等宽与比例的差别,因为所有中文字都是等宽的。中文字体中的“等宽”指的是字体的西文部分是等宽的,2个字母对应1个汉字。
-
sans-serif [无衬线]
是指笔画末端没有修饰(衬线)的字体,通常用于屏幕显示。中文的黑体与圆体就属于此类字体。
-
serif [有衬线]
是指笔画末端有修饰(衬线)的字体,通常用于打印。中文的宋体与仿宋就属于此类字体。
我们要做的字体配置主要就是针对上面这三类字体。
# 选字体
有了目标,下面就是选一个自己喜欢的字体了。不过,对于中文字体,目前现在 Linux 上常用的、在维护的开源中文字体就一套——“思源黑体”和“思源宋体”,同时被 Noto 和思源两个项目收录。Noto 系列字体是 Google 主导的,思源系列字体是 Adobe 主导的。2
对于编程字体,可以选择的余地就多多了,像是Source Code Pro,Consolas,Menlo等等。我一开始选择的是 Iosevka 。但是,这款字体给我感觉太瘦了,看起来很是别扭。最后,我选择了广受好评的 Fira Code 。
小结一下,我的选择是:
- 无衬线:西文 Noto Sans,中文 Noto Sans CJK
- 衬线:西文 Noto Serif,中文 Noto Serif CJK
- 等宽:西文 Fira Code,中文 Noto Sans Mono CJK
在ArchLiunx上,我们只需要安装noto-fonts
和noto-fonts-cjk
这两个包即可,他们分别提供了西文字体 Noto Sans / Noto Serif和中文字体 Noto Sans CJK / Noto Serif CJK / Noto Sans Mono CJK 。3
关于 emoji,我选择了 Twitter 推出的字体 Twemoji。
- ttf-twemoji
另外,少不了人见人爱的图标字体 Nerd Fonts。
- ttf-nerd-fonts-symbols
这里就有小伙伴开始好奇了,如何让西文和中文使用不同的字体呢?
在 Windows 下,我们可以选择合成字体,即将各类字体打包到一起。例如更纱黑体就是由思源黑体和西文字体 Iosevka 整合而来的。这种字体的好处就是方便,直接选择使用即可。但是缺点也是显而易见,就是打包太麻烦了,引入 Iosevka 要打一次包,想要支持 Nerd Fonts,又要打一次包。如果是别人帮你提供好的合成字体那还好说, 从网上下载、从软件仓库安装就完事了,自己打包的话真的工作量巨大。
而在 Liunx 下,我们只需要配置 fontconfig 就好了,无论想怎么搭配都可以实现,听起来是不是特别酷😎。可惜的是,有一些程序对 fontconfig 支持并不完善,这就达不到我们想要的效果。(说的就是你,Chrome😠)
# fontconfig
在我们开始正式配置前,还是有必要了解一些基本的知识。这里我就简单介绍一下,如果想要深入了解的话可以看看双猫大佬的这篇文章,里面详细介绍了Linux fontconfig 的字体匹配机制。
# 字体的属性
字体有很多属性,常用的有字族(family)、倾斜(slant)、字重(weight)。后两者合一起叫样式(style)。
字族就是它的名字啦。一个字体文件,可以提供多个字体族名 (family)。比如 Arch Linux 用户在安装 wqy-microhei 后,系统端增加了 wqy-microhei.ttc 这个字体文件,分别提供「WenQuanYi Micro Hei」「文泉驛微米黑」,「文泉驿微米黑」三个字体族名,它们是一个意思。我们可以运行 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
即可,例如:
|
|
fontconfig 就会打印调试信息,其中可以看到:
|
|
除了启动一个程序来看它字体的调用日志,我们也可以手动调用。例如,我想看 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 代表着字体匹配顺序。它就是配置文件中的<edit target="pattern">
操作。
最后应该关心 FcConfigSubstitute donePattern,这是 fontconfig 执行完字体替换后的结果。
# 配置文件
整个配置文件由如下几个部分依次拼接而成:
- 目录设置(
<dir>
,<cachedir>
,<include>
) - 杂项设置(
<config>
) - 扫描阶段(
<match target="scan">
) - 匹配阶段(
<alias>
,<match target="pattern">
) - 渲染阶段(
<match target="font">
)
想要实现合成字体的效果,一个最简单的思路,本文也基于该思路:不让程序使用某个具体的字体,而是使用通用字体族名 (Generic Font Family)。比如,让程序使用 sans-serif,也就是默认的无衬线字体。
我们要关心第四个部分,即匹配阶段,使用 fontconfig 配置如下:
|
|
这种 font stack 的方式,即可让程序按照以下顺序渲染字体:
|
|
这里的<test>
就是条件判断,mode="prepend"
指在前添加,binding="strong"
则是强绑定
# 开始配置
我们的思路就是就是修改默认的字族,让其成为我们想要指定的字体。然后将所有程序的字体配置改为通用字体族名:sans-serif,serif,monospace。
我们的配置都放在此目录中~/.config/fontconfig/fonts.conf
,我的完整配置在此仓库中。
# 设置默认字体
|
|
对 system-ui,sans-serif,serif,monospace 设置优先显示的字体。在这里我让 system-ui 默认为无衬线。注意,system-ui 必须在最前。由于 fontconfig 对 font pattern 的操作是按顺序执行的,所以必须先让 system-ui 能优先以 sans-serif 显示,然后才是对 sans-serif 的操作。
所有的 Noto CJK 字体都以 SC 结尾,因为我希望在没指定语言的默认情况下,以简体中文显示中文字形。
# 设置异形字
什么是异形字?Noto Sans CJK 中的异体字,是在 相同的 Unicode 码位下,不同的语言会使用不同的字形。
可以在双猫大佬的这个测试网站中看到,不同的语言环境下,这些字的显示是不同的。
我们想要实现在保留异体字的情况下,让它默认显示中国大陆字形;只在特定语言下显示异体字,比如用浏览器查看一个日文页面。
|
|
在字体匹配时,将 Noto Sans CJK SC 替换成 Noto Sans CJK JP。除了 ,你还需要照葫芦画瓢分别指定 zh-TW、zh-HK、ja、ko,以及对宋体和等宽字体进行重复的步骤。在此我就不重复了。
# 解决全角引号
英文中的单引号有两种,'
(U+0027) 和’
(U+2019)。可能会觉得前一种出现在英文文本中,后一种出现在中文文本中,并且宽度和中文等宽。然而,英语世界中的一些人在英文文章中也是会使用后一种的。所以,字体在显示后一种引号的时候,究竟是和英文字母一样窄,还是该和中文字体等宽呢?如果在英文文章中显示成全宽字符则会显得突兀。
比如,思源黑体其实也包含了拉丁字母的字形,可以完全使用思源黑体去显示西文。此时,后一种单引号的宽度会显示成中文字形的宽度。导致即使在全英文环境中,单引号也会突兀地在文本中过宽。
同样地,‘
(U+2018) “
(U+201C) ”
(U+201D) 也有类似的情况。
我们的目标是在不同语言环境下字形会有所区别,让引号只在中文文本中全宽。
同样,在双猫大佬的这个测试网站中可以进行测试:
利用fontconfig,可以方便的解决此问题
|
|
# 覆盖西文字体
如果去观察 Noto Sans CJK 这个中文字体,会发现它的西文部分的字形其实和 Noto Sans 不一样,虽然它们都以 Noto 自称。中文字体携带的英文字符有可能十分糟糕,特别是 Windows 自带的 SimHei,也就是中易黑体,它的英文相当糟糕。另外,微软雅黑的字重实在是太少了,对于设计师来说很不友好。而各种流行的英文字体支持很多字重。
Noto Sans CJK 的英文还是比较不错的,为了字体的样式统一,这里就不修改sans-serif和seri了。主要将编程用的等宽字体换成Fira Code。
|
|
这里为什么要多加一句判断呢?这是因为有些程序居然只使用 font pattern 结果中的首个字体,比如 Chrome(以及衍生的Chromium),虽然 Chrome 接受了我们指定的西文字体,但是它忽略了紧接其后的中文字体,即使配置采用了强绑定!然后中文字体又不知道它 fallback 到哪去了,可能会出现你想要的中文字体,也可能不是。
这里是 msedge 主要是我平时都用 edge 而不是 Chrome。可惜由于 Chromium 在 Linux 上小问题实在是太多了,还是老老实实的用 firefox 吧。
在所有情况下,除了程序名为 msedge 的情况下,优先使用 Fira Code 显示西文,再用 Noto Sans Mono CJK 显示中文。虽然我不能让 msedge 使用 Fira Code,但它一定能用上 Noto Sans Mono CJK 显示中文。
# 替换任意字体
当系统里已经安装了一些不需要的字体,但又不想删除或者屏蔽它怎么办呢?替换掉 font pattern 就可以了。
我这里则是用来替换思源字体,毕竟它就是 Noto 嘛
|
|
# 字体渲染参数
|
|
这里主要设置了一些字体的渲染方式:
autohint
:优先使用内嵌微调hinting
:开启微调hintstyle
:微调的程度,轻微antialias
:开启抗锯齿功能lcdfilter
:LCD filter 的风格,默认rgba
:LCD 子像素的排列顺序,rgb
这里就直接抄作业了。
# 遇到的问题
# 全角引号设置无效
一开始我遇到了这个问题,无论怎么设置都是半角,经过排查以及网友的留言后发现,这是由于系统变量中的LANG
为en_US.utf-8
,这会导致传递给 fontconfig 的 lang 是 lang: zh-cn(s) "en"(w)
。于是就一直匹配西文字体。
解决办法有两个,一个是直接修改LANG
环境变量为zh_CN.UTF-8
,我目前采用的就是这种形式。
或者就是默认渲染英文,当网页声明是中文时再换字体,相关配置如下:
|
|
这样如果网页声明了lang=zh-cn
,就会用Noto Sans CJK SC
字体进行覆盖,也就实现了全宽引号。假如网页没有声明lang
或者错误地声明了lang=en
,这时就会出现半角引号+汉字的组合了。
# 不能解决的问题
Linux 不强迫程序必须使用特定的依赖,而是程序主动选择了约定俗成的依赖。老话重谈,程序可以自由选择完全遵守 fontconfig,也可以选择部分使用 fontconfig 的配置,或者完全不遵守它。这也导致了对一些程序无法实现字体的修改。以及上面提到的 chrome 对 fontconfig 并不是很好,或许面对这种程序,就需要合成字体的出场了。