Hatena::ブログ(Diary)

Life like a clown このページをアンテナに追加 RSSフィード Twitter

2010-01-08

TrueType フォント名を取得する

TrueType フォント名を取得する必要があったので作成してみました.今回必要だったのは,(ASCII での)フォント名とフォント・ファイル名(*.ttf)のマッピング,および日本語でのフォント名と ASCII でのフォント名のマッピングでした.尚,コードは FontForgeshowttf.c と言うプログラムを参考に作成しました.

全体として注意する事は,*.ttf ファイルはビッグ・エンディアンで値が格納されている事です.ファイルを扱っているとエンディアン関係で悩まされる事は頻繁に遭遇するので,以下のような関数を定義しておく事にしました.

template <class Ch, class Tr, class Type>
inline bool get(std::basic_istream<Ch, Tr>& in, Type& dest, std::size_t which = endian::little);

istream から sizeof(Type) バイト読み込んで,dest に格納します.which が endian::big の場合は,返す直前に dest に対して std::reverse() を適用してバイト列を反転させています(参考: エンディアンを考慮したメモリコピー - Life like a clown).

TTF ヘッダの解析

namespace cliff {
    class true_type {
    public:
        true_type();
        explicit true_type(istream_type& in);
        explicit true_type(const string_type& path);
        ~true_type() throw();
        
        bool read(istream_type& in);
        bool read(const string_type& path);
        
        /* ----------------------------------------------------------------- */
        /*
         *  access methods.
         *
         *  解析したブロックに応じて増やしていく.
         */
        /* ----------------------------------------------------------------- */
        name_block& names();
        const name_block& names() const;
    };
}

*.ttf ファイルは,「ブロック」と呼ばれる単位で様々な情報が管理されています.ヘッダには,各ブロックの「識別名,チェックサム,オフセット値,ブロック長」の値がブロック数分格納されています.ヘッダを解析する関数(メソッド)は以下のようになります.

bool read_header(istream_type& in) {
    size_type sig = 0;
    if (!clx::get(in, sig, clx::endian::big)) return false;
    if (sig == clx::combine4_<'t', 't', 'c', 'f'>::value) return false; // *.ttc
    
    unsigned short count = 0;
    if (!clx::get(in, count, clx::endian::big)) return false;
    if (!this->check_version(sig, count)) return false;
    in.seekg(6, std::ios_base::cur); // sr, es, rs
    
    for (size_type i = 0; i < count; ++i) {
        size_type tag = 0, checksum = 0, offset = 0, length = 0;
        if (!clx::get(in, tag, clx::endian::big)) return false;
        if (!clx::get(in, checksum, clx::endian::big)) return false;
        if (!clx::get(in, offset, clx::endian::big)) return false;
        if (!clx::get(in, length, clx::endian::big)) return false;
        // pos_ は,std::map<size_type, std::pair<size_type, size_type> >
        pos_[tag] = std::make_pair(offset, length);
    }
    
    return true;
}

name ブロックの解析

各種(ASCII, Japanese, ...)フォント名は name ブロックで管理されています.なので,今回は,この name ブロックのみを解析して終わります.name ブロックへは,識別名が 0x6E616D65 ('n', 'a', 'm', 'e') のオフセット値を利用してアクセスします.

for (std::map<size_type, pos_type>::iterator it = pos_.begin(); it != pos_.end(); ++it) {
    // ToDo: 必要なブロックに関して,処理するためのコードを追加していく.
    switch (it->first) {
    case clx::combine4_<'n', 'a', 'm', 'e'>::value:
        status &= names_.read(in, it->second);
        break;
    default:
        break;
    }
}

name ブロックでは,「platform, encoding, language, id」の 4 つの値で各情報が管理されています.このうち name ブロックの各情報の内容は language と id の組み合わせで識別する事ができます.例えば,language == 0, id == 4 の場合は ASCII でのフォントのファミリ名を表し,language == 0, id == 5 の場合は ASCII でのバージョン名を表します.尚,日本語は language == 1041 のようです.

platform と encoding は,以下のような値を取ります.

MicrosoftのTTFの仕様書ではプラットフォームは以下のように定義されています。

IDPlatformSpecific encoding
0Apple Unicodenone
1MacintoshScript manager code
2ISOISO encoding
3MicrosoftMicrosoft encoding

文字コードは以下のように定義されています。

Encoding IDDescription説明
0Symbol記号
1Unicode世界標準となりつつある
2ShiftJIS主に日本
3Big5台湾
4PRC
5Wansung中国
6Johab韓国
TrueTypeフォントのフォーマットを調べる その8 - Webと文字

encodign == 0 (Symbol) は,ASCII と言う意味でしょうか.ただ,platform == 3 (Microsoft) の場合,encoding の値に関係なくエンコードUTF-16 になっているようです.違うかも?自信がないので保留.platform と encoding の関係は今後の要調査項目です.

今回は,language 毎に一つのクラスにまとめる事にします.

namespace cliff {
    class name_type {
    public:
        typedef unsigned short value_type;
        
        const value_type& platform() const;
        const value_type& encoding() const;
        const string_type& copyright() const;
        const string_type& familyname() const;
        const string_type& fullname() const;
        const string_type& psname() const; // PostScript name
        const string_type& version() const;
        ...
    };
    
