288
204

More than 3 years have passed since last update.

SF Mono を使って最高のプログラミング用フォントを作った話

Last updated at Posted at 2018-12-17

みなさんターミナルは使ってますか? Terminal.app? iTerm2? Hyper? それとも他の何か?

それではフォントは何を使っていますか? Menlo? Consolas? Ricty? 今日はそんなお話です。

  • 対象とする読者
    • ターミナルやエディタでカコイイ等幅フォントが使いたい人。
    • 既存のフォントを FontForge + Python で色々いじって遊びたい人。

プログラミング用フォント SF Mono Square

ずっと Ricty を使っていたのですが、色々こだわる余り自分でフォント作ってしまいました。作ったものをそのまま配布するのはライセンス上できませんが、Homebrew で誰でも導入できるようにしています。

brew tap delphinus/sfmono-square
brew install sfmono-square
# /usr/local/opt/sfmono-square/share/fonts にフォントが作成されます

スクリーンショット 2018-12-1 19.05.15.png

SF Mono Square には以下のような特徴があります。

  • レポジトリ: delphinus/homebrew-sfmono-square
  • 半角フォントには Apple 純正の SF Mono、全角フォントには Migu 1M を利用。
  • 半角・全角は幅がちょうど 1 対 2 になるようにグリフを調整。
  • 全角空白の可視化。
  • Nerd Fonts グリフの追加(いわゆる Powerline glyphs を含む)。
  • その他たくさんのグリフの調整。

要するに Ricty の SF Mono 版+α というわけです。

  • TypeScript のコードを表示した例
    スクリーンショット 2018-12-16 14.34.58.png
  • 日本語もバッチリ
    スクリーンショット 2018-12-16 14.35.48.png
  • Powerline glyphs や Nerd Fonts 由来の様々なアイコンを搭載
    スクリーンショット 2018-12-16 14.45.41.png

SF Mono は最近の macOS, Xcode には標準で含まれています。気になるライセンスですが、フォントファイルはそれ自体を販売・配布しない限り、このように改変して利用することは問題ないそうです(Apple に問い合わせました)。これは SF Mono に限らず、macOS に含まれるフォント全てに当てはまります。太っ腹ですね。

以下、SF Mono Square を作成することになった経緯を述べ、そのあとに実際のフォントの作り方を載せていきます。

プログラミング用フォントに求められること

ターミナルやエディタを使う時、フォントには何を選択したらよいでしょうか。ここで(少なくとも僕が)重要と思うことを列挙してみます。

1. 等幅フォントであること

これは10人に聞いたら10人が賛成するでしょう。プロポーショナルなフォントでコーディングするなんて色々と無理です。

2. CLI 環境、あるいは IDE での作業に適したフォントであること

「作業」とは、コーディング以外にも、ログやドキュメントの閲覧も含みます。何よりも読みやすい文字であること、他の文字と混同しないことが求められます。具体的には、以下のような点は必須でしょう。

  • 0, o, O, 1, i, I, l が一目で区別できること。
  • 字間、行間が適切であること。

スクリーンショット 2018-12-16 14.52.51.png

これらは多くの人が必須とするはずです。他、いくつかの「プログラミング用」フォントは、以下のような特徴を持つことを長所としています。

3. 全角文字にも対応すること

iTerm2 のように半角・全角に別のフォントを指定できるターミナルアプリもありますが、Terminal.app など、フォントを一種類しか指定できないアプリもまだ多いです。このようなアプリを使う場合、あるいは、単に見た目に統一感を持たせたい場合は、一つのフォントで半角・全角のグリフを持っていると嬉しいです。

M+ FONTS の M シリーズ、Ricty更紗ゴシックなどが有名です。特に Ricty は他にない様々な特徴(後述)を持ったフォントで、一世を風靡しました。

  • Ricty でのテキストの例。
    スクリーンショット 2018-12-16 15.33.21.png

4. リガチャ(合字)に対応していること

これは 2. 読みやすい文字であることには逆行するかもしれませんが、CLI 環境下とは思えないほど表現力を高めることができるので人気です。

