こんにちは、ponpoko1968です。人間でいうと20代後半に相当します。 少し間が開いてしまいましたが、第2回はページイメージの表示を行います。

1 ページ表示ビューの作成

まずページのイメージを表示するビューを作成します。(紛らわしいですが便宜上ページビューと呼ぶことにします。)Xcodeから、[ファイル][新規ファイル]を選択し、必要なオプションを指定(下図参照)して、「PageViewController」を作成します。
新規クラスの追加


PageVewController.hを開いて、下記のように定義を追加します。
@interface PageViewController : UIViewController <UIScrollViewDelegate>{
  IBOutlet UIView*      contentView_;
  Document*             document_;
}
@property (nonatomic,retain) Document*          document;
まず、UIScrollViewDelegateプロトコルを実装し、画面の拡大/スクロールに対応します。UIViewクラスのcontentViewメンバを追加します。IBoutletキーワードを前につけることで、Interface Builder(以下IB)から認識されるようにします。

1.1 nibの作成

次にPageViewController.xibをダブルクリックしてIBを立ち上げ、nibにUIScrollViewを追加します。下図のドキュメントウインドウの状態のように、スクロールビューのサブビューとしえViewが来るようにしてください。 (Viewをドラッグして、Scroll Viewの上でドロップすればOKです。)


nibとPageViewコントローラのメンバの結びつきを変えるため、IBのドキュメントウインドウでFile'sOwnerを選択し、インスペクタを表示します。


contentView_ → View view → Scroll View にそれぞれ結びつければ、IBを使っってのPageViewコントローラの準備は完了です。

2 ページビューの表示

2.1 Documentクラスの作成

ユーザが選択した文書をページビューが表示するため、文書前回Bookshelfクラスを定義し、アプリにインストールされた文書ファイル名を返すメソッドを定義しましたが、今回はBookshelfクラスにDocumentクラスの作成するメソッドを追加します。文書ファイルへのパス名を送ると対応するDocumentオブジェクトを返すメソッドを定義します。
-(Document*) documentWithPath:(NSString*)path {
  Document* doc = [documents_  objectForKey:path];
  if( doc ){
      return doc;
  }
  doc = [[Document alloc] initWithPath:path];
  [documents_  setObject:doc forKey:path];
  return doc;
}

2.2 画面遷移

文書一覧画面で、ファイル名をタップするとページビューに遷移して、選択したファイルを内容を表示するようにします。ファイル名がタップされたことをハンドリングするには、UITableViewDelegateプロトコルのtableView:didSelectRowAtIndexPath:をインプリメントします。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  // Bookshelfインスタンスを取得
  Bookshelf* shelf = [Bookshelf sharedBookshelf];

  // nibファイルの内容に基づいてページビューを作成
  PageViewController* pageVc = [[PageViewController alloc] initWithNibName:@"PageViewController" bundle:nil];

  // pageViewにDocumentオブジェクトをセットする(documentプロパティが暗黙的にretainする。)
  NSString* path = [[shelf documentNames] objectAtIndex:indexPath.row];
  pageVc.document = [shelf documentWithPath:path];

  // 画面遷移を行う
  [self.navigationController pushViewController:pageVc animated:YES];

  // ビューコントローラをnavigationControllerに渡したので所有権を放棄する
  [pageVc release];
}
この時点でiPhoneシミュレータで実行すると、左から右に画面が切り替わって、真っ白な画面が表示されると思います。 次はいよいよ、ページの表示を実装します。

3 ページクラスの定義

ここで文書に含まれるページを抽象化したPageクラスを導入します。 PageクラスはDocumentクラスに包含されるクラスなので、Documentが保持するページ数、すなわちページオブジェクトの数を返すメソッド、そして、ページ番号を指定してPageオブジェクトを取得するメソッドを定義します。

4 PDFイメージのレンダリング

PDFページのレンダリングは、Quartz(Core Graphics)に含まれるPDF関連のAPIを使います。APIを用いて画面に描画する流れとしては、
  1. PDFファイルをオープン
  2. Core GraphicsのPDFページオブジェクトを取得
  3. オフスクリーンバッファにPDFページオブジェクトを描画
  4. オフスクリーンイメージを画面に表示
