前回に引き続き、PDFデータへのアクセス方法を説明していこう。CGPDFContentStreamから、各種データを取り出していく。そのためには、まずPDFのオペレータについて解説しなくてはいけない。そこから始めよう。

オペレータとは

前回紹介したように、PDFのコンテンツデータはストリーム型と呼ばれる。これは、コンテンツの中にデータとその処理方法を示す記号とが混在しており、先頭から順に読み込みながら処理していく事を示している。この「処理方法を示す記号」のことを、「オペレータ」と呼ぶ。

PDFフォーマットでは、たくさんの種類のオペレータが定義されている。これらは、PDF仕様書の「Annex A. Operator Summary」でその一覧を確認する事ができる。

このオペレータの一覧を見ていくと、その中に「Tj」および「TJ」というオペレータがある。説明文を見ると、テキストを表示するためのオペレータ、とある。つまり、このオペレータの前に、テキストデータがあるのだ。

前回紹介したVoyeurでコンテンツデータを表示し、TjおよびTJオペレータを探してみよう。すると、次のようなデータを見つける事ができる。

「[ (iPh) 1 (o) 1 (n) 1 (e) ] TJ」という文字列がある。一番最後にあるのが、「TJ」というオペレータだ。その前にある文字列をよくみると、「iPhone」という文字が点在している事が分かるだろう。つまり、これは「iPhoneというテキストデータがある」ことを表しているのだ。

これが、PDFでのテキストデータの格納場所だ。これを抜き出す事ができれば、テキストを取得する事ができる。

オペレータテーブルの作成とコールバックの登録

iOSでは、コンテンツデータをスキャンし、オペレータを見つけ出すためのAPIが用意されている。これの使い方を説明しよう。

使用する際の基本的な考え方は、スキャンとコールバックだ。iOSは、コンテンツストリームを先頭からスキャンしてくれる。そして、指定したオペレータを見つけると、あらかじめ登録しておいたコールバック関数を呼び出してくれる。これをトリガーとして、そこまでスキャンしたオブジェクトを取り出すのだ。

まずは、コールバックを登録しよう。これには、PDFオペレータテーブルと呼ばれるテーブルを使う。このテーブルに、オペレータとコールバックのペアを登録する。そして、これとストリームを組み合わせて、PDFスキャナを作るのだ。

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

List 1.

    // PDFオペレータテーブルを作成
    CGPDFOperatorTableRef   table;
    table = CGPDFOperatorTableCreate();
    CGPDFOperatorTableSetCallback(table, "TJ", operator_Text);
    CGPDFOperatorTableSetCallback(table, "Tj", operator_Text);

    // PDFスキャナを作成
    CGPDFScannerRef scanner;
    scanner = CGPDFScannerCreate(_stream, table, self);

    // スキャンを開始
    [_text release], _text = nil;
    _text = [[NSMutableString string] retain];
    CGPDFScannerScan(scanner);

PDFオペレータテーブルは、CGPDFOperatorTableCreateという関数で作る。オペレータとコールバックを登録するのは、CGPDFOperatorTableSetCallbackだ。ここでは、「TJ」と「Tj」というオペレータに対して、operator_Textというコールバックを登録している。

続いて、PDFスキャナの作成だ。これには、CGPDFScannerCreateを使う。第一引き数にはコンテンツストリーム、第二引き数にはオペレータテーブルを指定する。第三引き数は、コールバックが呼び出されるときに引き数として付加される値になる。ここでは、selfを指定しておく。

最後にスキャンを開始する。CGPDFScannerScan関数だ。これで、ストリームをスキャンし、登録したコールバックが順次呼び出されていく事になる。

テキストの抽出と日本語での問題

コールバックでは、スキャンされたデータを取り出す事になる。その処理を紹介しよう。

