日本語のウェブフォントをまともに使えるようにするよ(サブセット化)#2

前回のエントリー
日本語のウェブフォントをまともに使えるようにするよ(サブセット化)#1

というわけで物を作る

準備する物

  • Python(私が使ったバージョンは2.7.x)
  • pip
    • pipでインストールするモジュール
      • FontTools
  • フリーフォント
  • 負けない心

必要なモジュールのインストール

※ Pythonとpipはインストール済みを前提としています

# pip install FontTools

全体の流れ

  1. 管理画面にて何かしらのアクションを行う
    • ボタンクリックや保存処理など
  2. バックエンド側に処理開始のトリガーになるリクエストを送信
    • Python側でリクエスト取得後、DBからWebサイトの表示に利用しているテキストデータの全取得を行う
  3. 取得したテキストデータをWebFont最適化クラスに渡す
    • このタイミングで、最適化を行う対象のフォントファイルおよび最適化後のフォントファイル出力先などの設定も行えるようにしておく
  4. 上記で渡した処理完了後、出力パス先に最適化されたフォントファイルが生成される

対象文字列を最適化処理にかける部分のサンプル

DBから対象文字列を取得して渡しているだけなので、実装イメージを載せておきます

# DBコネクション管理インスタンス
conf = Conf()
curs = conf.cursor
# 対象テーブルからフォントに必要なフィールドを全取得
curs.execute('SELECT body_md FROM posts')
r = curs.fetchall()

# WebFont用のインスタンス生成(これから実装を行う)
wbft = WebFont()
# DBから取得した文字列データを結合するため配列に格納
s = []
for e in r:
    s.append(e['body_md'])

# 上記で用意した配列を結合して対象文字列としてWebFontクラスに渡し
# 最適化対象フォントファイルおよび最適化後フォントファイルを指定
wbft.optimize(''.join(s),
        ipath='static/assets/fonts/mplus-1p-light.ttf',
        opath='static/assets/fonts/optim.ttf')

WebFontクラスを実装する

上記の実装例に出てきた WebFont クラスを実装します。
このクラスで用意するメソッドは3つ

  • WebFont class
    • __init__ method
      • インスタンス初期化処理
    • optimize method
      • 最適化WebFont出力処理
    • _convertUnicode method
      • 対象文字列のUnicode変換処理

上記クラスのスタブを作ります

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

import fontTools.ttLib.tables
import fontTools.ttLib

import os.path
import sys

class WebFont(object):
    # 対象フォントファイルパス保持用
    ipath = None
    # 最適化フォントファイル出力パス保持用
    opath = None

    def __init__(self):
        pass
    def optimize(self, chars, ipath=None, opath=None):
        pass    
    def _convertUnicode(self, chars):
        pass

続いて、インスタンス初期化処理のタイミングで ipath および opath の初期値設定を行っていきます。(正直これはやってもやらなくてもどっちでも良いかと)

__init__ method

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

import fontTools.ttLib.tables
import fontTools.ttLib

import os.path
import sys

class WebFont(object):
    # 対象フォントファイルパス保持用
    ipath = None
    # 最適化フォントファイル出力パス保持用
    opath = None

    def __init__(self):
        self.ipath = 'mplus-1p-regular.ttf'
        self.opath = 'optimize-{0}'.format(self.ipath)
    def optimize(self, chars, ipath=None, opath=None):
        pass    
    def _convertUnicode(self, chars):
        pass

optimize を実装する前に、先に _convertUnicode を実装しちゃいましょう。

_convertUnicode method

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

import fontTools.ttLib.tables
import fontTools.ttLib

import os.path
import sys

class WebFont(object):
    # 対象フォントファイルパス保持用
    ipath = None
    # 最適化フォントファイル出力パス保持用
    opath = None

    def __init__(self):
        self.ipath = 'mplus-1p-regular.ttf'
        self.opath = 'optimize-{0}'.format(self.ipath)
    def optimize(self, chars, ipath=None, opath=None):
        pass    
    def _convertUnicode(self, chars):
        # 例外的な文字列については、予めココでセットしUnicodeに変換を行う
        # 未定義文字
        # 半角スペース
        # CR
        # NULL文字
        chars = map(ord, chars) + [0,13,-1,32]
        # 重複するコードを削除し、ソート用にIndexを割り当てる
        chars = sorted(set(chars), key=chars.index)
        # ユニークはコードリストのソートを行う
        chars.sort()

        return chars

