PDFからテキストを取り出すために苦労を重ねているが、前回まででエンコーディング情報を取り出すことができた。今回は、この情報を考慮したテキストの変換を説明しよう。

グリフからUnicodeへのマップ

さて、エンコーディングが分かったところで、日本語テキスト取り出すことに挑戦する訳だが、そのためには絶対に必要な機能がある。それは、グリフの値をUnicodeに変換する機能だ。

これを実現するには、この変換を行うマップがあればいい。このマップは、Adobe-Japan1の仕様書を見れば理屈としては作ることができるのだが、手作業で作るのは控えめに言っても大変な作業だ。

どうにかしてプログラムで作れないかを考えてみよう。まず、Unicodeからグリフへのマップは比較的簡単に作ることができるはずだ。なぜなら、このマップはフォントファイルに含まれているからだ。いま必要になっているのは、この逆引きを行うためのマップである。それならば、すべてのUnicodeの値に対するグリフの値を取得して、それをひっくり返す事で逆引きマップを作成してやればいいのではないだろうか。

そのようなアイディアをもとにiOSのAPIを眺めていると、ちょうどいい関数が見つかる。Core Textフレームワークに含まれるCTFontGetGlyphsForCharactersだ。この関数は、あるフォントに対してUnicodeの配列を与えると、グリフの配列を得ることができるものだ。これを使ってみよう。

グリフからUnicodeを得るための、unicharWithGlyphという関数を作ってみた。引数としてグリフの値をとり、返り値はUnicodeの値になるものだ。次のようなソースコードになる。

List 1.

unichar unicharWithGlyph(
        CGGlyph glyph)
{
    int i;

    // グリフからUnicodeへのマップの作成
    static CGGlyph  _glyphs[65535];
    static BOOL     _initialized = NO;
    if (!_initialized) {
        // Unicodeテーブルの初期化
        UniChar unichars[65535];
        for (i = 0; i < 65535; i++) {
            unichars[i] = i;
        }

        // CTFontの作成
        CTFontRef   ctFont;
        ctFont = CTFontCreateWithName((CFStringRef)@"HiraKakuProN-W3", 10.0f, NULL);

        // Unicodeからグリフの取得
        CTFontGetGlyphsForCharacters(ctFont, unichars, _glyphs, 65535);

        // 初期化済みフラグの設定
        _initialized = YES;
    }

    // マップの検索
    for (i = 0; i < 65535; i++) {
        // 指定されたグリフが見つかった場合、そのインデクッスがunicodeとなっている
        if (_glyphs[i] == glyph) {
            return i;
        }
    }

    return 0;
}

まず、Unicodeのテーブルというものを作る。これは単純に、0から65534の値が入っている配列だ。次に、CTFontオブジェクトを作っている。ここでは、ヒラギノ角ゴシックのCTFontを作成している。このフォントが、Adobe-Japan1に準拠しているためだ。そして、CTFontGetGlyphsForCharacters関数を呼び出す。この呼び出しで、すべてのUnicodeに対するグリフの値が得られるのだ。このグリフの配列を使って、Unicodeを探してやる。単純な値の比較で見つかるはずだ。

このソースコードでは毎回ループを回しているので、あまり効率はよくない。キャッシュしたり、辞書構造にすることでスピードは上がるだろう。

このようにして作ったグリフからUnicodeへのマップは、ある程度は役に立つ。だが、完璧ではない。なぜなら、Unicodeとグリフのマップは1対1ではないからだ。1つのUnicodeに対して複数のグリフが割り当たっている場合があるので、そのようなグリフは正確に引けないことになる。だが、とりあえずのマップとしては使えるはずだ。

エンコーディングに基づくテキストの変換

では、このunicharWithGlyph関数を使って、PDFドキュメントから日本語テキストを取り出してみよう。

ここまでのサンプルで、文字列を取り出すために用意したメソッドは、PDFViewControllerのstringInPDFObject:だった。このメソッドを、取得したエンコーディングに応じて文字列を取り出すように、拡張する事にしよう。

次のようなソースコードになる。

List 2.

- (NSString*)stringInPDFObject:(CGPDFObjectRef)object
{
    bool    result;

    // PDFオブジェクトタイプの取得
    CGPDFObjectType type;
    type = CGPDFObjectGetType(object);

    // タイプ別による処理
    switch (type) {
    // PDF文字列の場合
    case kCGPDFObjectTypeString: {
        // PDF文字列の取得
        CGPDFStringRef  string;
        result = CGPDFObjectGetValue(object, type, &string);
        if (!result) {
            return nil;
        }

        // MacRomanEcodingの場合
        if ([_encoding isEqualToString:@"MacRomanEncoding"]) {
            // CGPDFStringからNSStringへの変換
            NSString*   nsstring;
            nsstring = (NSString*)CGPDFStringCopyTextString(string);
            [nsstring autorelease];

            return nsstring;
        }

        // Identity-Hの場合
        if ([_encoding isEqualToString:@"Identity-H"]) {
            // バッファの作成
            NSMutableString*    buffer;
            buffer = [NSMutableString string];

            // バイトのポインタを取得
            const unsigned char*    tmp;
            tmp = CGPDFStringGetBytePtr(string);

            // NSStringへの変換
            int i;
            for (i = 0; i < CGPDFStringGetLength(string) / 2; i++) {
                // CIDを取得
                uint16_t    cid;
                cid = *tmp++ << 8;
                cid |= *tmp++;

                // CIDをunicharへ変換
                unichar c;
                c = unicharWithGlyph(cid);
                if (c == 0) {
                    break;
                }

                // NSStringへ変換して追加
                NSString*   nsstring;
                nsstring = [NSString stringWithCharacters:&c length:1];
                if (nsstring) {
                    [buffer appendString:nsstring];
                }
            }

            return buffer;
        }
    }

    ...

エンコーディングがMacRomanEncodingの場合は、今まで通りだ。Identity-Hの場合が問題になる。この場合は、CGPDFStringとして取得されるデータが、Adobe-Japane1になっている。そこで、CGPDFStringGetBytePtrという関数を使い、文字列データに直接バイト列としてアクセスするのだ。

そして先頭から順に2バイトずつデータを取り出す。これが、CIDになっているはずだ。この値をunicharWithGlyph関数を使って、Unicodeに変換してやる。これで、日本語文字列として認識できるようになるのだ。

このソースコードを組み込んだ実行結果は、次の図のようになる。無事に日本語に変換できている事が分かるだろう。また、英数字もうまく変換できている。

この出力結果を見ると、文が途中でブツブツと切れていることが分かる。これは、PDFには文や段落といった構造的な情報は含まれず、単にグリフと画面の座標という幾何学的な情報しかないからだ。そのため、PDFから再利用可能なテキスト情報は、あまりきれいな形では取得できない。これはPDFの出自が、電子媒体ではなく紙媒体である宿命なのだろう。

ここまでのソースコード: PDF-5.zip