iPhone で動く epub リーダを作る

はじめに

epub とは iPhone をはじめとした各種デバイスで採用されている公開電子書籍フォーマットの事です。epub リーダを作る前に、いったい epub とは何なのかについて簡単に説明します。非常に乱暴にまとめると、「epub とは特定の形式に従った xhtml ファイルとマニフェストの集まりを zip 圧縮した後、拡張子を epub に変更したもの」だと言えます。そこでこの記事では、ファイルの解凍と読み込みの2段階に分けて解説していこうと思います。

ファイルの解凍

ファイルの解凍には、次のライブラリを利用させてもらいました。

http://code.google.com/p/ziparchive/

このライブラリを使うために、まずは iPhone 向けに用意された zlib のフレームワークを読み込む必要があります。XCode 左部の「グループとファイル」から「Frameworks」のコンテキストメニューを開き、「既存のフレームワークを追加」を選択し、「libz.x.x.x.dylib」を読み込みます。参考までに記事執筆現在、筆者の環境では「libz.1.2.3.dylib」を用いました。

次に「グループとファイル」の「Classes」のコンテキストメニューから、「追加」→「既存のファイル」で、上のアドレスからダウンロードして解凍した「ZipArchive」ディレクトリをそのまま追加します。

それから、解凍する epub ファイルも用意しましょう。外部から読み込める形にしてもいいのですが、まずはバンドルされた epub を読み込む方向で作る事にします。テストに使った epub は、以下のアドレスからダウンロードさせていただきました。

http://www.kobu.com/docs/epub/index.htm

ダウンロードした「sample.epub」を、先ほど同様に「グループとファイル」の「Resources」以下に追加しておきます。これで epub ファイルをバンドルとして利用可能になりました。

下準備が終わったら、実際に解凍を行うクラスを編集します。まずは ZipArchive ライブラリのヘッダファイルをインポートします。

#import "ZipArchive.h"

続いてアプリケーションが利用可能なホームディレクトリを取得します。iPhone 開発では、アプリケーションからアクセス可能なディレクトリに制限がかかっているため、ここできちんと設定しないとファイルの書き出しを行う事ができません。

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,  NSUserDomainMask, YES);
NSString *outPath = [paths objectAtIndex:0];

この outPath が出力されるホームディレクトリになります。なお、ここでは読み込んだファイルが永久に保存される Documents ディレクトリを用いますが、一時的なファイルを保存するための Tmp ディレクトリも利用可能なので、興味のある方は調べてみてください。

次に、読み込むファイルの設定を行います。先ほどインポートした sample.epub を読み込むには、次のように指定します。

NSString *inPath = [[NSBundle mainBundle] pathForResource:@"sample"ofType:@"epub"];

ここまで終わったら、後はファイルの解凍を行うだけです。

ZipArchive *za = [[ZipArchive alloc] init];

if([za UnzipOpenFile:inPath]) {
  BOOL ret = [za UnzipFileTo:[outPath stringByAppendingPathComponent:@"ext"] overWrite:YES];
  if(NO == ret) {
    // error handling
  }
  [za UnzipCloseFile];
}
[za release];

ここまで終わったらシミュレータで動作を確認してみます。無事に解凍が行われた場合、以下のディレクトリに書き込みが行われたはずです。

/Users/userName/Library/Application Support/iPhone Simulator/version/Applications/appCode/Documents/ext

場所は環境によって微妙に変わるようなので、それらしい箇所を適宜調べてみてください。

解凍したファイルを読む

解凍は終わったので xml のパーサを使って epub の中身を解析していきます。解析には、組み込みの NSXMLParser を使います。ここではどのような順番でファイルを読み込むかと、簡単なサンプルソースを説明します。

最初に読み込むのは META-INF/container.xml です。このファイルは位置、名称ともに固定なので、特に注意せずに読み込む事ができるでしょう。中身は次のようになります。

<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>

ここで注目すべきなのは rootfile というタグで、full-path 属性で指定されている content.opf というファイルが次に読むべき目標になります。このファイルは位置や名前は異なりますが、中身は xml なのでパーサで読み込めます。

では、この値を取り出すサンプルを。MyParser というクラスを定義して、そこで読み込む事にします。まずはヘッダファイルから。

// MyParser.h
#import <Foundation/Foundation.h>

// <NSXMLParserDelegate> を必ず記述しておく
@interface MyParser : NSObject <NSXMLParserDelegate> {

}

@property (nonatomic, retain) NSString *opfFilePath;

-(void)parse:(NSString*) output parseError:(NSError**) error;

@end

次に実装部分です。

