Fontconfig (11日目) - monospace

sans-serif、serifとくればmonospaceですよね。 やるべき設定としてはmonospaceもserifと同じです。 フォントパターンのリストにsans-serifが混じらないようにして、適切なフォールバックが行われるようにして……ですね。 では簡単かといいますと、monospaceならではの注意点もあります。

半角幅/全角幅の比率

等幅フォントは等幅フォント同士でフォールバックするよう設定するわけですが……さて、等幅フォントの幅は全て同じでしょうか?

アウトラインフォントの座標系はフォントごとにまちまちですので統一のためフォントサイズと同じ幅、所謂em幅を1とした比率で表記します。 (em幅の語源は「M」の幅らしいですが実際にはフォントサイズと同じ幅という意味で使われる場合がほとんどです。 フォントサイズを16pxに設定したらem幅も16pxということになります。 CSSもそうですよね。)

大抵の等幅フォントで全角文字はem幅比1.0になります。 (U+2002 EM SPACE " " や U+2014 EM DASH "—" はemと付いていますが1.0とは限りません。)

それに対し半角文字はフォントごとに異なります。

ファミリー名

半角文字の幅/em幅

Ubuntu Mono

0.500

IPAゴシック/明朝

0.500

Noto Sans Mono CJK JP

0.500

BIZ UDゴシック/明朝

0.500

Courier

0.600

Liberation Mono

0.600

Noto Sans Mono

0.600

Go Mono

0.600

DejaVu Sans Mono

0.602

Source Han Code JP

0.667

欧文等幅フォントは0.6が多いです。 全角文字が混じらない言語で全ての文字が0.5ですと窮屈に見えますし。 Ubuntu Monoのような小サイズでの使用を狙ったフォントは0.5のものもあります。

日本語フォントを含む大抵のCJKフォントは半角:全角比が1:2の歴史がありますので0.5です。 ただしSource Han Code JPのような例外もあります。

それでですが、フォールバックによって幅0.5の半角文字と幅0.6の半角文字が混じったら、もう等幅フォントとは言えないですよね? カッコイイ等幅フォントには収録文字数が少ないものも多く知らないうちに結構フォールバックしてると思います。

monospaceをどういうフォントと捉えるか

まずFontconfigの扱う総称ファミリー名こと存在しないフォント名monospaceをどういうフォントと捉えるかから考えなければいけません。

単に入力しやすくするためのなんちゃって等幅で良いのであれば幅が微妙に違う文字が混じった状態でもあまり問題はないわけです。 ソースコードであってもインデント以外の行の途中での桁揃えをしないのであれば少しぐらいずれたってどうということはないですよね。

桁揃えが必要なのであれば厳密に揃えた等幅が必要です。 CSVでカンマの位置を揃えたりですね。 今これを書いているreStructuredTextでも セクションtable で「=」等を使って任意のテキストと同じ幅に揃える必要がありますのでem幅比0.5の等幅フォントが必要です。 (今はBIZ UD明朝を使用させていただいています。)

矩形選択やマルチカーソル等の等幅でないと使い辛いエディタの機能もあります。

monospaceという名前は /etc/fonts/conf.d/ 以下の設定ファイルが追加することもありますので厳密化は諦めてなんちゃって等幅扱い、それとは別に、例えばmonospace5やmonospace6といった名前で分類するのも手かもしれません。

やり方は……serifのときと同じですのでいちいち書きませんよ。 (手抜き。)

East Asian Width

等幅フォントにまつわる問題はまだあります。

ある文字が半角か全角かを定める仕様としてUnicodeの UAX #11: East Asian Width があります。

KDEのKonsoleやGNOMEのGNOME Terminal等の多くのターミナルエミュレーターはこの仕様に沿って文字を描画しています。 Konsoleは曖昧幅(A)は半角と決め打っています。 GNOME Terminalをはじめとするlibvteを使用しているものは曖昧幅の文字をどちらにするかのオプションがあります。