Fira CodeHaskligIosevka などが有名です。上述の更紗ゴシックIosevka から派生したフォントであるため、(おそらく日本語対応フォントでは唯一)リガチャに対応しています。

  • 更紗ゴシックでのテキストの例。
    スクリーンショット 2018-12-16 15.28.23.png

ただ、今回作成した SF Mono Square ではこのリガチャには対応させていません。作るのが技術的にも難しいということもあるのですが、個人的に優先度がそこまで高くないということで……。次回は挑戦したいですね。

5. 様々な外字に対応していること

まだリガチャも一般的でなかった昔、CLI の表現力に悩んだ人たち(主に Vim 勢)にとって Powerline は革命と言っていいものでした。Vim や各種シェルでかっこいいステータスライン、プロンプトを実現するためだけに、フォントに新しい外字(グリフ)を追加するパッチを使ったのです。

このアイディアは新しくプログラミング用フォントを作る人にも影響を与えました。その後も相次ぐフォントが Powerline-ready であることをアピールしています。Powerline 自体の人気が下火になった今でも、いわゆる “Powerline glyphs” を含むフォントであることは、プログラミング用であることの証です。

  • ステータスラインをカッコよく見せたいときは必須ですね。
    スクリーンショット 2018-12-16 15.12.16.png

さらに、このアイディアを一層進めたプロジェクトに Nerd Fonts があります。サイトをご覧になれば分かる通り、Font AwesomeOcticons といった、Web アプリの UI 用アイコンに利用されるグリフを、フォントの外字領域に入れてしまうパッチを公開しています。ここまで行くと 2.見やすい混同しないからは大きく外れている気もしますので、過度に依存しないほうがいいかもしれません。

スクリーンショット 2018-12-16 15.04.58.png

Ricty の何が不満なのか

さて、何度かでてきている Ricty です。これは日本語環境におけるプログラミング用フォントとしては、公開から日が経つ今でも決定版と言っていいでしょう。その特徴は公式サイトを見てもらった方が早いのですが、いくつかここにまとめてみます。

  • Ricty を使ったテキストの例 スクリーンショット 2018-12-16 15.33.21.png
  • 半角フォントには人気のある Inconsolata を使用。
  • Migu 1M 由来の見やすい全角文字。特に、濁点、半濁点を識別しやすくした仮名文字が独創的です。
  • 全角空白を可視化。
    • エディタの機能でこれを行うものも多いですが、それがフォントをインストールするだけで達成できるようになりました。
  • 他色々。

さてここで気になったのは、太字にした Inconsolata です。なんか Ricty を使ってターミナルでコードを書いていると、こう、幅がキュッと締まった感じがするんですよね。SF Mono だとそれがないんです。

  • Ricty 16pt
    スクリーンショット 2018-12-16 15.47.44.png
  • SF Mono 14pt
    スクリーンショット 2018-12-16 15.50.36.png

これはどこから来ているのかと言いますと、フォントの縦横比が違うのです

FontForge でフォントの詳細を確認してみる

FontForge はオープンソースのフォント編集ソフトです。Mac/Windows/Linux に対応しています。Mac なら公式サイトでのダウンロードのほか、Homebrew でもインストール可能です。

brew install fontforge
brew cask install fontforge

/Application/FontForge.app を Finder からダブルクリックすると GUI が起動します。ここでは紙面の都合から、詳しい使い方は端折ります。

早速、FontForge で Inconsolata と SF Mono を開いてみましょう。それぞれ以下から手に入れることができます。

Inconsolata SF Mono
スクリーンショット 2018-12-16 15.57.08.png スクリーンショット 2018-12-16 15.57.12.png

同じ「A」のグリフを開いたところです。これをみると Inconsolata の方がグリフを囲む四角形が縦に細長いことがわかりますね。縦横比を計算してみると次のようになります。

縦横比
Inconsolata 1000 500 2.0
SF Mono 2048 1266 1.617694
  • ここで「縦」は Ascent + Descent の値を使っています。詳しい用語の説明はこちら

SF Mono の縦横比、これ、どこかでみたことありませんか? そう。黄金比です! SF Mono のグリフが美しく見えるのにはこんなところに秘密があったのです。

日本語フォントの縦横比