#import "MyParser.h"

@implementation MyParser

@synthesize opfFilePath;

-(void)parse:(NSString*) output parseError:(NSError**) error {
  NSString *path = [output stringByAppendingPathComponent:@"META-INF/container.xml"];
  NSData *xml = [NSDatadataWithContentsOfFile:path];
  NSXMLParser *parser = [[NSXMLParseralloc] initWithData:xml];

  [parser setDelegate:self];
  [parser setShouldProcessNamespaces:NO];
  [parser setShouldReportNamespacePrefixes:NO];
  [parser setShouldResolveExternalEntities:NO];
  [parser parse];
  NSError *parseError = [parser parserError];
  if (parseError && error) {
    *error = parseError;
  }
  [parser release];
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
  if(qName) {
    elementName = qName;
  }
  if([elementName isEqualToString:@"rootfile"]) {
    opfFilePath = [attributeDict objectForKey:@"full-path"];
  }
}
@end

この時、ヘッダファイルでデリゲートの設定を行わないと警告が出るので注意して下さい。実際にこのクラスを適当に呼び出して使ってみます。

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,  NSUserDomainMask, YES);
NSString *outPath = [paths objectAtIndex:0];

MyParser *mParser = [[MyParseralloc] init];
NSError *parseError = nil;
[mParser parse:outPath parseError:&parseError];
NSString *opfFilePath = [mParser opfFilePath];
[mParser release];

これで、次に読むべき opf ファイルのパスが opfFilePath に代入されました。

後は、同じような要領で読み進めていきます。opf ファイルの中身はこんな感じ。

<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0" xmlns="http://www.idpf.org/2007/opf";
         unique-identifier="BookId">
 <metadata xmlns:dc="http://purl.org/dc/elements/1.1/";
           xmlns:opf="http://www.idpf.org/2007/opf">;
   <dc:title>Epubサンプル本</dc:title>
   <dc:creator opf:role="aut">編集 荒井文吉</dc:creator>
   <dc:language>ja</dc:language>
   <dc:rights>Public Domain</dc:rights>
   <dc:publisher>横浜工文社</dc:publisher>
   <dc:identifier id="BookId">urn:uuid:kobu.com06282007214712</dc:identifier>
 </metadata>
 <manifest>
  <item id="ncx" href="toc.ncx" media-type="text/xml" />
  <item id="style" href="stylesheet.css" media-type="text/css" />
<!--
  <item id="pagetemplate" href="page-template.xpgt" media-type="application/vnd.adobe-page-template+xml" />
--><!-- seems better if not used -->
  <item id="titlepage" href="title_page.xhtml" media-type="application/xhtml+xml" />
  <item id="chapter01" href="chap01.xhtml" media-type="application/xhtml+xml" />
  <item id="chapter02" href="chap02.xhtml" media-type="application/xhtml+xml" />
  <item id="imgl" href="images/koma.gif" media-type="image/gif" /><!-- replaced -->
 </manifest>
 <spine toc="ncx">
  <itemref idref="titlepage" />
  <itemref idref="chapter01" />
  <itemref idref="chapter02" />
 </spine>
</package>

中央付近の ncx ファイルが次の呼び出し先です。コードは省略して、ファイルの中身を示します。

<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/"; version="2005-1">
  <head>
    <meta name="dtb:uid" content="kobu.com06282007214712"/>
    <meta name="dtb:depth" content="1"/>
    <meta name="dtb:totalPageCount" content="0"/>
    <meta name="dtb:maxPageNumber" content="0"/>
  </head>
  <docTitle>
    <text>日本語のEpubサンプル本</text>
  </docTitle>
  <navMap>
    <navPoint id="title_page" playOrder="1">
      <navLabel>
        <text>タイトルページ</text>
      </navLabel>
      <content src="title_page.xhtml"/>
    </navPoint>
    <navPoint id="chapter01" playOrder="2">
      <navLabel>
        <text>第一章</text>
      </navLabel>
      <content src="chap01.xhtml"/>
    </navPoint>
    <navPoint id="chapter02" playOrder="3">
      <navLabel>
        <text>第二章</text>
      </navLabel>
      <content src="chap02.xhtml"/>
    </navPoint>
  </navMap>
</ncx>

これも単なる xml なのが分かると思います。ここに含まれる content タグがそれぞれのファイルへのリンクになっており、src 属性で指定された xhtml を読み込むと、書籍の本文を読む事ができる仕組みというわけです。xthml の表示は UIWebView という標準のクラスで簡単にできるので、後は並べ方等に気をつければ、簡単な epub リーダの完成です。