今回からは、PDFドキュメントの内部データにアクセスする方法を説明しよう。まず最初のターゲットは、テキストを取り出す事だ。テキストにアクセスできれば、検索などの処理が可能になる。

だが現状のiOSでは、PDFデータにアクセスするAPIが用意されてはいるものの、何らかの有意なデータを取り出すにはPDFフォーマット自体の理解が不可欠となる。そこでまずは、簡単にPDFフォーマットについて説明しよう。

PDFフォーマットとは

PDF (Portable Document Format)は、アドビシステムズが開発した電子文書のためのフォーマットだ。1993年に登場しており、現在の最新バージョンは1.7となっている。2008年にISO 32000-1として標準化された。フォーマットの仕様書は公開されており、アドビシステムズのWebサイトからダウンロードすることができる

PDFの特徴として、異なるデバイスであってもオリジナルのレイアウトを再現できる事、を挙げる事ができる。これは技術的には、PDFは印刷用のページ記述言語であるPostScriptをベースにしているためであり、印刷物をそのまま電子化したものと捉えることが出来る。

そのためページを紙として捉えたときの再現性は高いが、電子情報として考えると再利用性は低いとも言える。たとえば、様々なデバイスで表示させるときに、そのスクリーンに応じて最適な表示に変更する事ができない。また、これは本連載で詳しく見ていくのだが、内部のテキストデータなどにアクセスするのが容易ではないし、セマンティックな情報も抜け落ちている。

このように考えると、PDFフォーマットは、柔軟なレイアウトや検索の容易性に重点を置いた、HTMLなどとは対極にあるフォーマットとも言えるだろう。

VoyeurによるPDFフォーマットの概観

PDFフォーマットの全容を理解するには、さきほど紹介した仕様書を読み下していけばいいのだが、これはあまりにも規模が大きくて複雑だ。そこで、もう少しプラクティカルなアプローチをしたい。まずはPDFの構造を体感してみよう。そしてその中から、必要な情報だけを抜き出していくのだ。

そのために使うのが、「Voyeur」というツールだ。これは、もともとMac OS XのSDKにサンプルとして付属していたものであり、Mac OS Xが持つPDFのためのAPIを使って、PDFドキュメントのデータを表示するものだ。いまのSDKには残念ながら含まれていないが、GitHubの方にソースコードがアップロードされているので、そちらから手に入れる事が可能だ

上の画面が、VoyeurによるPDFのデータ構造の表示だ。このように、木構造として把握する事ができる。

この木構造のルートを見てみよう。「/Type」「/Pages」「/Version」という項目が並んでいる。PDFは辞書型のオブジェクト構造を持ち、これらはそのキーとなっている。「/Type」というキーには「/Catalog」、「/Version」というキーには「/1.4」という値が関連付けられている。そして、「/Pages」というキーにはさらなる辞書オブジェクトが関連付けられており、ここにPDFドキュメントのページ情報があるのだ。

続いて、「/Pages」の中身を見てみよう。ここには、「/Type」「/MediaBox」「/Count」「/Kids」というキーがある。これらのうち、「/Count」がPDFドキュメントのページ数を表している。ここでは「5」だ。そして「/Kids」の下は配列になっており、ここにページデータがある。

ページデータを開くと、ここにも様々なキーがある。「/Resources」には、カラースペースやフォントに関する情報がある。そして「/Contents」の下に、テキストデータを含むページのコンテンツデータがあるのだ。この内容を解析するのが、最初の目標になる。

コンテンツデータの内容を確認してみよう。Voyeurでは、[File]→[Show Info]メニューで表示する事ができる。これが、PDFの生データとなる。コンテンツデータは、ストリームデータとも呼ばれる。これは、データを先頭から読み込みながら処理する事ができるからだ。

ざっと見ても、ただのバイナリデータではなく、何らかの処理を示しているような記号が含まれている事が分かるだろう。これらを使ってデータを読み解いていく事になる。

コンテントストリームオブジェクトの取得

PDFデータがどのような構造になっているかを少し体験してもらったところで、ここにアクセスするためのiOSのAPIを紹介しよう。コンテンツデータにアクセスするためには、CGPDFContentStreamというオブジェクトを使う。このオブジェクトは、PDFページからCGPDFContentStreamCreateWithPageという関数を使って取得する事ができる。

List 1.

    // PDFファイルのパスを取得
    NSString*   path;
    path = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"pdf"];

    // PDFドキュメントを作成
    CGPDFDocumentRef    document;
    document = CGPDFDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:path]);

    // PDFページを取得
    CGPDFPageRef    page;
    page = CGPDFDocumentGetPage(document, 1);

    // PDFコンテントストリームを取得
    CGPDFContentStreamRef   stream;
    stream = CGPDFContentStreamCreateWithPage(page);

このstreamから、様々なデータを引っ張りだす事になる。また、このオブジェクトは、ページオブジェクトから取得している事に注目してほしい。PDFでは、すべてのデータはページ毎に分割されているのだ。もしテキストがページをまたいでつないでいるとすると、多くの場合はそうなのだが、バラバラに取得して、その後つなぎあわせることになる。

次回はコンテントストリームへのアクセス方法について説明しよう。