某学内サイトにおけるキリル文字フォント問題を考える

注:この記事は技術的誤りに関する指摘を歓迎します

キリル文字フォント問題とは、某学内サイトにおいてページ内のキリル文字に対して不適切なフォント(ヒラギノ角ゴシック)が適用されてしまう問題でした。この件はクラス内でも問題になっていたので先方に報告もしたのですが、それ以上に先生方や学生を含む多くの方の精力的な働きかけと運用サイドの迅速な対応によって、現在では無事解決に至っているようです。

さて、この問題の現象をより正確に述べるとすれば、それはキリル文字のコードポイント(基本的なアルファベットの範囲では 0x0410 から 0x044F まで)に対して幅の広すぎるグリフがマッピングされてしまうという問題でした。これは直接的には当該サイトの CSS の font-family の設定不備に起因していました。

そもそも CSS の font-family プロパティは複数のフォント名(厳密にはフォントファミリー名)をカンマ区切りで指定できるようになっていますが、適用されるフォントの選択は文字ごとに行われます。そして通常は、より先に指定されたフォントのうち対象文字のグリフをサポートしているものが優先されるようになっています(「通常は」と断ったのは、異体字セレクタなどの結合文字の扱いは必ずしも文字単位ではないからです)。このアルゴリズムは Font Matching Algorithm として CSS Fonts Module 仕様で定められていて、その実装は例えば Chrome であれば Blink と呼ばれるレンダリングエンジンの内部(ソースコード)に含まれています。

ところで、ご存知の通り HTML 文書には(UTF-8 が強く推奨されているものの)多様なエンコーディングが許されています。したがってブラウザは、いかなる有効なエンコーディングのバイト列をも正しくデコードし、 Unicode のコードポイントに変換してから上述の Font Matching Algorithm に与える必要があります。もちろんフォントによっては予め Unicode 以外のコードポイントからグリフへのマッピングを持つものも存在しますが、そうしたマッピングはアルゴリズムに影響を与えてはならないと規定されています。要するに一旦全部 Unicode にしてからアルゴリズムに通しなさいよということです。

さて話が脱線しましたが、次に Font Matching Algorithm と件の問題との関連を確認していきます。問題のサイトにはかつて次のようなスタイル指定が存在し、キリル文字の表示部分に適用されていました(繰り返しますが、現在では修正済みです)。

* {
font-family: メイリオ, Meiryo, "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "MS Pゴシック", "MS P Gothic", Osaka, "MS ゴシック", Verdana, Arial, Helvetica, Gothic, sans-serif;
box-sizing: border-box;
}

ご覧の通り font-family と box-sizing が全称セレクタで指定されています。余談ですが、全称セレクタを使用するのはメンテナビリティの観点でバッドプラクティスのひとつです(昔はパフォーマンス悪化も理由のひとつでしたが、現在ではブラウザ性能の向上によってそちらは無視できるほどになっています)。私もコードベース全体を把握しているわけではないのでもしかすると大域的には妥当な判断だったのかもわかりませんが、とはいえなるべく使用を避けるのが普通の感覚だと思います。が、今回そのことは議論しないことにします。さて本題に戻って font-family を見てみると、メイリオやヒラギノといったフォントが先にあるのがわかります。とくにヒラギノはキリル文字に対応するグリフを含んでいます。たとえば macOS においてヒラギノが「Б (U+0411)」に対応するグリフを含んでいることは、fonttools を用いて以下のように確認できます。

> echo -e '\u0411'
Б
> ttx -y 3 ヒラギノ角ゴシック\ W3.ttc
> grep 'code="0x411"' ヒラギノ角ゴシック\ W3\#4.ttx
<map code="0x411" name="cid01060"/><!-- CYRILLIC CAPITAL LETTER BE -->

ここで "ヒラギノ角ゴシック W3.ttc" ファイルはいくつかのフォントのコレクションであり、上ではその3番目 "ヒラギノ角ゴ Pro W3" の情報のみを抽出しています。grep の結果より、ヒラギノが「Б (U+0411)」に対応するグリフを含んでいることがわかります。

つぎにグリフの幅を比較してみます。cid の取得部分は省略しますが、以下のコードで上から順に cid01060, cid00843, cid00035 はそれぞれ「Б (U+0411)」「あ (U+3042)」「B (U+0042)」に対応します。

