缘起

最近因为仰慕org-mode,从vim迁移到了Emacs。偶然发现org-mode中调出的calendar第一行居然没有对齐,排查一下发现是字体的问题。刚好也想改改Emacs的字体,于是我就开始了一段查找资料的过程。

背景

纯原始Emacs,init.el中仅有Customize的theme信息,以前一直使用的是默认字体。笔者刚开始使用Emacs三天,因假期在即得以偷闲有时间查资料。

浅探

现象:Org mode使用C-c .调出的Calendar第一行日期没有对齐,看起来非常别扭。

过程:再横排出现问题但是竖排没有,Google之,是字体不等宽的问题。于是乎,最简单的方式就是设置一个等宽字体,Google 一下“Emacs设置等宽字体”得到函数set-face-attribute,抄之,写作

(set-face-attribute 'default nil :font "Fira Code Retina")

其中,Fira Code是我很喜欢的一个等宽字体。

结果:Calendar显示正常。

追究

然而字体问题远不止这么简单,现在我设置了一个等宽字体,如果我想再换一个中文字体呢?再使用一遍set-face-attribute显然是行不通的。使用微软雅黑等宽这样的特制字体自然是很方便,但是这样就限制了选择字体的自由。

路线

明确一下,我的目标是能够为不同语言定制字体,最好保持等宽,并且能够适应放大缩小。

很显然,问题在于emac在背后是如何选择一个字体(font)的。从头捋一遍,思路如下:

  1. Emacs内部使用的编码集是什么?是否支持Unicode?
  2. Emacs对编码方式的支持如何?
  3. 对特定的character,Emacs如何选择字体?
  4. set-face-attribute是如何起作用的?应该用什么方式实现定制?

前两个问题使用Google可以得到,Emacs在最近(实际上也是很久以前了)的版本中使用Unicode重新实现了一遍,我的Emacs是24,支持Unicode;Emacs支持多种编码方式,至少utf-8、gb2312、gbk这样的编码方式是支持的。

face

对于第三个问题,GNU Emacs Manual的font slection一节中指出:

那么,什么是face呢?继续查阅Emacs手册,在Display Faces一节中,有:

无论是Wiki还是能找到的资料,对于Face的定义都是”关于要显示出来的东西的外在属性的定义“,包括font的属性(family,width,slant等等),还有颜色、下划线等等等等(Emacs wiki上甚至说“我们需要一个明确的定义”)。话句话说,face指定了我们会看到什么东西。

同一节指出了合并face的属性的优先级。其中最低的优先级是default face,也就是我一开始查到的命令所设置的东西,使用M-h f set-face-attribute可以得到

因为设置了默认的face,并且init.el和别的插件(org)也没有更改face,所以对于能够用Fira Code显示的character,Emacs自动选择了Fira Code。

那么,Emacs又是如何寻找”匹配程度最高“的字体的呢?这就不得不说到另外一个概念了:fontset

fontset

Emacs Wiki上对fontset有一个基本的描述,总结起来要点如下:

  • fontset是能够确定某个font能表示哪些character。它使用<CHARSET or CHAR RANGE> - <FONT NAME>的二元组的表来实现这一点。
  • fontset可以被修改,因而如果想使用某种特殊的font来绘制某些字符,使用标准fontset并修改它是最好的选择。

使用M-x describe-fontset <RET> <RET>可以查询到当前fontset的详细信息(运行比较慢),我的显示如下(经过了修改):


Fontset: -outline-Fira Code Retina-normal-normal-normal-mono-17----c--fontset-auto1
CHAR RANGE (CODE RANGE)
FONT NAME (REQUESTED and [OPENED])
C-@ .. Ÿ (#x43 .. #x9F)
-------------iso8859-1
  (#xA0)
--微软雅黑------------
¡ .. « (#xA1 .. #xAB)
-
------------iso8859-1
¬ (#xAC)
-
-微软雅黑------------
­ .. ¯ (#xAD .. #xAF)
-------------iso8859-1
° .. ± (#xB0 .. #xB1)
--微软雅黑------------*
...


CHAR RANGE打头的第二行以及第三行是表头,下面每两行是一组,每组第一行格式是

<范围开始处符号> .. <范围结束处符号> (<范围开始处符号码值> .. <范围结束处符号码值>)

第二行即是XLFD格式的font描述,这是X window system的字体标准。

当Emacs发现指定的face中的font(我的是Fira Code)无法显示这个字符时,它就会按照字符集到fontset中找到能够显示这个字符的字体,并且使用之。

fontset可以使用set-fontset-font来进行修改。我设置中文字符的代码如下,其中script的顺序来自这篇博客

(dolist (charset '(kana han symbol cjk-misc bopomofo))
(set-fontset-font (frame-parameter nil 'font)
            charset (font-spec :family "微软雅黑"))

set-fontset-font的使用非常易于理解,

以上代码其实就是从frame-parameter中取出当前frame的fontset,然后向这个fontset插入某些字符的字体。TARGET可以是字符范围的起始和结束的cons;可以是script的名字(我的就是script的名字),也可以是一个charset。FONT-SPEC可以用font-spec来确定字体,不用手写XLFD了。

使用按键组合C-u C-x =可以查看point下的那个character的信息,比如笔者在”你“字上按下之后显示如此:


position: 192 of 192 (99%), column: 0
character: 你 (displayed as 你) (codepoint 20320, #o47540, #x4f60)
preferred charset: chinese-gbk (GBK Chinese simplified.)
code point in charset: 0xC4E3
script: han
syntax: w which means: word
category: .:Base, C:2-byte han, L:Left-to-right (strong), c:Chinese, j:Japanese, |:line breakable
to input: type "C-x 8 RET HEX-CODEPOINT" or "C-x 8 RET NAME"
buffer code: #xE4 #xBD #xA0
file code: #xC4 #xE3 (encoded by coding system chinese-gbk-dos)
display: by this font (glyph code)
uniscribe:-outline-微软雅黑-normal-normal-normal-sans-20----p--iso8859-1 (#x482)

Character code properties: customize what to show
name: CJK IDEOGRAPH-4F60
general-category: Lo (Letter, Other)
decomposition: (20320) ('你')

中英混排等宽

关于emacs的中英文混排下的等宽以及放缩兼容,这篇博客狠狠地折腾了一把Emacs中文字体进行了一系列探索,改进了把中文字体和英文字体各自设置一个固定的值的方法,转为某种字体设置放缩系数,最终得到了一个不错的结果。

但是字体之间的宽度并不是一个固定的比例,对于每种不同的中文——英文字体组合,使用者都需要找不同的参数,还是比较麻烦的。虽然字体并不是一个常换的东西(也许。从这个角度讲,或许直接换一个中英文兼有的等宽字体才是正道。

github上也有个项目cnfonts,能够解决中英混排等宽的问题,作者自述原理是”让中文字体和英文字体使用不同的字号,从而实现中英文对齐“,效果非常不错,安装也很方便,推荐大家试试。

结论

鱼和熊掌不可得兼,选择了选取字体的自由后,就势必牺牲了适配的便捷性。

  • 可以set-face-attribute设置一个默认的中英文等宽字体
  • 通过修改fontset来修改特定的字体
  • 可以使用第三方包来解决问题。

另外,找完之后才发现,我上一秒还在看Org-mode学习timestamp的用法,回过神来就已经开了十几个网页学习Emacs的font了。这种time-killer的折腾还是需要谨慎。

04-06 00:45