cockscomblog?

cockscomb on hatena blog

Webフォントを使うときは空のグリフに注意

昨今のWebデザイン界隈ですと、それは当然Webフォントを使おうということにもなろうと思いますが、注意すべき事柄に気付きましたのでここに御シェアさせていただきたく存じます。

f:id:cockscomb:20130423201402p:plain

問題

フォントの中にはたくさんのグリフ(字形)が入っていて、「あ」だったらこのグリフ、「い」だったらこのグリフ、というように対応しています。たくさんの文字を作るのは大変だから、たとえばひらがなとカタカナだけとか、第一水準漢字までとか、少ないグリフだけのフォントもあります。

Webフォントを設定していて、存在しないグリフの文字があったとすると、ブラウザが良い感じにフォールバックして別なフォントで表示してくれたりします。またCSSのunicode-rangeというのを使うと、あるフォントから使うグリフをUnicodeのコードポイントの範囲で設定することができます。範囲外のグリフはやはりフォールバックされます。((unicode-rangeはIE9以降とWebKit系のブラウザで対応しているようです))

ところで、もしグリフは存在しているけどその中身が空だったらどうなるでしょうか。こういうときは何も表示されていないように見えます。つまり空のグリフが入ったフォントをWebフォントに使うと、見えない文字ができてしまいます。

こういうフォントについて適切に対策しないと読めない文字が生まれて、訪問者を困らせてしまうことになるでしょう。これはユーザビリティが低く、避けるべきです。

解決

フォントの改変が許されている場合は空のグリフを削除するのがいちばんよいです。ブラウザの互換性の問題もなく、ファイルサイズも減ります。Homebrewでfontforgeをインストールして、Pythonのスクリプティングインターフェースを用いてフォントを操作しましょう。

brew install fontforge —with-x

こういう感じでインストールします。--with-xオプションを使うとX11でGUIが使えるからちょっと便利。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import fontforge
import argparse


def remove_empty_glyphs(input, output):
    font = fontforge.open(input)
    code_points = []
    for glyph in font.glyphs():
        # 実際には glyph のデータが空になっていることがある
        if glyph.left_side_bearing != 0.0 and glyph.right_side_bearing != 0.0:
            pass
        else:
            font.removeGlyph(glyph)
    font.generate(output)
    font.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Make unicode-range from font.')
    parser.add_argument('input', nargs=1, help='path to input font file')
    parser.add_argument('output', nargs=1, help='path to output font file')
    args = parser.parse_args()
    input_file_path = args.input[0]
    output_file_path = args.output[0]


    remove_empty_glyphs(input_file_path, output_file_path)

Pythonのスクリプトはこういう感じで

python remove-empty-glyphs.py INPUT_FONT_FILE OUTPUT_FONT_FILE

みたいな感じで使います。拡張子に応じて保存されるフォントのフォーマットが変わって便利。

フォントの改変ができない場合は、ブラウザの互換性問題がありつつもunicode-rangeを設定しておくのがよさそうです。@font-face {}unicode-range: U+XXX-XXX, U+XXX;のようなかたちで指定します。unicode-rangeをフォントの情報から生成するスクリプトを用意しました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import fontforge
import argparse


def _code_points_from_font(filepath):
    font = fontforge.open(filepath)
    code_points = []
    for glyph in font.glyphs():
        point = glyph.unicode
        # unicode が見つからないとき -1
        # 実際には glyph のデータが空になっていることがあるから side_bearing が 0.0 のものを除く
        if point != -1 and glyph.left_side_bearing != 0.0 and glyph.right_side_bearing != 0.0:
            code_points.append(point)
    return sorted(code_points)


def _code_ranges_from_code_points(code_points):
    code_ranges = []
    last_point = code_points[0]
    code_ranges.append([last_point])
    for point in code_points:
        if point - last_point > 1 and point != last_point:
            # 連続していない
            code_ranges[-1].append(last_point)
            code_ranges.append([point])
        else:
            # 連続している
            pass
        last_point = point
    return code_ranges


def unicode_range_from_font(filepath):
    code_points = _code_points_from_font(filepath)
    code_ranges = _code_ranges_from_code_points(code_points)
    
    unicode_ranges = []
    for code_range in code_ranges:
        if len(code_range) > 1:
            unicode_ranges.append('U+{0:X}-{1:X}'.format(code_range[0], code_range[-1]))
        elif len(code_range) == 1:
            unicode_ranges.append('U+{0:X}'.format(code_range[0]))

    unicode_range = ', '.join(unicode_ranges)
    unicode_range = 'unicode-range: {0};'.format(unicode_range)

    return unicode_range


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Make unicode-range from font.')
    parser.add_argument('font', nargs=1, help='path to font file')
    filepath = parser.parse_args().font[0]

    unicode_range = unicode_range_from_font(filepath)
    print unicode_range

これを用いて

python unicode-range-maker.py FONT_FILE | pbcopy

のようにするとクリップボードにunicode-rangeがコピーされます。これをCSSに書いておきましょう。

@font-face {
  font-family: "name";
  src: local("path");
  unicode-range: ...;
}

第一水準漢字までしか表示できないと「櫻井翔」が「 井 」みたいになってしまいますから、対応必至ですね。

gist