Unicode時代のCJKT環境におけるEmacsのフォント設定

1\. はじめに
------------

皆さんはEmacsのフォント設定どうやってますか?

最近は良質な組み合わせフォント、いわゆるコーディング用フォントが出回っていますので、それを

```elisp:init.el
(set-face-attribute 'default nil :family "HackGen" :height 140)
```

のように設定しておけばなんとなくOK、という感じなのかもしれません。もはやinit.elすら書かずに`M-x customize`でﾎﾟﾁｰなのかもしれません[^1]。いい時代になったものです。

しかし、CJKT全域[^2]の文字が編集対象になっていたり、ギリシャ文字やキリル文字、あるいは一部記号[^3]のようなEast Asian Widthの文字、全部ひっくるめてキレイに「いわゆる全角」の幅で表示できるようにしようと思ったら、やはり色々フォントをかき集めてフォントセットを正しく構築する必要があります。多分[^4]。

::: note
全角半角警察が飛んでこないように、ねんのため定義しておきますｗ

* 等幅フォント使用時のUS-ASCIIの文字と同じ幅で表示される文字(英数字、ASCIIの記号、欧文文字、JIS X 0201カナ等)を、以後「半角」と称します
* 同様に、US-ASCIIの文字の2倍の幅で表示される文字(JIS X 0208/0212/0213、すなわちJIS第一～第四水準文字、KS X 1001、GB 18030、CNS 11643をはじめとするCJKT漢字、かな、ハングルおよびUnicodeのCJK統合・互換・拡張漢字、さらには全角形、East Asian Widthの文字等)を、以後「全角」と称します
:::

そんな人いるのかって? すみません、いるんです… 手元のとあるRubyコードの中に、たしかにCJKTとExt-Bまでは網羅されてるファイルが現実にありまして、これがズレちゃうのが気持ち悪いなぁ、と思ったのが発端だったりします。

そこまでいかなくても、例えばWindowsではメイリオという読みやすい[^5]フォントがあります。macOSではヒラギノという美しさには定評のあるフォントがあります。コーディング用フォントもいいけど、これらのフォントがカバーしている範囲はこちらで出したい、という方は少なくないと信じています。

2\. 目的
--------

以下のテキストは、CJKT各言語での挨拶プラス、Unicode的に地雷を踏みやすい文字をかき集めたものです。

```text:font-test.txt
0123456789|
こんにちは|
你好早晨①|
㈱㈳㈠⑴㎢|
○×■αд|
㔫华𠮷啃遼|
안녕하세요|
```

これが全て同じ幅でズレずに表示できるのをゴールとしますが、特定のフォントの組み合わせでは面白くないので、各人の好みで構成フォントを選択できるような仕掛けにしています。

3\. LISPコード
--------------

結局やることは、`set-face-attribute`でベースのフォントを指定してフォントセットを作り、それに`set-fontset-font`で個別にコード範囲とフォントを追加していくだけです。

毎度毎度やるのはめんどいので、汎用化してこんな感じにしています。`site-lisp`にでも放り込んでrequireしておきましょう。

```elisp:init-fontset.el
;;; init-fontset.el

;;; Commentary:
;;; set-face-attributeでベースのフォントセットを作成し、set-fontset-fontで
;;; 個別にコード範囲とフォントを追加。
;;;
;;; init-fontsetを呼ぶたびにデフォルトフォントセットを上書きする。
;;;
;;; 東アジアの文字幅を適用したい記号やギリシャ文字等については、コードポイント
;;; 単位でフォントセットの設定を上書きすることができる。

;;; Code:

;; 初期化処理の予約

(when window-system
  (add-hook 'after-init-hook 'init-fontset))

;; 設定値

(defvar default-font-size 12
  "フォントサイズの初期値。ポイント単位で指定。")

(defvar fs:font-base
  (cond
   ((eq window-system 'x) "VL Gothic")
   ((eq window-system 'w32) "MS Gothic")
   ((memq window-system '(mac ns)) "Osaka")
   (t "Fixed"))
  "ASCIIおよびラテン文字系言語用のフォントファミリー名。リスト指定不可。
他のフォントを指定しない場合、全てこのフォントが使われる。
東アジア系言語環境では、当該言語用の等幅フォントを指定すること。")

(defvar fs:font-unicode nil
  "Unicode汎用のフォントファミリー名。
ASCIIを含めた他言語用のフォントと重複するため、必ずリストで指定すること。
リスト指定の場合、優先リストの後方にフォントを追加する。
nilの場合は無効。")

(defvar fs:font-japanese nil
  "日本語用のフォントファミリー名。指定した場合、強制的にこちらを使う。
`fs:font-chinese', `fs:font-taiwan', `fs:font-korean'と重複するコードポイントではこちらを優先する。
nilの場合は無効。
かなの文字幅が狭い場合や、かなのみ別フォントを使いたい場合は、`fs:font-fw-ovr-kana'も指定するとよい。")

