C++でCoreText

NodeモジュールでまずはOSのフォント情報を取ろうと思います。Objective-CでやればサンプルもたくさんあるのだけどここはC++で書きたかった。

#include <ApplicationServices/ApplicationServices.h>
#include <nan.h>
using namespace v8;

ApplicationServices/ApplicationServices.hを取り込みます。この中にCoreFoundationやCoreTextやCoreGraphics等のOSXの中で特にC++で書けるライブラリのヘッダだけがまとまってます。他はObjective-CかSwiftじゃないと書かせてもらえないみたい。文字列はNSStringではなくCFString。

char* CFStringToUtf8String(CFStringRef str) {
    if (str == NULL) {
        return NULL;
    }
    CFIndex max = CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1;
    char* buffer = (char*)malloc(max);
    if (CFStringGetCString(str, buffer, max, kCFStringEncodingUTF8)) {
        return buffer;
    }
    free(buffer);
    return NULL;
}

char* getStringAttribute(CTFontDescriptorRef ref, CFStringRef attr) {
    return CFStringToUtf8String((CFStringRef)CTFontDescriptorCopyAttribute(ref, attr));
}

char* getLocalizedAttribute(CTFontDescriptorRef ref, CFStringRef attr, CFStringRef *localized) {
    CFStringRef value = (CFStringRef)CTFontDescriptorCopyLocalizedAttribute(ref, attr, localized);
    if (value) {
        return CFStringToUtf8String(value);
    }
    return getStringAttribute(ref, attr);
}

逆算的な説明ですが、とにかくCoreFoundationの文字列であるCFStringから、char*へ変換するのは上記の通り。この用意を踏まえて、フォントDescriptorをいじっていきます。キモはCFStringGetCStringで、CoreFoundationの提供する世界から外界たるC++へ値を取り出すのは他のAPIでもおよそこんな感じ。戻りがBoolで成功失敗で値はバッファをポインタ渡しで受け取ります。APIはだいたい汎用な作りになっていて細かにフラグを設定することが多いです。ここではエンコーディング種別がフラグになってます。

CTFontDescriptorCopyLocalizedAttributeでは可能な場合にログインロケールな文字列を戻します。すなわち日本語をメイン言語として利用している私は「ja」でロケールが探され、得られれば第3引数のポインタにjaを返します。多分このログイン言語環境を動的に変更できるAPIもあるんだろうけど、調べてません。とりあえず今の私は不要。

int getFontWeight(CTFontDescriptorRef ref) {
    CFDictionaryRef traits = (CFDictionaryRef)CTFontDescriptorCopyAttribute(ref, kCTFontTraitsAttribute);
    CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(traits, kCTFontWeightTrait);
    CFRelease(traits);
    float weight = 0.0f;
    if (CFNumberGetValue(value, kCFNumberFloat32Type, &weight)) {
        if (weight <= -0.8f) {
            return 100;
        } else if (weight <= -0.6f) {
            return 200;
        } else if (weight <= -0.4f) {
            return 300;
        } else if (weight <= 0.0f) {
            return 400;
        } else if (weight <= 0.25f) {
            return 500;
        } else if (weight <= 0.35f) {
            return 600;
        } else if (weight <= 0.4f) {
            return 700;
        } else if (weight <= 0.6f) {
            return 800;
        }
        return 900;
    }
    return 0;
}

char* getSrc(CTFontDescriptorRef ref) {
    CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(ref, kCTFontURLAttribute);
    return CFStringToUtf8String((CFStringRef)CFURLCopyPath(url));
}

Local<Object> toJSObject(CTFontDescriptorRef ref) {
    Nan::EscapableHandleScope scope;
    Local<Object> res = Nan::New<Object>();
    CFStringRef localized = NULL;
    res->Set(
        Nan::New<String>("font-family").ToLocalChecked(),
        Nan::New<String>(getLocalizedAttribute(ref, kCTFontFamilyNameAttribute, &localized)).ToLocalChecked()
    );
    res->Set(
        Nan::New<String>("font-style").ToLocalChecked(),
        Nan::New<String>(getStringAttribute(ref, kCTFontStyleNameAttribute)).ToLocalChecked()
    );
    res->Set(
        Nan::New<String>("font-weight").ToLocalChecked(),
        Nan::New<Number>(getFontWeight(ref))
    );
    res->Set(
        Nan::New<String>("src").ToLocalChecked(),
        Nan::New<String>(getSrc(ref)).ToLocalChecked()
    );
    if (localized) {
        res->Set(
            Nan::New<String>("localized").ToLocalChecked(),
            Nan::New<String>(CFStringToUtf8String(localized)).ToLocalChecked()
        );
    }
    return scope.Escape(res);
}

JSの世界へ持って来るために、Local<v8::Object>へ変換するのですが、いちいち面倒ですね。〜Refというのは全てポインタ型ですが、API毎に細かく定義されているのでCoreTextやCoreFoundationのヘッダファイルを見てあたりつけて、AppleのDeveloperサイトを検索するというのを繰り返す。。。同じCTFontDescriptorCopyAttributeでも、引数のフラグに応じて戻るポインタ型が違うので組み合わせの正解探しが続きましたが、ヘッダをよく見たらフラグのコメントに戻りの型がちゃんと書いてあった。

フォントWeightの変換ロジックは、font-managerからもらってきました。このfont-managerがメンテナンス止まってたので今回のコードを書いてます。ちょっと直すだけで使えたのだけど、繰り返しますが自分で書いてみたかったから。font-managerでのOSX版はObjective-Cを用いてます。

NAN_METHOD(getAllLocalFonts) {
    CTFontCollectionRef collection = CTFontCollectionCreateFromAvailableFonts(NULL);
    CFArrayRef results = CTFontCollectionCreateMatchingFontDescriptors(collection);
    CFRelease(collection);
    if (results) {
        CFIndex i, count = CFArrayGetCount(results);
        Local<Array> res = Nan::New<Array>(count);
        for (i = 0; i < count; i++) {
            res->Set(i, toJSObject((CTFontDescriptorRef)CFArrayGetValueAtIndex(results, i)));
        }
        CFRelease(results);
        info.GetReturnValue().Set(res);
    } else {
        info.GetReturnValue().Set(Nan::New<Array>(0));
    }
}

NAN_MODULE_INIT(Init) {
    Nan::Export(target, "getAllLocalFonts", getAllLocalFonts);
}

NODE_MODULE(fontFinder, Init)

JSからアクセスされる関数はこちら。C++というよりボキャブラリを問われる書き方が過ぎて、新種のDSLみたいな。。。先日はAsyncの作りを調べてましたが、結局ローカルのAPIを叩くのは完全にSyncで十分だった。500以上のフォントが入っていてもすぐに値が返って来ます。