備忘録:rubyでttc & ttfファイルからフォント名を取り出す。

覚え直すのは多分三回目くらい。毎度の如く、「よし出力だ、ただし詳細説明&コードの最適化してからな!」と思ってたら、
案の定の浦島メソッドにつき、結局雑把なコードと簡易コメントのみで。

# coding: utf-8

FONTS_PATH = {Windows_NT: "フォントフォルダパス"}[ENV["OS"].to_sym]
Dir.chdir(FONTS_PATH)

def parse_ttf(str, ttf_head_offset = 0) # j = ttc内index、ttf単独なら当然0
    # TTFヘッダ(ttf_head_offset)先頭4byte〜 2byte整数で、オフセットテーブル数
    (ttf_table_num = str[ttf_head_offset + 4, 2].unpack("n").pop).times { |j|
        # TTFヘッダ(12byte分)後、各オフセットテーブルの構造が続く(ここではnameのみをターゲットに)
        table_offset = ttf_head_offset + 12 + 16 * j
        # テーブル先頭〜 4byte符号無し整数でタグ名の文字列
        if str[table_offset, 4] == "name"
            # テーブル先頭8byte〜 4byte符号無し整数で、タグ(name)の実体を指すオフセット
            name_offset = str[table_offset + 8, 4].unpack("N")[0]
	    # タグの実体に飛び、その先頭〜 2byte符号無し整数でレコード数、
	    # さらに2byteでレコード情報の文字列実体へのオフセット
            record_count, storage_offset = str[name_offset + 2, 4].unpack("nn")
            # レコード開始位置
            record_offset = name_offset + 6
            record_count.times { |k|
            	# レコード開始+4byteの位置〜 2byteで言語(japanese等)ID、さらに2byteで名前(フォント名等)ID
                language_id, name_id = str[record_offset + 4 + 12 * k, 4].unpack("nn")
                # 日本語フォントまたはフォントのフルネーム情報以外はスキップ
                next if language_id != 0x411 or name_id != 4
                # レコード開始+8byteの位置〜 2byteでフォント名の長さ、さらに2byteでそのオフセット
                string_length, string_offset = str[record_offset + 8 + 12 * k, 4].unpack("nn")
                # 各オフセットを合計して、フォント名への位置を取得
                pos = name_offset + storage_offset + string_offset
                # 最終的に得られたバイナリ文字列をUTF_16BEに変換して、大域脱出と共に返す。
                # rescue節は変換失敗の際に無理くりスキップさせるため。
                # ここではさらにutf-8に変換して、ファイルエンコーディングに合わせている。
                font_name = str[pos,string_length].force_encoding(Encoding::UTF_16BE).encode(Encoding::UTF_8) rescue next
                return font_name
            }
        end
    }
end

result = {} # フォント名: [ フォントパス, インデックス ]
Dir.glob("*.tt[fc]") { |path|
    # バイナリ文字列として変数に格納しておく
  # 全部読み込みたく無い場合は、このまま中でファイルオブジェクト使って良しなに。
    str="";open(path, "rb") { |f| str = f.read }
    # .ttc = 複数のttfデータを格納する形式
    if path =~ /\.ttc/
    	 # 先頭8byte〜 32bit整数で、ttc内に含まれるフォント(ttfデータ)数
        (ttf_num = str[8,4].unpack("N*")[0]).times { |i|
            # 先頭12byte〜 32bit整数で、フォントの数だけ各フォントの開始オフセットが並ぶ
            ttf_head_offset = str[12 + i * 4, 4].unpack("N")[0]
            # オフセット位置を元に、ttfデータの解析を行い、戻り値(結果)を格納する。
            font_name = parse_ttf(str, ttf_head_offset)
            result[font_name] = [path, i] if font_name.kind_of?(String)
        }
    else
    	# こちらはttfデータそのままなので、一気に解析して結果まで。
        font_name = parse_ttf(str)
        result[font_name] = [path, 0] if font_name.kind_of?(String)
    end
}
# 結果出力
result.each {|r| puts r.join(",") }

今回はフォント名の取得のみでしたが、その他の情報を得たい場合は、http://www.microsoft.com/typography/otspec/otff.htmを参照して辿るべし。