    class name_block : public clx::map_wrapper<size_type, name_type> {
    public:
        name_block();
        virtual ~name_block() throw();
        bool read(istream_type& in, const std::pair<size_type, size_type>& pos);
    };
}

name_type クラスに格納される文字列のエンコードは platform と encoding の値によって判断する事になります.ただし,name_block クラスでは,CLIFF_USE_BABEL を定義するとそれらの値に関わらず文字列を全て UTF-8 で格納するようにしています.尚,文字型はエンコードに関わらず(UTF-16 の場合でも)常に char (文字列クラスは,std::string)を使用しています.

TrueType Collection の解析

TrueType フォント・ファイルには,*.ttf の他に *.ttc と言う拡張子のファイルが存在しています.*.ttc には,複数の TrueType フォントの情報が格納されており,それぞれの TrueType フォントに関する情報が格納されている場所へのオフセット値が,*.ttc ファイルの先頭に記述されています.

namespace cliff {
    class true_type_collection : public clx::vector_wrapper<true_type> {
    public:
        true_type_collection();
        explicit true_type_collection(istream_type& in);
        explicit true_type_collection(const string_type& path);
        virtual ~true_type_collection() throw();
        
        bool read(istream_type& in);
        bool read(const string_type& path);
    };
}

サンプルコード

以下がサンプル・コードです.

#include <iostream>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <clx/hexdump.h>
#include <clx/ini.h>
#include <cliff/font/true_type.h>
#include <cliff/font/true_type_collection.h>

#define FONT_DIR "C:\\Windows\\Fonts"

template <class MapT>
void put_element(const std::string& path, MapT& v, clx::ini& dest, size_t index = 0) {
    if (dest.find("General") == dest.end()) dest.insert("General");
    for (typename MapT::iterator pos = v.begin(); pos != v.end(); ++pos) {
        if (pos->second.fullname().empty()) continue;
        
        if (pos->first == 0) {
            std::stringstream ss;
            ss << path << ":" << index;
            dest["General"][pos->second.fullname()] = ss.str();
        }
        else if (pos->first == 1041) { // 日本語のフォント名
            if (dest.find("Japanese") == dest.end()) dest.insert("Japanese");
            dest["Japanese"][pos->second.fullname()] = v[0].fullname();
        }
    }
}

namespace fs = boost::filesystem;

int main(int argc, char* argv[]) {
    clx::ini dest;
    
    fs::path sdir = (argc > 1) ? std::string(argv[1]) : std::string(FONT_DIR);
    fs::directory_iterator pos(sdir);
    fs::directory_iterator last;
    for(; pos != last; ++pos) {
        fs::path pabs = fs::complete(pos->path());
        fs::ifstream ifs(pabs, std::ios::binary);
        std::string ext = fs::extension(pabs);
        clx::downcase(ext);
        if (ext == ".ttf") {
            cliff::true_type f;
            if (f.read(ifs)) put_element(pabs.string(), f.names(), dest);
        }
        else if (ext == ".ttc") {
            cliff::true_type_collection f;
            if (f.read(ifs)) {
                for (size_t i = 0; i < f.size(); ++i) {
                    put_element(pabs.string(), f.at(i).names(), dest, i);
                }
            }
        }
    }
    
    dest.write("fontmap.ini");
    
    return 0;
}

コンパイル方法は,cliff_20100108.zip および CLX C++ Libraries をダウンロードして解凍した後,それらのディレクトリ (cliff, clx) への直上の階層をインクルードパスに指定します.また,サンプル・コードでは Boost.Filesystem を利用しているので boost がインストールされている必要があります(参考: Boost.Filesystem - Life like a clown).

尚,CLIFF_USE_BABEL を定義する際には,それに加えて,文字コード変換ライブラリである バベル - extra - C++ - TrickLibrary をダウンロードして解凍,コンパイル (e.g., g++ -c babel.cpp) を行います.

例えば,サンプルコードのコンパイル方法は以下のようになります(cliff, clx, babel, work ディレクトリが全て同じ階層に存在する場合).

[clown@stinger cliff_example]$ g++ -I.. -Wall -DCLIFF_USE_BABEL
example_true_type.cpp ../babel/babel.o -lboost_system -lboost_filesystem

今回は,結果を ini ファイルとして格納しています.私の環境で試した結果は以下のようになります.

[General]
Aharoni Bold=C:/Windows/Fonts/ahronbd.ttf:0
Andalus=C:/Windows/Fonts/andlso.ttf:0
Angsana New=C:/Windows/Fonts/angsa.ttf:0
Angsana New Bold=C:/Windows/Fonts/angsab.ttf:0
Angsana New Bold Italic=C:/Windows/Fonts/angsaz.ttf:0
Angsana New Italic=C:/Windows/Fonts/angsai.ttf:0

...

[Japanese]
HGP創英角ゴシックUB=HGPSoeiKakugothicUB
HGP創英角ポップ体=HGPSoeiKakupoptai
HGP創英プレゼンスEB=HGPSoeiPresenceEB
HGP教科書体=HGPKyokashotai

...

ファイル名の“:0”は,*.ttc の場合にそのフォント名が *.ttc ファイル中の何番目に出現するかを識別するために使用します.そのため,*.ttf ファイルの場合は一つのフォントしか格納できないので常にゼロになります.

References