今回は、PDFドキュメントの表示を拡大する事を議論しよう。PDFで提供されるドキュメントは、紙だとA4サイズ程度のものが多く、これをiPhoneで閲覧するには拡大が不可欠だ。しかも、1.5倍とか2倍程度ではなく、4倍や8倍といった拡大率が必要になるだろう。まずは、iOSでこのような拡大を行うにはどうすればよいか、というところから解説を始めよう。

拡大画像の取り扱い方

iOSプログラミングの最大の難敵は、メモリサイズだ。iOSアプリの開発者は、昔からメインメモリサイズの少なさに泣かされてきた。iPhone 4やiPad 2になっていくらかましになってきたものの、気を抜くとあっという間にメモリを食いつぶしてしまう。

そして、画像の拡大表示は最もメモリを食う処理だ。単純に計算してみよう。通常、フルカラーの画像を表すには、1ピクセルあたり32bit(8bit x RGBA)必要になる。iPhoneの画面サイズは320 x 480なので、一画面あたり必要なメモリ量は4,915,200bit = 600Kbyteということになる。これを縦横4倍までひきのばすと、サイズは16倍になるので9.375MB。iPhone 3Gあたりだと、1つのアプリが使えるメモリサイズは10MBから20MBと言われているので、この辺でアップアップになってくる。仮に8倍までひきのばすと37.5MB。iPhone 4のRetinaディスプレイにいたっては、さらにこれの縦横2倍になるので、もはや考えたくもない。

つまり、拡大画像をそのまますべてオンメモリに格納する事は、不可能な訳だ。そこで出てくる考え方が、画面に表示されているところだけメモリに確保する、というものだ。それを実現するのが、CATiledLayerだ。

CATiledLayerは、CALayerのサブクラスである、レイヤーの一種だ。このクラスを使うと、名前の通り、ビューをタイル状に分割する事ができる。大きなビューを表示するときは、それをタイル状に分けて、画面に表示されるものだけを描画してくれる。これにより、メモリ消費を抑える事ができるのだ。

PDFViewのCATiledLayer対応

CATiledLayerを使ったPDFの表示は、Appleがとてもよくできたサンプルを提供している。ZoomingPDFViewerというものだ。ここでは、このサンプルで使われている手法を、前回のソースコードに組み込みながら拡張して行く事にしよう。

まず前回説明した、PDFViewクラスをCATiledLayerに対応させてみよう。そのため宣言部では、拡大率を表すscaleというプロパティを追加しておく。

List 1.

@interface PDFView : UIView
{
    CGPDFPageRef    _page;
    float           _scale;
}

// プロパティ
@property (nonatomic) CGPDFPageRef page;
@property (nonatomic) float scale;

@end

実装部では、layerClassメソッドを上書きする。これはUIViewで定義されているクラスメソッドだ。このクラスが使うレイヤーのクラスを指定する。このメソッドでCATiledLayerクラスを返す事で、タイル分割が可能になる。

List 2.

+ (Class)layerClass
{
    return [CATiledLayer class];
}

初期化メソッドの中で、CATiledLayerの設定を行っておく。分割するときの段階や、タイルのサイズなどを設定する。

List 3.

- (void)_init
{
    // インスタンス変数の初期化
    _scale = 1.0f;
    self.contentScaleFactor = 1.0f;

    // レイヤーの設定
    CATiledLayer*   layer;
    layer = (CATiledLayer*)self.layer;
    layer.levelsOfDetail = 4;
    layer.levelsOfDetailBias = 4;
        layer.tileSize = CGSizeMake(512.0f, 512.0f);
}

最後に、レイヤーにPDFページを描画する。レイヤーを使って表示する場合は、上書きするメソッドはdrawLayer:inContext:となる。Quartzベースのときに使われるdrawRect:メソッドとは違うので、注意してほしい。描画の基本的な流れは、前回説明したものと同じだ。

List 4.

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    // 背景を白で塗りつぶす
    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
    CGContextFillRect(context, self.bounds);

    // グラフィックコンテキストの保存
    CGContextSaveGState(context);

    // 垂直方向に反転するアフィン変換の設定
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGContextTranslateCTM(context, 0, -CGRectGetHeight(self.bounds));

    // スケールの設定
    CGContextScaleCTM(context, _scale,_scale);

    // ページの描画
    CGContextDrawPDFPage(context, _page);

    // グラフィックコンテキストの復元
    CGContextRestoreGState(context);
}

これで、PDFViewのレイヤー対応は完了だ。

PDFViewControllerへの組み込み

次に、PDFViewControllerを拡張しよう。PDFViewControllerはフリックによるページ移動を実現するために、3つのPDFViewを持っている。前回まではこれらはInterface Builderで配置していた。だがこれをプログラム中で毎回作り直す事にする。理由は、CATiledLayerを使う事で、拡大表示したときなどに予期せぬキャッシュなどが行われることもあり、それを避けたいためだ。

PDFViewControllerに、PDFページのインデックスを指定してPDFViewのインスタンスを作る、_createPdfViewWithIndex:メソッドを追加した。

List 5.

- (PDFView*)_createPdfViewWithIndex:(int)index
{
    // PDF viewを作成
    PDFView*    pdfView;
    pdfView = [[PDFView alloc] initWithFrame:CGRectZero];
    [pdfView autorelease];

    // PDFページを取得
    CGPDFPageRef    page = NULL;
    if (index > 0 || index <= CGPDFDocumentGetNumberOfPages(_document)) {
        page = CGPDFDocumentGetPage(_document, index);
    }
    pdfView.page = page;

    // PDFの大きさを取得
    CGRect  pageRect = CGRectZero;
    float   scale = 1.0f;
    if (page) {
        pageRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
    }
    if (CGRectGetWidth(pageRect) > 0) {
        scale = CGRectGetWidth(self.view.frame) / CGRectGetWidth(pageRect);
    }

    // 初期のPDF表示の大きさおよびスケールを設定
    pageRect.size.width *= scale;
    pageRect.size.height *= scale;
    pdfView.frame = pageRect;
    pdfView.scale = scale;

    return pdfView;
}

このメソッドでは、まずPDFViewのインスタンスを作成。続いて、PDFドキュメントから指定されたインデックスに対応するページを取得。このページをPDFViewで表示させるのだが、初期状態としてページ全体を表示させたい。そのためのスケールの計算も同時に行っている。

後はこのメソッドを、ページの更新を行う_renewPagesの中で呼んでやればいい。これで、フリックによりページ移動を行い、ピンチイン/アウトで拡大/縮小を行うPDFビューワが出来上がった。

実際にこれを動作させてみると、ページを表示するときに分割されたタイル毎に更新されて行く事が分かるだろう。これが、CATiledLayerによる分割表示だ。ただ、見た目がカクカクとした印象を与えてしまうだろう。これを避けたい場合は、あらかじめページを描画した画像をUIImageViewなどで表示させておき、その背後でCATiledLayerの更新を行う、といったテクニックが必要になる。これは、AppleのサンプルであるZoomingPDFViewerで実装されているので、そちらを参照してほしい。ここでは、CATiledLayerの効果を分かりやすくするために、このままにしておく。

CATiledLayerによる分割表示

次回は、PDF内部のデータにアクセスする方法を紹介しよう。

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