コールバックとして登録できるのは、C関数である。先ほどはoperator_Textという関数を登録した。だが、C関数の中では何かと処理がやりにくい。そこで、この関数の中ではすぐに、PDFViewControllerのメソッドを呼び出すようにしている。インスタンスは、コールバックの引き数として取得する事ができる。

List 2.

void operator_Text(
        CGPDFScannerRef scanner,
        void* info)
{
    [(PDFViewController*)info operatorTextScanned:scanner];
}

operatorTextScanned:メソッドの中でデータを取得していこう。CGPDFScannerによってスキャンされたデータは、CGPDFObjectという形で取り出す事ができるのだ。CGPDFObjectはすべてのデータのジェネリックな表示形態であり、そのタイプを調査する事で、実際の型と値を取得する事ができる。タイプとしては、以下のものが用意されている。

  • null
  • 論理値
  • 整数値
  • 実数値
  • 名前
  • 文字列
  • 配列
  • 辞書
  • ストリーム

では、テキストを取り出す手順を順番に紹介していこう。まず、operatorTextScanned:では、スキャンしたCGPDFObjectを取り出している。使う関数は、CGPDFScannerPopObjectだ。そして、そのオブジェクトからテキストを抽出するために、stringInPDFObject:というメソッドを呼んでいる。

List 3.

- (void)operatorTextScanned:(CGPDFScannerRef)scanner
{
    // PDFオブジェクトの取得
    CGPDFObjectRef  object;
    CGPDFScannerPopObject(scanner, &object);

    // テキストの抽出
    NSString*   string;
    string = [self stringInPDFObject:object];

    // テキストの追加
    if (string) {
        [_text appendString:string];
        [_text appendString:@"\n"];
    }
}

stringInPDFObject:メソッドでは、PDFオブジェクトのタイプを調査する。CGPDFObjectGetType関数を使う。TjまたはTJオペレータでは、文字列タイプまたは配列タイプのオブジェクトが取得できるはずだ。

文字列タイプの場合は、CGPDFStringからNSStringに変換する。これには、CGPDFStringCopyTextString関数を使う。配列タイプの場合は、含まれるオブジェクトを順次取り出し、再びstringInPDFObject:メソッドを呼び出すようにする。

List 4.

- (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;
        }

        // CGPDFStringからNSStringへの変換
        NSString*   nsstring;
        nsstring = (NSString*)CGPDFStringCopyTextString(string);
        [nsstring autorelease];

        return nsstring;
    }

    // PDF配列の場合
    case kCGPDFObjectTypeArray: {
        // PDF配列の取得
        CGPDFArrayRef   array;
        result = CGPDFObjectGetValue(object, type, &array);
        if (!result) {
            return nil;
        }

        // バッファの作成
        NSMutableString*    buffer;
        buffer = [NSMutableString string];

        size_t  count;
        count = CGPDFArrayGetCount(array);

        // PDF配列の中身の取得
        int i;
        for (i = 0; i < count; i++) {
            // PDF配列からオブジェクトを取得
            CGPDFObjectRef  child;
            CGPDFArrayGetObject(array, i, &child);

            // テキストの抽出
            NSString*   nsstring;
            nsstring = [self stringInPDFObject:child];
            if (nsstring) {
                [buffer appendString:nsstring];
            }
        }

        return buffer;
    }
    }

    return nil;
}

これでテキストデータが抽出できる、はずだ。だが実際に動作させてみると、取り出した文字列は次のようになっている。

見ての通り、文字化けしている。だがよく見ると、「iPhone」や「AppStore」といった単語は確認できる。つまり、英単語は無事にテキストとして取り出す事に成功しており、日本語は化けてしまっている状態だ。これは、PDFドキュメント内部では、テキストは様々なエンコーディングの元にデータ化されているためである。つまりiOSのAPIでは、テキストエンコーディングのケアまではしてれくないのだ。

次回は、日本語を含む、PDFドキュメントでのテキストエンコーディングについて説明しよう。

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