ここで問題になるのが日本語フォントです。英字は割合自由に縦横比を決められますが、日本語で使うカナ・かな・漢字は正方形が基本です。SF Mono と組み合わせるには縦横比をなんとかしないといけません。ここで SF Mono と Migu 1M の Metrics(フォントの形式を決める各数値)を比べてみます。

Ascent Descent Width
SF Mono 1638 410 1266
Migu 1M 860 140 1000
SF Mono Migu 1M
SF Mono Ascent-Descent.png Migu 1M Ascent-Descent.png

並べてみると、縦横比以外にもグリフの配置が違うことがわかります。具体的には、Migu 1M は Ascent を広めに取ってあるので、SF Mono よりも全体的に下によっています。つまり、フォントの縦横比を揃えながら多少上下に動かしてバランスを取る必要があるわけです。

フォントを作る

上記の他にもいろいろ考慮する点があるのですが、まずは実際にフォントを作成してみましょう。

FontForge を使えば GUI で自由にフォントがいじれるのですが、全てのグリフに同じような操作を行うならスクリプトで行うのが簡単です。FontForge は Python を使って自動化ができるようになっていますのでそれを使います。

単純に、2つのフォントから一つを合成するなら以下のように書けます。

import fontforge

font = fontforge.font()
font.mergeFonts('SFMono-Regular.otf')
font.mergeFonts('migu-1m-regular.ttf')
font.generate('merged-font.ttf')

簡単ですね! でもこれでは例の、グリフの大きさ・配置の調整ができていませんし、フォント名その他の情報も載っていません。これらを設定するには膨大な調整が必要になってくるのですが……。以下、実際のスクリプトに従って見ていきましょう。

SF Mono Square の詳細な説明

フォントを作成するスクリプトは以下のレポジトリーにまとめています(Homebrew の tap も兼ねています)。それぞれのスクリプトを読んでいきましょう。

Homebrew Formula –– sfmono-square.rb

ここから先、↑のようなファイル名をクリックすると GitHub へのリンクが開きます。

これは Homebrew の設定ファイルです。中では必要なアプリ(FontForge や Python など)をインストールしたあと、bin/sfmono-square というシェルスクリプトを実行しています。Homebrew の Formula(インストールスクリプト)の書き方はこの記事の主題とは外れるので省きます。

2020 年 7 月現在、Homebrew の fontforge Formula は python@3.8 用にライブラリーをコンパイルするようになっています。OS 標準の Python や、python Formula では利用できないので注意が必要です。

これを解決するため、sfmono-square.rb では動的にパスを追加して次項の build.py を実行するようになっています。

# Set path for fontforge library to use it in Python
fontforge_lib = Formulary.factory("fontforge").lib / "python3.8/site-packages"
# Supply the full path for Python3.8 executable to use with fontforge
python38 = Formulary.factory("python@3.8").bin / "python3"

system python38, "-c", <<~PYTHON
  import sys
  sys.path.append('#{buildpath / 'src'}')
  sys.path.append('#{fontforge_lib}')
  import build
  sys.exit(build.build('#{version}'))
PYTHON

ビルドスクリプト本体 –– build.py

このスクリプトから各段階向けのスクリプトをさらに呼び出しています。その際、複数のフォントファイルをいじるときは並行に動作して CPU をフルに使うため、concurrent.futures モジュールを利用しています。

Migu 1M の調整 –– migu1m.py

このスクリプトでは Migu 1M フォントに各種調整を行なっています。

1. Ascent/Descent(フォントの高さ)の調整

フォントの高さは ascent/descent という2つのプロパティで決められます。この2つの値の比率を SF Mono と同じ割合にしたあと、最後に em プロパティに値をセットするとグリフが自動的に拡大縮小されて調整が行われます。

ASCENT = 1638
DESCENT = 410
OLD_EM = 1000
EM = ASCENT + DESCENT

font.ascent = float(ASCENT) / EM * OLD_EM
font.descent = float(DESCENT) / EM * OLD_EM
font.em = EM

2. グリフを縮小する

黄金比の縦横比を持つ SF Mono と、正方形に収まるグリフの Migu 1M をそのままくっつけると Migu 1M のグリフが相対的に大きくなってしまいます。これをあらかじめ縮小しておく必要がありますが、その割合はもう勘と経験です。今回は 82% という値を使っていますが、反復して試して良さそうな値を探すしかありません。

