Tech Notes

macOSでシステムのフォントファイルを読み込む方法(CoreText)

libfontlist」というC++向けのライブラリを公開した。これはシステムに登録されたフォント情報をクロスプラットフォームで読み込めるライブラリだ。無かったので作った。

libfontlistはWindows、Linux、macOSの主要3プラットフォームに対応している。それぞれ頑張って個別に対応したに過ぎないのだが、macOS対応が最も情報が少なく苦労した。そういう訳でこの記事ではその技術的な詳細についてまとめる。

CoreText」というAPIを利用したのだが、公式ドキュメント以上の情報がほぼ無かった。

CoreTextについて

CoreTextはAppleが提供する、テキスト表示処理のための低レイヤなAPIだ。今回はあくまでフォント情報の読み込み機能だけを使っているが、テキストのレイアウト・描画といった高次の機能も備えた複合的なAPIだ。

なお、CoreTextはmacOSのほかiOSなどにも対応している。AppleはUSBの代わりにThunderBoltを付けたり、Vulkanの代わりにMetalを出したことに代表されるように、基本的にオープンスタンダードとの付き合いは悪い。その一方でそれはそうと、自社製品向けのAPIはある程度プラットフォーム間で統一しているようだ。

利用するオブジェクト

フォント情報を取りたいだけなら、主に使うのはFont DescriptorFont Collectionの2つだけになる。

Font Descriptor

これは1つのフォントに関する様々な情報を記録したオブジェクトだ。型はCTFontDescriptorRefで表される。

それぞれの情報はAttributeと呼ばれ、それはフォント名であったり、フォントファイルのパスであったり、フォントの太さとかスタイルの情報であったりする。あるフォントのFont Descriptorが手に入ればそれらの情報を取得できる。

Font Collection

これは複数のFont Descriptorをまとめたオブジェクトだ。型はCTFontCollectionで表される。

Core Foundationについて

C/C++からCoreText APIを扱うには、Core Foundationについて十分に知る必要がある。

まずFoundationフレームワークというものがあり、これはApple系OSにおけるアプリ開発の基礎部分で多大な役割を負っている。具体的にはデータ管理の仕組みのほとんどがFoundationフレームワークの上に成り立っていて、文字列ひとつ配列ひとつすら基本的にはFoundationフレームワークの上に成り立っている。

Core Text APIで手に入れたオブジェクトも同様である。具体的な文字列などのデータはFoundationフレームワークの上で管理されているものだから、それに乗っからなければ何一つデータに触れないのである。

で、このFoundationフレームワークは、基本的にSwiftやObjective-Cからしか触れるように出来ていない。これは大変によろしくない。

しかしさしものAppleも正気が残っていたのか、C言語からも触れる窓口が整備されていて、それがCore Foundationである。Core Foundationを利用することでFoundationフレームワークの上に構築された様々なデータにCからアクセスすることができる。

Core Foundationオブジェクトから値を取り出す

Foundationフレームワークの上に構築されたAPIは、文字列をCFStringRefという型で返してくる。C/C++erとしては生C文字列が欲しい。Core Foundationを使うと例えばこのように取得できる。

// CFStringRef str;

CFIndex len = CFStringGetLength(str);
size_t cap = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8);
char* buf = new char[cap];
CFStringGetCString(str, buf, cap, kCFStringEncodingUTF8);

(例なので生ポインタとnewを使っているが、メモリ安全性的には適当なスマートポインタかstd::stringなどを使うべきだ。またエラー処理もする必要がある。)


数値はCFNumberRefで表される。これも頑張って取り出す必要がある。

// CFNumberRef num;

int32_t value;
CFNumberGetValue(num, kCFNumberSInt32Type, &value);

数値には型があるので、それも明確化しておく必要がある。CFNumberGetValueの第2引数に指定する定数の一覧はここ: CFNumberType


配列はCFArrayで表される。

要素数はこのように取る。

// CFArray arr;

size_t count = CFArrayGetCount(arr);

個別の何番目といった要素にアクセスするにはこうする。