最初に渡ってくる chars は、最適化対象の文字列がすべて入っている文字列データです。

その文字列データに対して ord メソッドを利用してUnicodeに変換します。その際に固定値の配列を結合していますが、渡ってくる文字列データに含まれる可能性が低い特殊文字(CRとかは入っているかもですが・・・)のデータを固定で追加しています。

その後、変換したUnicodeの配列に対して set メソッドを利用して、重複するUnicodeをすべて削除し、かつソート用の index を割り当てます。

そして、ユニークになったUnicodeの配列をソートしリターンするといった処理が _convertUnicode になります。

続いてメインの処理の optimize メソッドを実装していきます。

optimize method

ちょっと長いので、一度全体を記載した上で少しずつ見ていきます。
ということで最終的なソースコードが次の形になります。

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

import fontTools.ttLib.tables
import fontTools.ttLib

import os.path
import sys

class WebFont(object):
    # 対象フォントファイルパス保持用
    ipath = None
    # 最適化フォントファイル出力パス保持用
    opath = None

    def __init__(self):
        self.ipath = 'mplus-1p-regular.ttf'
        self.opath = 'optimize-{0}'.format(self.ipath)

    def optimize(self, chars, ipath=None, opath=None):
        if chars != None:
            if ipath != None:
                self.ipath = ipath
            if opath != None:
                self.opath = opath

            if not os.path.exists(self.ipath):
                print 'file not found:{0}'.format(self.ipath)
                return False

            # フォントデータ解析用TTFontインスタンス生成
            ttfont = fontTools.ttLib.TTFont(self.ipath)
            # 対象文字列のUnicodeコンバート
            chars = self._convertUnicode(chars)
            # 出力対象グリフ名格納用
            lists = set()

            # fomart4形式のマッピングサブテーブルの取得
            subtbl = ttfont['cmap'].getcmap(3,1)

            # 各グリフデータに対するイテレータ処理
            for name, g in ttfont['glyf'].glyphs.items():
                # グリフ名を元にグリフIDを取得
                gid = ttfont.getGlyphID(name)
                # Unicode変換時に固定で追加を行った例外的な
                # 文字列については固定で処理を行っている
                if gid == 1:
                    code = 0
                    lists.add(name)
                elif gid == 2:
                    code = 32
                    lists.add(name)
                elif gid == 3:
                    code = 13
                    lists.add(name)
                elif gid == 0:
                    code = -1
                    lists.add(name)
                else:
                    # それ以外の文字列については、先ほど取得を行った
                    # format4のサブテーブルからグリフIDを利用してUnicodeを取得
                    try:
                        code = subtbl.cmap.keys()[subtbl.cmap.values().index(name)]
                    except:
                        code = -2

                #pprint(code)

                # 最初に定義した対象文字列のUnicodeかどうかの判定
                # ヒットする場合はフォント出力の対象になる
                if code in chars:
                    lists.add(name)

            for g in dict(ttfont['glyf'].glyphs):
                if g in lists:
                    print 'skipping {0}'.format(g)
                    continue
                ttfont['glyf'].glyphs[g] = fontTools.ttLib.tables._g_l_y_f.Glyph()

            ttfont.save(self.opath)
            ttfont.close()

            return True
        else:
            return False

    def _convertUnicode(self, chars):
        # 例外的な文字列については、予めココでセットしUnicodeに変換を行う
        # 未定義文字
        # 半角スペース
        # CR
        # NULL文字
        chars = map(ord, chars) + [0,13,-1,32]
        # 重複するコードを削除し、ソート用にIndexを割り当てる
        chars = sorted(set(chars), key=chars.index)
        # ユニークはコードリストのソートを行う
        chars.sort()

        return chars