グリフの変形は glyph.transform() メソッドに変形したい内容を表す行列を与えることでできます。行列を手で書くのは大変なので、psMat モジュールの便利関数を使いましょう。

# 0.82 倍に縮小するための行列
scale = psMat.scale(0.82)
# scale() だけだと左に寄りすぎてしまうので、translate() で右にずらす
# EM は横幅全体を表す値(今回の場合は 2048)
x = EM * (1 - 0.82) / 2
trans = psMat.translate(x, 0)
# 2つの行列の積を求める
mat = psMat.compose(scale, trans)
# 変換実行!
glyph.transform(mat)

ここで厄介なのは translate() メソッドに与える x の値です。半角文字はこの値が全角文字の半分になります。あとで SF Mono で上書きしてしまう半角文字は特にいじらず、ここでは半角カナのみ translate() で右にずらしました。

3. 全角空白を可視化する

Ricty の大きな特徴であった全角空白の可視化を SF Mono Square でも実現します。これは Ricty の生成スクリプトからアイディアを頂戴しました。

font.selection.none()
# ☐  をコピーして 0x3000(全角空白のコードポイント)に貼り付け
font.selection.select(0x2610)
font.copy()
font.selection.select(0x3000)
font.paste()
# ✚  をコピーして 0x3000 に貼り付け
font.selection.select(0x271a)
font.copy()
font.selection.select(0x3000)
font.pasteInto()
# intersect() メソッドで、重なり合った部分のみを残してそれ以外を削除
font.intersect()

making zenkaku space.png

うまいこと考えたもんですね。

4. 斜体を作る

全ての修正を行ったあと、最後に斜体フォントを作ります。Migu 1M フォントには Regular(通常)と Bold(太字)しかありません。SF Mono に合わせて Oblique(斜体)と Bold Oblique(太字斜体)を作りましょう。

skew() メソッドはこの用途にぴったりです。今回は 0.2 という値を与えて行列を生成しています。

mat = psMat.skew(0.2)
font.selection.all()
font.transform(mat)

スクリーンショット 2018-12-16 16.33.25.png

いい感じですね。

SF Mono の調整 –– sfmono.py

次は SF Mono 自体にも修正を加えます。

1. Italic フォントのグリフを補完する

SF Mono は、なぜか Italic, Bold Italic フォントについて罫線素片、矢印といった、一部のグリフが削られています。これを補うため、それぞれ Regular, Bold からコピーします。

font = fontforge.open('SFMono-RegularItalic.otf')
font.mergeFonts('SFMono-Regular.otf')

ちなみに、このような情報を探る場合、pettarin/glyphIgoというツールが便利です。フォントの含むグリフ一覧を表示してくます。

  • ちなみにこのツール、Python3 では動かないので fork して直したやつを載せておきます。
# フォントファイルの含むグリフ一覧を標準出力に吐き出す
glyphIgo.py list -f SFMono-Regular.otf

2. 縦横比を 1:2 にする

さて、ここが肝です。元々はこのためにこのフォントを作ったのでした。

SF Mono を Migu 1M と組み合わせるためには、ちょうど半角の大きさになるように縦横比を調整しないといけません。

縦横比
Migu 1M 1000 1000 1.0
SF Mono 2048 1266 1.617694
SF Mono(調整後) 2048 1024 2.0
OLD_WIDTH = 1266
WIDTH = 1024
SCALE_DOWN = float(WIDTH) / OLD_WIDTH

mat = psMat.scale(SCALE_DOWN)
glyph.transform(mat)
glyph.width = WIDTH
変形前 変形後
スクリーンショット 2018-12-16 15.57.12.png スクリーンショット 2018-12-16 16.38.59.png

ちょっと小さくなって横幅が狭くなっていますね。これで完了です。

SF Mono と Migu 1M の合成 –– sfmono_square.py

いよいよ SF Mono と Migu 1M をくっつけます。その後怒涛の微調整です。

1. フォント情報を設定する

フォントをくっつける前に、空フォントを用意してそれにフォント情報を設定します。