// CFArray arr;
// int index;

CFArrayGetValueAtIndex(arr, index);

CFArrayGetValueAtIndexの戻り値は適切に何らかの型にキャストしてやらなければならない。


とまあ、このような感じでCore Foundationのオブジェクトと生Cオブジェクトとの変換を適宜行う必要がある。

他にも連想配列を表すCFDictionaryRef、階層構造を表すCFTreeRef、UUIDを表すCFUUIDRef、パスを表すCFURLRef...などなど色々あるようなのだが、この記事の主目的はCoreTextを解説することなので、Core Foundationに関する詳しい解説は機会があれば別の記事で書くことにする。

フォント情報取得の流れ

ようやく具体的なCoreTextによる情報取得のコードに入る。

まずCTFontCollectionCreateFromAvailableFontsを呼ぶと、システムに登録された全てのフォント情報がFont Collectionの形で取得できる

CTFontCollectionRef fontcollection = CTFontCollectionCreateFromAvailableFonts(nullptr);

そしてCTFontCollectionCreateMatchingFontDescriptorsを呼ぶことで、その中にあるFont Descriptorの配列(CFArray型)を取得できる

CFArray型なので、上で書いたようにCFArrayGetValueAtIndexなどを使って頑張って各要素を取得する。

CFArray fontdescriptors = CTFontCollectionCreateMatchingFontDescriptors(fontcollection);

// 配列サイズを取得
size_t count = CFArrayGetCount(fontdescriptors);
for(int i = 0; i < count; i++) {
    // 各Font Descriptorを取得
    CTFontDescriptorRef descriptor =
        static_cast<CTFontDescriptorRef>(CFArrayGetValueAtIndex(fontdescriptors, i));
}

最後にCTFontDescriptorCopyAttributeを使うことで、Font Descriptorの各Attributeを取得する。第2引数にはAttributeを表す定数を指定する。取り出せるAttributeの種類と対応する定数はここにまとまっている: Font Attributes

戻り値は適宜キャストして使用する。

// ファイルパスを取得
CFURLRef path = (CFURLRef)CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);

// フォントファミリ名を取得
CFStringRef fontFamilyName = (CFStringRef)CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute);

// フォント特性情報(後述)を取得
CFDictionaryRef fonttrait = (CFDictionaryRef)CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);

// ...

大体のAttributeはまあCore Foundationで普通に取り出せばいいのだが、少し特筆すべきはフォント特性情報kCTFontTraitsAttributeの取得である。

フォント特性情報

このAttributeだけはCFDictionaryRefになっていて、つまり連想配列の形式で複数の値がまとまっている。CFDictionaryGetValueを用いて値を取り出すことになる。

// 例: 太さ情報の取り出し
CFNumberRef weight = (CFNumberRef)CFDictionaryGetValue(fonttrait, kCTFontWeightTrait);

取り出せる値と対応するキーはこちらのドキュメントにまとまっている: Font Traits

  • kCTFontWeightTrait: 太さ
    • -1.0+1.0のfloat型の値。CSSなどにおける太さ(100~900)と上手く対応が付かないのが難点。
  • kCTFontWidthTrait: 幅
    • -1.0+1.0のfloat型の値。0.0が最も普通の幅を表す。
  • kCTFontSlantTrait: (斜体フォントなどにおける)傾き
    • -1.0+1.0のfloat型の値。0.0が傾きなし、+1.0が時計回り30度の傾きを表すとドキュメントに書いてある。
  • kCTFontSymbolicTrait: 属性を表すビットマスク
    • 整数値。「斜体」「太字」などの2値情報がビットパターンの形で表されている。
    • 対応するビットマスクの定数はこちらのドキュメントにまとめられている: CTFontSymbolicTraits

おわりに

この記事はあくまでフォント取得について調べた時の知見をまとめたものなので、CoreTextの機能の一部しか利用していない。Apple側としてはもっと広い範囲(文字列のレイアウトなど)をCoreTextで実現することを想定していると思われるが、その辺については調べていないのでご容赦頂きたい。

参考資料

コメント