という手順で行います。このあたりの処理は、Appleの公式ドキュメント「Quarts 2D Programming Guide」の「Opening and Viewing a PDF」節で解説されているので、併せて読んでみてください。 まず、PDFファイルを開きます。
 CFURLRef url = CFURLCreateWithFileSystemPath (NULL, (CFStringRef)self.document.filePath,
kCFURLPOSIXPathStyle, 0);
CGPDFDocumentRef pdfDocument = NULL;
pdfDocument = CGPDFDocumentCreateWithURL (url);
CFRelease(url);
所定のページを開きます。
 page =  CGPDFDocumentGetPage ( pdfDocument, self.nombre );
CGPDFPageGetBoxRect()でページの矩形を取得します。
 CGRect aRect = CGPDFPageGetBoxRect (page,kCGPDFCropBox ); 
このとき、第2引数で、kCGPDFCropBoxを指定すると、PDF形式に指定されたcropbox属性の値を返してくれます。PDFではページ全体の大きさとは別に、ページの周囲の余白を切り取った矩形をページのcorpbox属性として別途格納できます。ちなみに、pdf編集ソフトなどでは、自炊したPDFに後からcropbox属性を追加することが出来ます。余白を切り取ることで画面上の文字が大きく表示でき、見やすくなります。
 CGContextRef context = [self createBitmapContextWithSize:CGSizeMake(aRect.size.width*scale,aRect.size.height*scale)]; 
メモリ上のビットマップにQuartzのPDF描画関数を用いてレンダリングするため、ページサイズの情報を元に描画コンテキストを作るcreateBitmapContextWithSize:メソッドを呼び出します。(ソースコード参照)
 CGAffineTransform pdfTransform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, aRect, 0, true);
CGContextConcatCTM( context, pdfTransform ); 
主にページの印刷方向を指定する目的で、各ページにはアフィン変換行列が指定されているので取得し、ページを描画する前に、設定します。 CGContextDrawPDFPage()で、描画コンテキストにレンダリングします。
 CGContextDrawPDFPage( context, page ); 
CGBitmapContextCreateImage()でCGImageオブジェクトに変換します。
 image = CGBitmapContextCreateImage ( context ); 
CGPDFDocumentは取得したページオブジェクトを内部でキャッシュするらしく1、iOSでオープンしたドキュメントはページ取得の都度オープン・クローズした方が良いようです。
 CGPDFDocumentRelease(pdfDocument); 

1 Life is Beutiful:iPadアプリ作成日誌: PDF関連APIのバグについてを参考にさせていただきました。大変助かりました。

5 PDFイメージの表示

イメージを画面に描画するにはいくつか方法がありますが、今回はCore Animationを用いた方法を使いました。 コードは下記のようにきわめてシンプルです。
-(void) renderPage {
Page* page = [document_ pageWithNombre:document_.currentNombre];
CGImageRef ref = [page imageWithScale:2.0];
[[contentView_ layer] setContentsGravity:kCAGravityResizeAspect];
[[contentView_ layer] setContents:(id)ref];
}

ビューからCore Animationのデフォルトレイヤーを取得し、レイヤーにsetContents:メソッドで、イメージを設定するだけです。 レイヤーのsetContentsGravity:メソッドでkCAGravityResizeAspectを指定することで、ページのアスペクト比を維持しつつビューいっぱいにページイメージが表示されます。下のスクリーンショットの例では、新書を自炊したものを表示しています。新書は縦に細長い判型のため、左右に生じた部分は黒く表示されています。これはページビューの背景色がそのまま出ています。開発に置いては、背景色を白以外の色にしておいた方がわかりやすいですが、リリース版では白にしておくとこのような縦長、もしくは横長の判型でも比較的違和感なく表示されると思います。


6 まとめ

今回はPDFファイルからページイメージをオフスクリーンバッファにレンダリングしたのち、画面へ表示するまでを説明しました。 PDFのページイメージの生成はメモリ、処理時間両方についてコストのかかる処理です。特に自炊の場合はページ全体がビットマップイメージとして保存されているのでなおさらです。この連載ではDocument,Pageオブジェクトと一緒にページイメージを永続化することで、処理の高速化を図る方法を解説する予定です。 次回はタップイベントを処理して、複数ページの表示を実現する方法を説明します。 今回より、launchpadというコードリポジトリサイトを用いてソースを公開します。
bzr branch -r article-2-release lp:~klabrd/jisuireader/trunk article-2-release
で取得してください。

7 前回までの目次

iPadで動く電子書籍アプリを作ってみる(1)