(defvar fs:font-chinese nil
  "中国語(簡体字)用のフォントファミリー名。指定した場合、強制的にこちらを使う。
`fs:font-korean'と重複するコードポイントではこちらを優先する。
nilの場合は無効。")

(defvar fs:font-taiwan nil
  "中国語(繁体字)用のフォントファミリー名。指定した場合、強制的にこちらを使う。
`fs:font-chinese', `fs:font-korean'と重複するコードポイントではこちらを優先する。
nilの場合は無効。")

(defvar fs:font-korean nil
  "韓国語用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。
ハングルの文字幅が狭い場合や、ハングルのみ別フォントを使いたい場合は、`fs:font-fw-ovr-hangul'も指定するとよい。")

(defvar fs:font-cyrillic nil
  "キリル文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。
東アジア言語環境では当該言語用フォントや`fs:font-eaw-cyrillic'で指定すること。")

(defvar fs:font-arabic nil
  "アラビア文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。")

(defvar fs:font-greek nil
  "ギリシア文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。
東アジア言語環境では当該言語用フォントや`fs:font-eaw-cyrillic'で指定すること。")

(defvar fs:font-hebrew nil
  "ヘブライ文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。")

(defvar fs:font-thai nil
  "タイ文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。")

(defvar fs:font-viet nil
  "ベトナム文字用のフォントファミリー名。指定した場合、強制的にこちらを使う。
nilの場合は無効。")

(defvar fs:font-eaw-ovr-symbol nil
  "東アジアの文字幅が適用される文字のうち、記号向けにフォント設定を上書きする場合に指定。
nilの場合は無効。")

(defvar fs:font-eaw-ovr-cyril nil
  "東アジアの文字幅が適用される文字のうち、キリル文字向けにフォント設定を上書きする場合に指定。
nilの場合は無効。")

(defvar fs:font-eaw-ovr-greek nil
  "東アジアの文字幅が適用される文字のうち、ギリシャ文字向けにフォント設定を上書きする場合に指定。
nilの場合は無効。")

(defvar fs:font-fw-ovr-kana nil
  "文字幅がFWである文字のうち、かな文字向けにフォント設定を上書きする場合に指定。
`fs:font-japanese'に、かなの文字幅が狭い日本語フォントを指定した場合や、かなのみ別フォントを使いたい場合に有効。
nilの場合は無効。")

(defvar fs:font-fw-ovr-hangul nil
  "文字幅がFWである文字のうち、ハングル文字向けにフォント設定を上書きする場合に指定。
`fs:font-korean'に、ハングルの文字幅が狭い韓国語フォントを指定した場合や、ハングルのみ別フォントを使いたい場合に有効。
nilの場合は無効。")

;; Symbol系でASCII用フォントを強制的に使うモードを解除(Emacs25 or later)

(setq use-default-font-for-symbols nil)

;; 各種定数

(defconst fontset-override-char-alist
  '((eaw-ovr-symbol
     . (#xa2 #xa3 #xa7 #xa8 #xac #xb0 #xb1 #xb4 #xb6 #xd7 #xf7
        #x2010 #x2015 #x2016 #x2018 #x2019 #x201c #x201d
        #x2020 #x2021 #x2025 #x2026 #x2030 #x2032 #x2033 #x203b
        #x2103 #x212b (#x2190 . #x2193) #x21d2 #x21d4
        #x2200 #x2202 #x2203 #x2207 #x2208 #x220b #x2212 #x221a #x221d #x221e
        #x2220 #x2225 (#x2227 . #x222c) #x2234 #x2235 #x223d #x2252
        #x2260 #x2261 #x2266 #x2267 #x226a #x226b
        #x2282 #x2283 #x2286 #x2287 #x22a5 #x2312))
    (eaw-ovr-cyril
     . (#x401 (#x410 . #x44f) #x451))
    (eaw-ovr-greek
     . ((#x391 . #x3a9) (#x3b1 . #x3c9)))
    (fw-ovr-kana
     . ((#x3041 . #x3096) (#x3099 . #x30ff)))
    (fw-ovr-hangul
     . ((#xac00 . #xd7a3)))
    )
  "コードポイント指定でフォントを上書きするリスト。")

(defun get-fontset-font-alist ()
  "`init-fontset'がフォントセットを設定する際に指定する、文字セットとフォントファミリーのalistを返す。"
  `((unicode                  . ,fs:font-unicode)
    (korean-ksc5601           . ,fs:font-korean)
    (chinese-cns11643-1       . ,fs:font-chinese)
    (chinese-cns11643-2       . ,fs:font-chinese)
    (chinese-cns11643-3       . ,fs:font-chinese)
    (chinese-cns11643-4       . ,fs:font-chinese)
    (chinese-cns11643-5       . ,fs:font-chinese)
    (chinese-cns11643-6       . ,fs:font-chinese)
    (chinese-cns11643-7       . ,fs:font-chinese)
    (chinese-cns11643-15      . ,fs:font-chinese)
    (chinese-gbk              . ,fs:font-chinese)
    (chinese-gb2312           . ,fs:font-chinese)
    (big5                     . ,fs:font-taiwan)
    (big5-hkscs               . ,fs:font-taiwan)
    (katakana-jisx0201        . ,fs:font-japanese)
    (japanese-jisx0208        . ,fs:font-japanese)
    (japanese-jisx0212        . ,fs:font-japanese)
    (japanese-jisx0213-2      . ,fs:font-japanese)
    (japanese-jisx0213.2004-1 . ,fs:font-japanese)
    (cp932                    . ,fs:font-japanese)
    (latin-iso8859-1          . ,fs:font-base)
    (latin-iso8859-2          . ,fs:font-base)
    (latin-iso8859-3          . ,fs:font-base)
    (latin-iso8859-4          . ,fs:font-base)
    (latin-iso8859-9          . ,fs:font-base)
    (latin-iso8859-10         . ,fs:font-base)
    (latin-iso8859-13         . ,fs:font-base)
    (latin-iso8859-14         . ,fs:font-base)
    (latin-iso8859-15         . ,fs:font-base)
    (latin-iso8859-16         . ,fs:font-base)
    (cyrillic-iso8859-5       . ,fs:font-cyrillic)
    (arabic-iso8859-6         . ,fs:font-arabic)
    (greek-iso8859-7          . ,fs:font-greek)
    (hebrew-iso8859-8         . ,fs:font-hebrew)
    (thai-tis620              . ,fs:font-thai)
    (viscii                   . ,fs:font-viet)
    (vscii                    . ,fs:font-viet)
    (vscii-2                  . ,fs:font-viet)
    (eaw-ovr-symbol           . ,fs:font-eaw-ovr-symbol)
    (eaw-ovr-cyril            . ,fs:font-eaw-ovr-cyril)
    (eaw-ovr-greek            . ,fs:font-eaw-ovr-greek)
    (fw-ovr-kana              . ,fs:font-fw-ovr-kana)
    (fw-ovr-hangul            . ,fs:font-fw-ovr-hangul)
    ))

;; 初期化処理

(defun init-fontset (&optional size frame)
  "指定したポイントサイズで、フォントセット \"default\" を初期化する。
SIZE省略時は `default-font-size` を用いる。
FRAME省略時は全てのフレームを対象とする。"
  (interactive (list (read-number "Font size: " default-font-size)
                     (selected-frame)))
  (let* ((sz (if (and (numberp size) (> size 0)) size default-font-size))
         ;; 指定されたサイズをポイントサイズとして扱うため実数化する
         (pt (* sz 1.0))
         (height (round (* sz 10)))
         )
    ;; default faceのプロパティでフォントを設定することで、新規フレームに対応
    ;; (default-frame-alistを用いても、フレーム生成時にascii-fontのみのフォント
    ;;  セットが動的に生成されてしまうため)
    (set-face-attribute 'default frame :family fs:font-base :height height)

    ;; デフォルトフォントセットを上書き
    ;; (フォントセットを作成してset-frame-fontを行うと、ascii-fontのみのフォント
    ;;  セットが動的に生成されてしまうため)
    (mapc
     (lambda (entry)
       (let* ((target (car entry))
              (family (cdr entry))
              (f (lambda (fml add)
                   (let ((spec (font-spec :family fml :size pt))
                         (ovr (assoc target fontset-override-char-alist)))
                     (if ovr
                         (mapc
                          (lambda (ch) (set-fontset-font nil ch spec frame add))
                          (cdr ovr))
                       (set-fontset-font nil target spec frame add))
                    )))
             )
         (cond
          ((not family)
           nil)
          ((listp family)
           (mapc (lambda (fml) (funcall f fml 'append)) family))
          (t
           (funcall f family nil)))
         ))
     (get-fontset-font-alist))
    nil))

;;;

(provide 'init-fontset)

;; init-fontset.el ends here
```

4\. カスタマイズ
----------------

### フォントの指定

requireすると`after-init-hook`で初期化処理が予約されるため、init.elでフォント設定を入れておきます。設定できる値は各defvarのdocstringを見ていただければいいのですが、一般的な日本語環境では、以下が設定できていればだいたいOKかと思います。

* `fs:font-base` … ベースとなるフォント。欧文用の半角文字はこれを使う。コーディング用フォントの場合はここに指定すればOK。
* `fs:font-japanese` … 日本語用(JIS X 0201/0208/0212/0213、つまり半角カナと第一～第四水準漢字および補助漢字)のフォント。
* `fs:font-eaw-ovr-symbol`, `fs:font-eaw-ovr-cyril`, `fs:font-eaw-ovr-greek` … 日本語環境では全角で表示される記号、キリル文字、ギリシャ文字用のフォント。指定するとfont-baseよりこちらを優先する。
* `fs:font-unicode` … 上記で定義外となっている文字(主にUnicodeのCJK拡張やCJK互換系で、上記に含まれていないもの)のフォールバック用。リストで指定可。

CKT用のフォントを定義したい場合、`fs:font-chinese`、`fs:font-korean`、`fs:font-taiwan`を個別に指定すると、`fs:font-unicode`で一律にフォールバックするよりキレイに出るかもしれません。

さらに、ハングルだけ別のフォントを使いたい場合は`fs:font-fw-ovr-hangul`を設定できます[^6]。同様に、かなだけ別のフォントを使いたい場合は`fs:font-fw-ovr-kana`もあります。かなの幅が狭いコーディング用フォントを`fs:font-base`に渡した場合等で役に立つかもしれません。

他にも、主要な各言語用にフォントを設定できます。等幅で書く文化がない言語になってくると思いますが、当該言語を表示することがある場合で、各言語専用のフォントを個別に指定したい場合は定義しておくといいでしょう。

あとは、`default-font-size`にフォントサイズを設定しておけば、そのサイズで初期化されます。

`(init-fontset サイズ)`を評価すると再設定できます。`M-x init-fontset`でinteractiveに呼ぶこともできます。設定変更後に試す場合はこれでテストするといいでしょう。

### サイズの微調整

フォントを混ぜると、だいたい尺が合いません。

Emacsはこういうケースを想定していて、`face-font-rescale-alist`で、フォントファミリーごとに表示倍率を調整できるようになっています[^7]。

通常は、`fs:font-base`以外で渡した全角文字用のフォントに対して、一律で倍率を設定すれば合うと思います。倍率は選定した欧文文字用のフォントと、表示サイズによって変わるので、調整してください。

なお、フォントごとの拡大率の誤差の問題があるので、特定のサイズでしか合いません。普段使うサイズで調整してください。

5\. 設定例
----------

### Windows

標準フォントがメイリオとBIZ UDゴくらいというあまりWindowsっぽくない構成ですがｗ

ベースは[Meslo LG S](https://github.com/andreberg/Meslo-Font)で、さらに[Noto Sans Mono CJK](https://github.com/notofonts/noto-cjk)にも依存しています[^8]。ギリシャ語やロシア語等はBIZ UDゴがキレイなのでこちらを指定しています。

```elisp:init.el
(setq default-font-size 14)

(setq fs:font-base     "Meslo LG S")
(setq fs:font-unicode  '("Meiryo"
                         "Noto Sans Mono CJK JP"
                         "Noto Sans Mono CJK SC"
                         "Noto Sans Mono CJK TC"
                         "Noto Sans Mono CJK KR"))
(setq fs:font-japanese "Meiryo")
(setq fs:font-chinese  "Noto Sans Mono CJK SC")
(setq fs:font-taiwan   "Noto Sans Mono CJK TC")
(setq fs:font-korean   "Noto Sans Mono CJK KR")
(setq fs:font-eaw-ovr-symbol "BIZ UDGothic")
(setq fs:font-eaw-ovr-cyril  "BIZ UDGothic")
(setq fs:font-eaw-ovr-greek  "BIZ UDGothic")
(setq fs:font-fw-ovr-hangul  "Malgun Gothic")

(setq face-font-rescale-alist
      '((".*メイリオ.*" . 1.24)
        (".*Noto Sans.*" . 1.24)
        (".*BIZ UD.*" . 1.24)
        (".*Malgun Gothic.*" . 1.24)
        ))
```

Meslo LG S以外でも、同じBitstream Vera系フォントである[DejaVu Sans Mono](https://github.com/dejavu-fonts/dejavu-fonts)等でも同じ設定でいけます。標準のConsolasを使う場合は`face-font-rescale-alist`の倍率を1.18くらいにするといいでしょう。もちろん、`default-font-size`を変えた場合、この値はまるっと変わってきます。

上記設定で拡大率100%のデスクトップ環境に表示すると、このようになります。全ての全角文字がピッタリ合っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3667105/0c9e664c-8f39-9150-c318-133087602f89.png)

拡大率150%で運用するFHDのノートPC等ではこのままでも大丈夫ですが、200%以上になるSurface等の高解像モニターでは、倍率を1.2(Consolasの場合1.12)にしないと合わなくなります。同じ設定ファイルで自動認識しようと思ったらこのようにすればいいでしょう。

```elisp:init.el
(let* ((dpi (/ (cadddr (frame-monitor-attribute 'geometry))
               (/ (cadr (frame-monitor-attribute 'mm-size)) 25.4)))
       (rescale (cond
                 ((> dpi 240) 1.2)      ;; Consolasの場合1.12
                 (t 1.24))))            ;; Consolasの場合1.18

  (setq face-font-rescale-alist
        `((".*メイリオ.*" . ,rescale)
          (".*Noto Sans.*" . ,rescale)
          (".*BIZ UD.*" . ,rescale)
          (".*Malgun Gothic.*" . ,rescale)
          )))
```

### macOS

macOS環境でEAWな記号を比較的キレイに出せる等幅フォントが見当たらない[^9]ため、こちらもNoto Sans Mono CJKのインストールが前提となります。Windowsでは日本語環境であれば標準フォントのみでの設定も可能でしたが、こちらは事実上必須扱いです。

日本語用にヒラギノ丸ゴを指定した場合は、キリル文字とギリシャ文字の上書きは不要ですが、一部記号が縮むので、`fs:font-eaw-ovr-symbol`だけは指定が必要です。

```elisp:init.el
(setq default-font-size 14)

(setq fs:font-base     "Menlo")
(setq fs:font-unicode  '("Hiragino Maru Gothic ProN"
                         "Noto Sans Mono CJK JP"
                         "Noto Sans Mono CJK SC"
                         "Noto Sans Mono CJK TC"
                         "Noto Sans Mono CJK KR"))
(setq fs:font-japanese "Hiragino Maru Gothic ProN")
(setq fs:font-chinese  "Noto Sans Mono CJK SC")
(setq fs:font-taiwan   "Noto Sans Mono CJK TC")
(setq fs:font-korean   "Noto Sans Mono CJK KR")
(setq fs:font-eaw-ovr-symbol "Noto Sans Mono CJK JP")
(setq fs:font-fw-ovr-hangul  "Nanum Gothic")

(setq face-font-rescale-alist
      '((".*Hiragino.*" . 1.2)
        (".*Noto Sans.*" . 1.2)
        (".*Nanum.*" . 1.28)
        ))
```

2020年代のmac環境はほぼRetina一択と思いますので、DPIでの分岐はありませんが、外部モニターを使っていてズレちゃう方はWindowsの例を参考に調整してみるといいでしょう。ハングル用に指定したNanumは他とサイズ感が合わないため、これだけ倍率が異なります。`fs:font-eaw-ovr-symbol`にOsakaを指定した場合も同様に倍率を変える必要があります[^10]。

こちらも、Windowsと同様に、`default-font-size`を変更すると再調整になります。

このような表示になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3667105/8bd7d956-0ed9-e9cd-7de2-247d3bee174a.png)

6\. まとめ
----------

結局のところ、よくあるフォント設定の一例ですので、新たな知見はあまりありませんが、setqを並べてカスタマイズできるように汎用化できているので、ためしに放流してみようと思いました。

年の瀬は大掃除の季節です。お手元のEmacsの設定も、たまには煤払いしてみるのもいいのではないでしょうかｗ

[^1]: 筆者はカスタマイズ系の機能は使ったことないのでこのへんは完全にエアプです。
[^2]: 最近のExt-B以降はそもそもその漢字何語用なの? みたいなのがゴロゴロしてるのでCJKT(あとVも入れる?)っていう括りも怪しいところですけど。
[^3]: マルとかバツとか、セント記号とかも該当しますね。
[^4]: 結局のところ、TrueType/OpenTypeの1フォント65535グリフというフォーマット制限がある限り、IVSまで含めると、CJKTの各ローカル規格で定義されている漢字に限定したとしても、1フォントで全部を網羅するのは無理なので…
[^5]: 特にメトリックの面で、メイリオの設計がいいかと言われると異論は多々あるかと思いますが、ことグリフデザインと、特に低解像度環境におけるヒンティングデータの品位という観点での実用的な読みやすさは秀逸と思っています(個人の感想です)。
[^6]: Noto Sans Mono CJK KRのハングルは少し幅が狭いので、別フォントを追加しないとズレるのです。
[^7]: このせいで行の高さが合わなくなりますが。本来は、[PuTTYのD2DDWパッチ版](https://ice.hotmint.com/putty/d2ddw.html)のように、全角文字の間に隙間を挿入してくれるのがベターなんですけどね…
[^8]: Super OTC版を使わない場合、フォントファミリー名にRegularを追加する必要があるかもしれません。
[^9]: Osakaは等幅とプロポーショナルでフォントファミリー名が同じなので使いづらいです。実際Sequoiaあたりで(もしかしたらSonomaからだったかも)プロポーショナル側を使われている気がします。
[^10]: `default-font-size`が14の場合、倍率を1.3または1.6で調整してみてください。等幅が出てくるかプロポーショナルが出てくるかで最適値は変わります。