「フォント情報」というのはフォント名やライセンスだけではありません。歴史的な経緯からか Ad Hoc に情報が追加されまくった形跡があり、ひたすらトライアンドエラーで設定すべき値を探しました。

  1. 既存のフォントを FontForge の GUI で開いて設定可能な値を確認。
  2. その値でググって意味を調べる。
    • とにかく情報が少ないです……。Microsoft の OpenType specification を知りたい語句で検索して調べました。
  3. スクリプトで設定する。
    • FontForge の Python API はドキュメントが不十分でほんと辛い。davidhalter/jedi-vim のコード補完を頼りにプロパティを探しました。

設定するには愚直にプロパティへ値を設定しまくるしかないので、詳しいコードはここに載せません。実際のコードを見てみてください。

# 空フォントを作る
font = fontforge.font()
# ひたすら値を設定しまくる
font.ascent = ASCENT
font.descent = DESCENT

...

2. SF Mono と Migu 1M をくっつける

1. で作った空フォントにマージするだけです。瞬殺です。

font.mergeFonts('modified-migu1m-regular.ttf')
font.mergeFonts('modified-SFMono-Regular.otf')

3. Migu 1M 由来の全角文字を SF Mono のグリフに置き換える

ここでの「全角文字」とは Unicode ブロックにおける半角・全角形と言われるものです。いわゆる全角数字とか全角アルファベットですね。コードポイント U+FF01 から始まって 95 文字あります。

Migu 1M 単体で使う分には特に違和感がないのですが、SF Mono と合成してみるとなんだかもっさりした感じに見えてしまいます。これを、SF Mono の半角文字からコピーすることで解決します。

hankaku_start = 0x21
zenkaku_start = 0xff01
glyphs_num = 95
font.selection.none()
for i in range(0, glyphs_num):
    # 半角文字をコピー
    font.selection.select(i + hankaku_start)
    font.copy()
    # 全角文字に貼り付け
    font.selection.select(i + zenkaku_start)
    font.paste()
    font.selection.none()

ただコピーしただけだと、左に字体が寄ったままになってしまいます。ちょっとだけこれを右にずらします。

# 毎度おなじみ translate()
trans = translate(WIDTH / 4, 0)
glyph.transform(trans)

これで終わりではありません。半角・全角形には()「」のような括弧が含まれています。開き括弧は左に寄ったままでよく、逆に閉じ括弧は通常よりもっと右に寄せた方が良いです。

開き括弧 閉じ括弧
move-to-right.png move-to-left.png

さらに、一部の変則的な括弧⦅⦆半角・全角形のみにあって半角バージョンはありません。これらは Migu 1M 由来のグリフをそのまま使いました。こういうのほんと困る……。

  • 変形前 スクリーンショット 2018-12-16 16.59.08.png
  • 変形後 スクリーンショット 2018-12-16 16.59.12.png

これでやっと(ここは)完成です。

4. 一部の記号を半角にする

最近は Unicode の普及のせいか、CLI ツールも様々な記号を使うものが多くなってきました。例えば jonas/tig では、記号をふんだんに使って綺麗なコミットツリーを表してくれます。

スクリーンショット 2018-12-16 17.03.01.png

このような CLI ツールで使われている●∙◎といった記号は SF Mono にないものが多いです。Migu 1M のものを使うしかないのですが、当然それらは全角文字となっています。CLI ツールの多くはこれらの記号、より専門的には East Asian WidthA の字体について半角であることを期待します。今回のフォントにおいても、代表的なものを半角で表すことにしました。

# 以下のグリフは半角にする
HANKAKU_GLYPHS = [
    0x25a0,  # ■  BLACK SQUARE
    0x25a1,  # □  WHITE SQUARE
    0x25cb,  # ○  WHITE CIRCLE
    0x25cc,  # ◌  DOTTED CIRCLE
    0x25ce,  # ◎  BULLSEYE
    0x25cf,  # ●  BLACK CIRCLE
    0x25ef,  # ◯  LARGE CIRCLE
]

さて、全角のグリフをどうやって半角に変形して違和感のないようにするか? ですが、これには簡単な方法はありません。実際のグリフとにらめっこして、その都度最適な変換行列を作るしかありません。