表示に使っているフォントがこの仕様に沿っていなければ表示がずれたりはみ出たりします。

そこで、本当に等幅フォントがEast Asian Widthの仕様に沿っているか確認する簡単なスクリプトを用意しました。 (Debian/Ubuntu系であればパッケージpython3-fontforgeが必要です。)

check-eaw.py
#!/usr/bin/python3

import sys
import unicodedata
import fontforge

f = fontforge.open(sys.argv[1])

halfwidth_width = f[next(iter(f.selection.select(('unicode',), 0x78)))].width
fullwidth_width = f.em

def check(g, unicode):
 width = g.width
 if width == 0:
  return
 eaw = unicodedata.east_asian_width(chr(unicode))
 if eaw == 'A':
  bad = False
 elif eaw in ['N', 'Na', 'H']:
  bad = width != halfwidth_width
 elif eaw in ['W', 'F']:
  bad = width != fullwidth_width
 else:
  assert False
 if bad:
  w = width / f.em
  print('U+%.4X "%s" %.3f %s' % (unicode, chr(unicode), w, eaw))

for g in f.glyphs():
 unicode = g.unicode
 if unicode >= 0:
  check(g, unicode)
 altuni = g.altuni
 if altuni is not None:
  for (unicode, vs, reserved) in altuni:
   check(g, unicode)

f.close()

sys.stderr.write('ok.\n')

曖昧幅の文字は見逃して、半角でなければならない文字、全角でなければならない文字だけをチェックします。

いくつか等幅フォントを見ていきましょう。

Ubuntu Mono

収録文字の少ないUbuntu Monoは違反文字はありませんでした。

DejaVu Sans Mono

$ ./check-eaw.py /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf 2> /dev/null
U+25FD "◽" 0.602 W
U+25FE "◾" 0.602 W
U+2614 "☔" 0.602 W
U+2615 "☕" 0.602 W
U+2648 "♈" 0.602 W
U+2649 "♉" 0.602 W
U+264A "♊" 0.602 W
U+264B "♋" 0.602 W
U+264C "♌" 0.602 W
U+264D "♍" 0.602 W
U+264E "♎" 0.602 W
U+264F "♏" 0.602 W
U+2650 "♐" 0.602 W
U+2651 "♑" 0.602 W
U+2652 "♒" 0.602 W
U+2653 "♓" 0.602 W
U+267F "♿" 0.602 W
U+2693 "⚓" 0.602 W
U+26A1 "⚡" 0.602 W

DejaVu Sans Monoですらいくつかの文字が違反してます。 これらの文字は全角でなければなりませんが半角で収録されています。

便宜上 2> /dev/null と書いてますが実際には1(stdout)の方をリダイレクトして貼り付けてます……わざわざ書くまでもないか。

IPAゴシック

$ ./check-eaw.py /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf 2> /dev/null
U+00A5 "¥" 1.000 Na
U+00A2 "¢" 1.000 Na
U+00A3 "£" 1.000 Na
U+00AC "¬" 1.000 Na
U+2600 "☀" 1.000 N
U+2601 "☁" 1.000 N
U+2602 "☂" 1.000 N
U+2603 "☃" 1.000 N
U+217A "ⅺ" 1.000 N
U+217B "ⅻ" 1.000 N
U+2756 "❖" 1.000 N
U+2318 "⌘" 1.000 N
U+29BF "⦿" 1.000 N
U+2616 "☖" 1.000 N
U+2617 "☗" 1.000 N
U+23BE "⎾" 1.000 N
U+23BF "⎿" 1.000 N
U+23C0 "⏀" 1.000 N
U+23C1 "⏁" 1.000 N
U+23C2 "⏂" 1.000 N
U+23C3 "⏃" 1.000 N
U+23C4 "⏄" 1.000 N
U+23C5 "⏅" 1.000 N
U+23C6 "⏆" 1.000 N
U+23C7 "⏇" 1.000 N
U+23C8 "⏈" 1.000 N
U+23C9 "⏉" 1.000 N
U+23CA "⏊" 1.000 N
U+23CB "⏋" 1.000 N
U+23CC "⏌" 1.000 N
U+2713 "✓" 1.000 N
U+2423 "␣" 1.000 N
U+23CE "⏎" 1.000 N
U+2051 "⁑" 1.000 N
U+01CD "Ǎ" 1.000 N
U+01D1 "Ǒ" 1.000 N