ちょっと長いですね :(
では、少しずつ見ていきましょう。

まず最初の

def optimize(self, chars, ipath=None, opath=None):
    if chars != None:
        if ipath != None:
            self.ipath = ipath
        if opath != None:
            self.opath = opath

        if not os.path.exists(self.ipath):
            print 'file not found:{0}'.format(self.ipath)
            return False
  • 対象文字列の空チェック
  • 入力および出力パスに値が入っていればプロパティを更新
  • 入力パスが正しいかどうかチェック

を行っているだけですね :)


# フォントデータ解析用TTFontインスタンス生成
ttfont = fontTools.ttLib.TTFont(self.ipath)
# 対象文字列のUnicodeコンバート
chars = self._convertUnicode(chars)
# 出力対象グリフ名格納用
lists = set()
  • 入力パスに指定されているフォントファイルを元に、解析用TTFontインスタンスの生成
  • 対象文字列のUnicode変換処理
    • ここで先ほどの _convertUnicode メソッドを利用します
  • 対象文字列とマッピングを行うグリフ名のリスト保持用変数を用意

# fomart4形式のマッピングサブテーブルの取得
subtbl = ttfont['cmap'].getcmap(3,1)
  • Unicodeのマッピング情報を取得する際に利用する、フォーマットのサブテーブルの取得

本来であればサブテーブルすべてを見たりした方が良いのかもしれないですが、とりあえず今回は大丈夫そうなので format4 のサブテーブルのみを利用しています。


# 各グリフデータに対するイテレータ処理
for name, g in ttfont['glyf'].glyphs.items():
    # グリフ名を元にグリフIDを取得
    gid = ttfont.getGlyphID(name)
    # Unicode変換時に固定で追加を行った例外的な
    # 文字列については固定で処理を行っている
    if gid == 1:
        code = 0
        lists.add(name)
    elif gid == 2:
        code = 32
        lists.add(name)
    elif gid == 3:
        code = 13
        lists.add(name)
    elif gid == 0:
        code = -1
        lists.add(name)
    else:
        # それ以外の文字列については、先ほど取得を行った
        # format4のサブテーブルからグリフIDを利用してUnicodeを取得
        try:
            code = subtbl.cmap.keys()[subtbl.cmap.values().index(name)]
        except:
            code = -2

    #pprint(code)

    # 最初に定義した対象文字列のUnicodeかどうかの判定
    # ヒットする場合はフォント出力の対象になる
    if code in chars:
        lists.add(name)

長いですね・・・ ;(
ここでは

  • グリフデータの一覧を取得し、グリフ名を取得
  • グリフ名を元にcmapからマッピング情報のUnicodeを取得
    • 例外的な特殊文字については、固定で記述
  • あらかじめ用意していた対象文字列のUnicodeリストに該当する物があれば、そのグリフ名をlistsに格納

これで、対象文字列を抽出するための用意が完了します。


for g in dict(ttfont['glyf'].glyphs):
    if g in lists:
        print 'skipping {0}'.format(g)
        continue
    ttfont['glyf'].glyphs[g] = fontTools.ttLib.tables._g_l_y_f.Glyph()

ttfont.save(self.opath)
ttfont.close()

そして最後に

  • 対象文字列以外のUnicodeだった場合は、空のグリフ情報で潰す
    • ここ合ってるか怪しいです
  • あらかじめ用意しておいた出力パスにフォントファイルを出力
  • TTFontのインスタンス破棄

といった流れになります。


実際に使ってみると

mplus-1p-light.ttfを対象に、管理画面から投稿時の処理をトリガーにフォントファイルをはき出すようにしてみたところ下記のようになりました。

optim.ttf が最適化後フォントファイル

そこそこ大きい・・・ :(

けど、やらないよりはマシですよね。
あとは、woff形式に変換してあげればもう少し小さくなるかな・・・

ただ難点としては、記事が膨大になって使っている文字種が増えれば増えるほど結局元のフォントファイルのサイズに近づいてしまうという欠陥がありますが・・・w


やってみて分かったこと

OpenType(というかフォント周り)の仕様難杉わろた・・・
でもなんか新鮮で面白かったです

なんか、ほかにも色々あるみたいなのでやってみたいなー :)