ここで、何度かでてきている psMat.scale() の挙動が問題になります。このメソッドによる拡大縮小は少し直観的でない動きをするのです。

# (例)グリフを 0.8 倍にする
mat = psMat.scale(0.8)
glyph.transform(mat)

「0.8 倍にする」とは言っても、どこを中心として縮小してくれるのか指定する方法がないし、どこが中心点なのかドキュメントにも記載がありません。実際に試してみたところ、中心点は (0, DESCENT) にあることがわかりました。

center-for-scale.png

さらに、Migu 1M の各種記号は左下隅が (DESCENT, DESCENT)、右上隅が (WIDTH - DESCENT, WIDTH - DESCENT) である正方形に収まることもわかりました。これを基にして、以下のようにすれば半角のグリフを生成できます。

WIDTH = 2048
DESCENT = 410
SCALE_DOWN = 0.65  # 縮小比率。この比率は何度か試して決めました。

orig_glyph_width = WIDTH - DESCENT * 2  # 元々のグリフの幅、および、高さ
glyph_width = float(orig_glyph_width) * SCALE_DOWN  # 変換後のグリフの幅、および、高さ
trans_x = (WIDTH / 2 - glyph_width) / 2  # 縮小後に右にずらす
trans_y = (WIDTH - glyph_width) / 2 - DESCENT  # 縮小後に上にずらす
trans = psMat.translate(trans_x, trans_y)
origin = psMat.translate(-DESCENT, 0)  # グリフを中心点に合わせる行列
scl = psMat.scale(SCALE_DOWN)  # グリフを縮小する行列
mat = psMat.compose(psMat.compose(origin, scl), trans)
glyph.transform(mat)
変形前 変形後
スクリーンショット 2018-12-16 17.19.26.png スクリーンショット 2018-12-16 17.19.29.png

いやー、大変ですね。数学苦手なので余計疲れました。

Nerd Fonts グリフの合成 –– font_patcher.py

最後に、Nerd Fonts によるグリフを合成します。最初これには公式スクリプトである font-patcher を利用していたのですが、いくつか修正を加えるためにまるっと書き直しちゃいました。具体的には、以下の点で本家 Nerd Fonts と異なっています。

1. Octicons, Material Icons のコードポイントを変更

Nerd Fonts SF Mono Square
Octicon U+F400 .. U+F505 U+F400 .. U+F4e9
Material Icons U+F500 .. U+FD46 U+E800 .. U+EFFF, U+F500 .. U+F546

なぜこのような変更をしたかと言いますと、Nerd Fonts の利用している領域はこのような外字に使って良いと定められている私用領域(Private Use Area, U+E000 .. U+F8FF)を逸脱しているからです。私用領域の直後の U+F900 から始まるCJK 互換漢字には、「﨑(U+FA11)」「福(U+FA1B)」といった、人名などの固有名詞によく使う漢字が含まれているのです。これは困ります。

2. 各グリフの大きさを調整

Nerd Fonts には Octicon, Material Icons 以外にも様々なアイコンセットが含まれているのですが、別々に開発されているものなのでグリフの大きさはまちまちです。これをそれぞれ微修正することによってできるだけ同じような大きさに見えるように調整しました。

if info['name'] == 'Seti-UI + Custom':
    # この例では縦横それぞれ 1.1 倍に拡大した上、
    # 左に 100、下に 450 動かしています。
    x_ratio = 1.1
    y_ratio = 1.1
    x_diff = -100
    y_diff = -450

elif info['name'] == 'Devicons':
    # 以下延々と続く
    ...

上記のような細かい修正を入れています。完全なリストは実際のコード を見て見てください。

apngb-animated.png

修正前後の比較なんですが……こうして比べてみると超微妙ですね。でも結構気になるんですよこれが!

最後に

フォントの作成について詳しく見てきました。僕がやったのは既存のフォントから合成するだけだったのですが、こうしてみると細かい部分の調整が非常に大変です。一からフォントを作成される方には本当に頭が下がります……。

FontForge によるフォントの修正についてはウェブ上でも大変情報が少ないです(特に日本語では)。この記事がフォントマニアの快適な人生の一助になれれば幸いです。

288
204
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
288
204