半角でなければならない文字が全角で収録されています。

Noto Sans Mono

$ ./check-eaw.py /usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf 2> /dev/null
U+FFFC "" 1.200 N
U+0468 "Ѩ" 1.200 N
U+046C "Ѭ" 1.200 N
U+046D "ѭ" 1.200 N
U+0478 "Ѹ" 1.200 N
U+0479 "ѹ" 1.200 N
U+01A2 "Ƣ" 1.200 N
U+01C4 "DŽ" 1.200 N
U+01C5 "Dž" 1.200 N
U+01C6 "dž" 1.200 N
U+01C7 "LJ" 1.200 N
U+01C8 "Lj" 1.200 N
U+01C9 "lj" 1.200 N
U+01CA "NJ" 1.200 N
U+01CB "Nj" 1.200 N
U+01CC "nj" 1.200 N
U+01F1 "DZ" 1.200 N
U+01F2 "Dz" 1.200 N
U+01F3 "dz" 1.200 N
U+02A5 "ʥ" 1.200 N
U+02A8 "ʨ" 1.200 N
U+1D7A "ᵺ" 1.200 N
U+1E9A "ẚ" 0.618 N
U+1F0A "Ἂ" 1.200 N
U+1F0B "Ἃ" 1.200 N
U+1F0C "Ἄ" 1.200 N
U+1F0D "Ἅ" 1.200 N
U+1F0E "Ἆ" 1.200 N
U+1F0F "Ἇ" 1.200 N
U+1F1A "Ἒ" 1.200 N
U+1F1B "Ἓ" 1.200 N
U+1F1C "Ἔ" 1.200 N
U+1F1D "Ἕ" 1.200 N
U+1F2A "Ἢ" 1.200 N
U+1F2B "Ἣ" 1.200 N
U+1F2C "Ἤ" 1.200 N
U+1F2D "Ἥ" 1.200 N
U+1F2E "Ἦ" 1.200 N
U+1F2F "Ἧ" 1.200 N
U+1F4A "Ὂ" 1.200 N
U+1F4B "Ὃ" 1.200 N
U+1F4C "Ὄ" 1.200 N
U+1F4D "Ὅ" 1.200 N
U+1F5B "Ὓ" 1.200 N
U+1F5D "Ὕ" 1.200 N
U+1F5F "Ὗ" 1.200 N
U+1F6A "Ὢ" 1.200 N
U+1F6B "Ὣ" 1.200 N
U+1F6C "Ὤ" 1.200 N
U+1F6D "Ὥ" 1.200 N
U+1F6E "Ὦ" 1.200 N
U+1F6F "Ὧ" 1.200 N
U+1F88 "ᾈ" 1.200 N
U+1F89 "ᾉ" 1.200 N
U+1F8A "ᾊ" 1.200 N
U+1F8B "ᾋ" 1.200 N
U+1F8C "ᾌ" 1.200 N
U+1F8D "ᾍ" 1.200 N
U+1F8E "ᾎ" 1.200 N
U+1F8F "ᾏ" 1.200 N
U+1F98 "ᾘ" 1.200 N
U+1F99 "ᾙ" 1.200 N
U+1F9A "ᾚ" 1.200 N
U+1F9B "ᾛ" 1.200 N
U+1F9C "ᾜ" 1.200 N
U+1F9D "ᾝ" 1.200 N
U+1F9E "ᾞ" 1.200 N
U+1F9F "ᾟ" 1.200 N
U+1FA8 "ᾨ" 1.200 N
U+1FA9 "ᾩ" 1.200 N
U+1FAA "ᾪ" 1.200 N
U+1FAB "ᾫ" 1.200 N
U+1FAC "ᾬ" 1.200 N
U+1FAD "ᾭ" 1.200 N
U+1FAE "ᾮ" 1.200 N
U+1FAF "ᾯ" 1.200 N
U+1FBC "ᾼ" 1.200 N
U+1FCC "ῌ" 1.200 N
U+1FFC "ῼ" 1.200 N
U+20A8 "₨" 1.200 N
U+0518 "Ԙ" 1.200 N
U+0520 "Ԡ" 1.200 N
U+0521 "ԡ" 1.200 N
U+0522 "Ԣ" 1.200 N
U+0523 "ԣ" 1.200 N
U+A728 "Ꜩ" 1.200 N
U+A732 "Ꜳ" 1.200 N
U+A734 "Ꜵ" 1.200 N
U+A736 "Ꜷ" 1.200 N
U+A738 "Ꜹ" 1.200 N
U+A73A "Ꜻ" 1.200 N
U+A73C "Ꜽ" 1.200 N
U+A74E "Ꝏ" 1.200 N
U+A729 "ꜩ" 1.200 N
U+A74F "ꝏ" 1.200 N
U+A773 "ꝳ" 1.200 N
U+AB60 "ꭠ" 0.797 N
U+A7FF "ꟿ" 1.200 N
U+A656 "Ꙗ" 1.200 N
U+A65C "Ꙝ" 1.200 N
U+A666 "Ꙧ" 1.200 N
U+A66C "Ꙭ" 1.200 N
U+A684 "Ꚅ" 1.200 N
U+A698 "Ꚙ" 1.200 N
U+052A "Ԫ" 1.200 N
U+A657 "ꙗ" 1.200 N
U+A65D "ꙝ" 1.200 N
U+A667 "ꙧ" 1.200 N
U+A66D "ꙭ" 1.200 N
U+A699 "ꚙ" 1.200 N
U+052B "ԫ" 1.200 N
U+211C "ℜ" 1.200 N
U+212C "ℬ" 1.200 N
U+210B "ℋ" 1.200 N
U+2110 "ℐ" 1.200 N
U+2112 "ℒ" 1.200 N
U+211B "ℛ" 1.200 N
U+2131 "ℱ" 1.200 N
U+2133 "ℳ" 1.200 N
U+2155 "⅕" 1.200 N
U+2156 "⅖" 1.200 N
U+2157 "⅗" 1.200 N
U+2158 "⅘" 1.200 N
U+2159 "⅙" 1.200 N
U+215A "⅚" 1.200 N
U+2150 "⅐" 1.200 N
U+2151 "⅑" 1.200 N
U+2152 "⅒" 1.800 N
U+2042 "⁂" 1.200 N
U+2E0E "⸎" 1.200 N
U+2047 "⁇" 1.200 N
U+2053 "⁓" 1.200 N
U+FF5B "{" 0.600 F
U+FF5D "}" 0.600 F
U+2E3B "⸻" 1.800 N
U+2E3A "⸺" 1.200 N
U+2057 "⁗" 1.200 N
U+20B7 "₷" 1.200 N
U+23DF "⏟" 1.800 N
U+23DD "⏝" 1.800 N
U+23E1 "⏡" 1.800 N
U+27D7 "⟗" 1.200 N
U+27D5 "⟕" 1.200 N
U+2A00 "⨀" 1.200 N
U+2205 "∅" 1.200 N
U+2031 "‱" 1.800 N
U+27D6 "⟖" 1.200 N
U+23DE "⏞" 1.800 N
U+23DC "⏜" 1.800 N
U+23E0 "⏠" 1.800 N
U+229B "⊛" 1.200 N
U+229C "⊜" 1.200 N
U+2298 "⊘" 1.200 N
U+2296 "⊖" 1.200 N
U+2297 "⊗" 1.200 N
U+29B8 "⦸" 1.200 N
U+229A "⊚" 1.200 N
U+219C "↜" 1.200 N
U+219D "↝" 1.200 N
U+219E "↞" 1.200 N
U+21A0 "↠" 1.200 N
U+21A2 "↢" 1.200 N
U+21A3 "↣" 1.200 N
U+21A4 "↤" 1.200 N
U+21A6 "↦" 1.200 N
U+21D0 "⇐" 1.200 N
U+21DA "⇚" 1.200 N
U+21DB "⇛" 1.200 N
U+21E8 "⇨" 1.200 N
U+21E6 "⇦" 1.200 N
U+27F5 "⟵" 1.200 N
U+27F6 "⟶" 1.200 N
U+25CD "◍" 1.200 N
U+25B0 "▰" 1.200 N
U+25B1 "▱" 1.200 N
U+25AD "▭" 1.200 N
U+25A2 "▢" 1.200 N
U+25E7 "◧" 1.200 N
U+25E8 "◨" 1.200 N
U+25E9 "◩" 1.200 N
U+25EA "◪" 1.200 N
U+25EB "◫" 1.200 N
U+25F0 "◰" 1.200 N
U+25F1 "◱" 1.200 N
U+25F2 "◲" 1.200 N
U+25F3 "◳" 1.200 N
U+25FD "◽" 0.600 W
U+25FE "◾" 0.600 W
U+25EC "◬" 1.200 N
U+25ED "◭" 1.200 N
U+25EE "◮" 1.200 N
U+25BB "▻" 1.200 N
U+25C5 "◅" 1.200 N
U+2349 "⍉" 1.200 N
U+2365 "⍥" 1.200 N
U+233E "⌾" 1.200 N
U+235F "⍟" 1.200 N
U+233D "⌽" 1.200 N
U+235C "⍜" 1.200 N
U+236B "⍫" 1.200 N
U+235A "⍚" 1.200 N
U+2371 "⍱" 1.200 N
U+2366 "⍦" 1.200 N
U+2367 "⍧" 1.200 N
U+236D "⍭" 1.200 N
U+2372 "⍲" 1.200 N
U+235D "⍝" 1.200 N
U+236C "⍬" 1.200 N
U+2736 "✶" 1.200 N
U+213A "℺" 1.200 N
U+1F67C "🙼" 1.200 N
U+1F67D "🙽" 1.200 N
U+1F67E "🙾" 1.200 N
U+1F67F "🙿" 1.200 N
U+213B "℻" 1.800 N
U+2114 "℔" 1.200 N
U+214F "⅏" 1.200 N
U+2118 "℘" 1.200 N