> grep 'mtx name="cid01060" width' ヒラギノ角ゴシック\ W3\#4.ttx
<mtx name="cid01060" width='1000' lsb="227"/>
> grep 'mtx name="cid00843" width' ヒラギノ角ゴシック\ W3\#4.ttx
<mtx name="cid00843" width='1000' lsb="109"/>
> grep 'mtx name="cid00035" width' ヒラギノ角ゴシック\ W3\#4.ttx
<mtx name="cid00035" width='712' lsb="77"/>

ここで mtx タグの width 属性の値は、OpenType 規格において文字幅を規定する advanceWidth に相当します(なぜか fonttools は "advanceWidth" を "width" にリネームしてしまうようです)。「Б (U+0411)」と「あ (U+3042)」が同じ幅である一方、「B (U+0042)」は少し狭いことがわかります。これが、ヒラギノにおいてキリル文字が "全角幅" になってしまう原因です。以上のことから、メイリオが無くヒラギノを標準搭載する macOS では件の問題が生じてしまうという結論になります(ちなみにメイリオのキリル文字のグリフは "半角幅" です)。ただし、macOS のバージョンによっては標準搭載のフォントが違ったり、ブラウザのバージョンによってはバグがあったり、個々人の環境によっては macOS なのにメイリオが入っていたり拡張機能が働いていたりするので、macOS だからといって一概に問題が再現するとは言い切れないのが微妙なところです。

それでは、なぜヒラギノはキリル文字に "全角幅" のグリフを与えてしまっているのでしょうか。参考文献として Unicode の附属書 East Asian Width を読んでみます。これは Unicode の規格でありながら文字幅(注意:文字符号化形式のビット数のことではない)に関する情報を規定している面白い文書です。ここでは「文字幅が曖昧な文字 ambiguous characters 」というものについて次のように述べられています。

Ambiguous characters occur in East Asian legacy character sets as wide characters, but as narrow (i.e., normal-width) characters in non-East Asian usage. (Examples are the basic Greek and Cyrillic alphabet found in East Asian character sets, but also some of the mathematical symbols.)
(引用元: http://www.unicode.org/reports/tr11/

私はこの記事を書くまで知らなかったのですが、この文言によればどうやら歴史的に東アジアではキリル文字を全角幅で扱っていた経緯があるようです。また同文書は、こうした ambiguous characters を描画する際はフォントなどのコンテキストに応じて全角幅/半角幅を決めることを推奨しています。したがって、日本語フォントであるヒラギノは、日本語というコンテキストのもとでキリル文字の文字幅を解釈した結果として "全角幅" のグリフを提供しているのではないかと考えられます。もちろんこの考え方は推測に過ぎませんから、誤りなどは修正していきたいと思いますのでご指摘いただけると幸いです。

さて個人的に Unicode におけるキリル文字の扱いで不思議なのは、ローマ字はコードポイントのレベルで全角/半角が区別されているのに対し、キリル文字はそうでない(1種類しかない)という点です。したがって、キリル文字をいかなる文字幅でブラウザ上に描画するかはフォントの定めるところに依存してしまっているわけです(ウェブブラウザ以外のアプリケーションでは ambiguous characters の全角幅/半角幅をユーザが切り替えられる場合もあります)。このようになった経緯として、JIS X 0201 と JIS X 0208 の文字集合が Unicode に取り入れられた際にローマ字は両規格に含まれていたので後方互換性のため全角/半角が区別されたが、キリル文字はそうでなかったから結局ひとつのコードポイントに統合されてしまった…というような指摘をされているツイートを拝見して、大変勉強になりました。考えてみるとむしろ、全角/半角が区別されているほうが不思議なことなのかもしれません。

最後に、Font Matching Algorithm を逆手(順手?)に取って日本語フォントの読み込みを高速化している例として Google Fonts を紹介します。Google Fonts が提供する CSS を読むとわかりますが、彼らは unicode-range プロパティを利用してフォントをサブセット化しており、必要最小限のチャンクだけが読み込まれるように設計されています。これによってもうひとつのメリットがあります。それは、今回の問題のような font-family の設定不備に強いということです。たとえば Noto Sans JP はキリル文字のグリフを持つフォントですが、Google Fonts が提供する CSS では @font-face 指定の unicode-range からキリル文字の範囲が省かれているため、font-family の順序がどのようであってもキリル文字に Noto Sans JP が適用されてしまうことはありません。したがって、どうしてもエンドユーザ環境によらずに文字の見た目を保ちたいならば(実際こうした需要は根強いものですが)、ウェブフォントの利用が手っ取り早いように思います。