半角でなければならない文字が全角どころかもっと広い幅で収録されています。 ターミナルの表示が崩れるのは必至。

Noto Sans CJK Mono JP

残念ながら現在のFontForgeは NotoSansCJK-Regular.ttc を正しく読み込めません。 ですのでパスで。

Go Mono

収録文字の少ないGo Monoは違反文字はありませんでした。 (Debian/Ubuntu系であればパッケージfonts-goに収録されています。)

BIZ UD明朝

貴重な等幅明朝体の BIZ UD明朝 です。 最近 github のほうでBoldも公開されました。 SIL Open Font Licenseです。 株式会社モリサワ様に感謝。

$ ./check-eaw.py BIZUDMincho-Regular.ttf 2> /deb/null
U+29BF "⦿" 1.000 N
U+2616 "☖" 1.000 N
U+2617 "☗" 1.000 N
U+2600 "☀" 1.000 N
U+2601 "☁" 1.000 N
U+2602 "☂" 1.000 N
U+2603 "☃" 1.000 N
U+23BE "⎾" 1.000 N
U+23BF "⎿" 1.000 N
U+23C0 "⏀" 1.000 N
U+23C1 "⏁" 1.000 N
U+23C2 "⏂" 1.000 N
U+23C3 "⏃" 1.000 N
U+23C4 "⏄" 1.000 N
U+23C5 "⏅" 1.000 N
U+23C6 "⏆" 1.000 N
U+23C7 "⏇" 1.000 N
U+23C8 "⏈" 1.000 N
U+23C9 "⏉" 1.000 N
U+23CA "⏊" 1.000 N
U+23CB "⏋" 1.000 N
U+23CC "⏌" 1.000 N
U+2713 "✓" 1.000 N
U+2318 "⌘" 1.000 N
U+2423 "␣" 1.000 N
U+23CE "⏎" 1.000 N
U+01CD "Ǎ" 1.000 N
U+01D1 "Ǒ" 1.000 N
U+217A "ⅺ" 1.000 N
U+217B "ⅻ" 1.000 N
U+2051 "⁑" 1.000 N
U+2756 "❖" 1.000 N
U+23A9 "⎩" 1.000 N
U+23A8 "⎨" 1.000 N
U+23A7 "⎧" 1.000 N
U+23AD "⎭" 1.000 N
U+23AC "⎬" 1.000 N
U+23AB "⎫" 1.000 N
U+27A1 "➡" 1.000 N
U+2B05 "⬅" 1.000 N
U+2B06 "⬆" 1.000 N
U+2B07 "⬇" 1.000 N
U+261D "☝" 1.000 N
U+261F "☟" 1.000 N
U+24EA "⓪" 1.000 N
U+217F "ⅿ" 1.000 N
U+213B "℻" 1.000 N
U+0403 "Ѓ" 1.000 N
U+0490 "Ґ" 1.000 N
U+040C "Ќ" 1.000 N
U+040E "Ў" 1.000 N
U+040F "Џ" 1.000 N
U+0409 "Љ" 1.000 N
U+040A "Њ" 1.000 N
U+0405 "Ѕ" 1.000 N
U+0404 "Є" 1.000 N
U+0406 "І" 1.000 N
U+0407 "Ї" 1.000 N
U+0408 "Ј" 1.000 N
U+040B "Ћ" 1.000 N
U+0402 "Ђ" 1.000 N
U+0453 "ѓ" 1.000 N
U+0491 "ґ" 1.000 N
U+045C "ќ" 1.000 N
U+045E "ў" 1.000 N
U+045F "џ" 1.000 N
U+0459 "љ" 1.000 N
U+045A "њ" 1.000 N
U+0455 "ѕ" 1.000 N
U+0454 "є" 1.000 N
U+0456 "і" 1.000 N
U+0457 "ї" 1.000 N
U+0458 "ј" 1.000 N
U+045B "ћ" 1.000 N
U+0452 "ђ" 1.000 N
U+0386 "Ά" 1.000 N
U+0388 "Έ" 1.000 N
U+0389 "Ή" 1.000 N
U+038A "Ί" 1.000 N
U+038C "Ό" 1.000 N
U+038E "Ύ" 1.000 N
U+038F "Ώ" 1.000 N
U+03AA "Ϊ" 1.000 N
U+03AB "Ϋ" 1.000 N
U+03AF "ί" 1.000 N
U+03CA "ϊ" 1.000 N
U+0390 "ΐ" 1.000 N
U+03CD "ύ" 1.000 N
U+03CB "ϋ" 1.000 N
U+03B0 "ΰ" 1.000 N
U+03CC "ό" 1.000 N
U+03CE "ώ" 1.000 N
U+03AC "ά" 1.000 N
U+03AD "έ" 1.000 N
U+03AE "ή" 1.000 N

これも半角でなければならない文字が全角で収録されています。

傾向として欧文フォントは全角でなれけばならない文字が半角に、CJKフォントや全ての文字を収録することを目標にしているフォントは半角でなければならない文字が全角になっているものが多く感じます。

これはUnicodeが悪くて U+25FD WHITE MEDIUM SMALL SQUAREが全角(W)なのにそれより大きいはずのU+2BC1 WHITE LARGE SQUAREが半角(N) になってたりするクソ定義がいたるところにあるからです。 仕様を無視してもまともな字形にしたくなるのは当然ですよね……。