ブログをePub化してみた

2010/9/9-1

ブログをePub化しようとしています。 過去に書いた記事等を、そのままePubに出来そうだったので、HTMLファイルを読み込んでePub化するPHPスクリプトを書いてみました。

まだ、全部うまくいっているわけではありませんし、色々問題が残っているのですが、いまのところこんな感じということで。 公開してみることにしました。 たとえば、次のような感じになります。

ePub版:[新連載]インターネット技術妄想論 [第1回] 結局、IPv6ってどうなのよ?! (参考:Web版

基本的にURLで渡す引数で過去の記事をePub化していますが、過去のHTMLがいい加減なので、結構駄目です。 今後、ePubにも出来るHTMLを心がけながらブログを書くという感じですかね。

ePubはxhtmlファイルなどをZIPで固めたもので、mimetypeのファイルがZIPの先頭で無圧縮状態で格納されている必要があるというフォーマットです。 今回は、以下の情報を参考にしながらePubファイルを作ってみました。

サンプルコード

ブログをePub化するコードそのものは、かなりごっちゃりしてしまったので、概要部分をサンプルとしてまとめてみました。 PHPで書いてあります。 テキストファイルの圧縮は行っていませんが、gzcompressを利用すれば比較的簡単に行えます。

何かの参考になれば幸いです。


<?php

$articletitle = "HOGE";
$bookid = 'urn:uuid:geekpage.jp_blog_hoge';
$creator = 'あきみち';
$publisher = 'あきみち';

$htmlbody = '  <p>hoge</p>' . "\n";
$htmlbody .= '  <p>hoge </p>' . "\n";

$file = "";
$cent = "";
$filenum = 0;
$offset = 0;

header('Content-Type: application/epub+zip; name="hoge.epub"');
header('Content-Disposition: attachment; filename="hoge.epub"'); 

/////

add("application/epub+zip", "mimetype");

/////

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

add($metainf, "META-INF/container.xml");

/////

$content = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$content .= '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookID" version="2.0">' . "\n";
$content .= '  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">' . "\n";
$content .= '    <dc:creator opf:role="aut">' . $creator . '</dc:creator>' . "\n";
$content .= '    <dc:publisher>' . $publisher . '</dc:publisher>' . "\n";
$content .= '    <dc:language>ja</dc:language>' . "\n";
$content .= '    <dc:identifier id="BookID">' . $bookid . '</dc:identifier>' . "\n";
$content .= '    <dc:title>' . $articletitle . '</dc:title>' . "\n";
$content .= '  </metadata>' . "\n";
$content .= '  <manifest>' . "\n";
$content .= '    <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>' . "\n";
$content .= '    <item id="main.xhtml" href="Text/main.xhtml" media-type="application/xhtml+xml"/>' . "\n";
$content .= '  </manifest>' . "\n";
$content .= '  <spine toc="ncx">' . "\n";
$content .= '    <itemref idref="main.xhtml"/>' . "\n";
$content .= '  </spine>' . "\n";
$content .= '</package>' . "\n";

add($content, "OEBPS/content.opf");

/////

$toc = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$toc .= '<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" ' . "\n";
$toc .= '  "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">' . "\n";
$toc .= '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">' . "\n";
$toc .= '  <head>' . "\n";
$toc .= '    <meta name="dtb:uid" content="' . $bookid . '"/>' . "\n";
$toc .= '    <meta name="dtb:depth" content="0"/>' . "\n";
$toc .= '    <meta name="dtb:totalPageCount" content="0"/>' . "\n";
$toc .= '    <meta name="dtb:maxPageNumber" content="0"/>' . "\n";
$toc .= '  </head>' . "\n";
$toc .= '  <docTitle>' . "\n";
$toc .= '    <text>' . $articletitle . '</text>' . "\n";
$toc .= '  </docTitle>' . "\n";
$toc .= '  <navMap>' . "\n";
$toc .= '    <navPoint id="navPoint-1" playOrder="1">' . "\n";
$toc .= '      <navLabel>' . "\n";
$toc .= '        <text>start</text>' . "\n";
$toc .= '      </navLabel>' . "\n";
$toc .= '        <content src="Text/main.xhtml"/>' . "\n";
$toc .= '    </navPoint>' . "\n";
$toc .= '  </navMap>' . "\n";
$toc .= '</ncx>' . "\n";

add($toc, "OEBPS/toc.ncx");

/////

$main = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
$main .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"' . "\n";
$main .= '  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' . "\n";

$main .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">' . "\n";
$main .= '<head>' . "\n";
$main .= '  <title>' . $articletitle . '</title>' . "\n";
$main .= '</head>' . "\n";

$main .= '<body>' . "\n";
$main .= $htmlbody;
$main .= '</body>' . "\n";
$main .= '</html>' . "\n";

add($main, "OEBPS/Text/main.xhtml");

//
// End of Central Directory
//

// end of central dir signature  4 bytes  (0x06054b50)
$end = pack('V', 0x06054b50);
$end .= pack('v', 0); // number of this disk         2 bytes

// number of the disk with the start of
// the central directory 2 bytes
$end .= pack('v', 0);

// total number of entries in the central directory
// on this disk 2 bytes
$end .= pack('v', $filenum);

// total number of entries in the central directory 2 bytes
$end .= pack('v', $filenum);

// size of the central directory 4 bytes
$end .= pack('V', strlen($cent));

// offset of start of central directory with respect to
// the starting disk number 4 bytes
$end .= pack('V', strlen($file));
$end .= pack('v', 0); // .ZIP file comment length   2 bytes


//
// OUTPUT
//
echo $file;
echo $cent;
echo $end;
// fwrite(STDOUT, $file, strlen($file));
// fwrite(STDOUT, $cent, strlen($cent));
// fwrite(STDOUT, $end, strlen($end));

function add($data, $filename) {
  global $file;
  global $cent;
  global $filenum;
  global $offset;

  $fnlen = strlen($filename);
  $datalen = strlen($data);

  // local file header signature 4 bytes(0x04034b50)
  $file .= pack('V', 0x04034b50);
  $file .= pack('v', 0x0014); //version needed to extract 2 bytes
  $file .= pack('v', 0x0000);//general purpose bit flag   2 bytes
  $file .= pack('v', 0x0000); // bcompression method      2 bytes
  $file .= pack('v', 0x0000); // last mod file time       2 bytes
  $file .= pack('v', 0x0000); // last mod file date       2 bytes
  $file .= pack('V', crc32($data)); //crc-32              4 bytes
  $file .= pack('V', $datalen); // compressed size        4 bytes
  $file .= pack('V', $datalen); // uncompressed size      4 bytes
  $file .= pack('v', $fnlen); //file name length          2 bytes
  $file .= pack('v', 0x0000); //extra field length        2 bytes
  $file .= $filename; //file name (variable size)

  // if you want to compress the data, you can use gzcompress().
  // when using gzcompress, don't for get to set compressed size
  // value appropriately.
  $file .= $data; // file data

  $filenum++;

  // central file header signature 4 bytes(0x02014b50)
  $cent .= pack('V', 0x02014b50);
  $cent .= pack('v', 0); // version made by            2 bytes
  $cent .= pack('v', 0); // version needed to extract  2 bytes
  $cent .= pack('v', 0); // general purpose bit flag   2 bytes
  $cent .= pack('v', 0); // compression method         2 bytes
  $cent .= pack('v', 0); // last mod file time         2 bytes
  $cent .= pack('v', 0); // last mod file date         2 bytes
  $cent .= pack('V', crc32($data)); // crc-32          4 bytes
  $cent .= pack('V', $datalen); // compressed size     4 bytes
  $cent .= pack('V', $datalen); // uncompressed size   4 bytes
  $cent .= pack('v', $fnlen); // file name length      2 bytes
  $cent .= pack('v', 0); // extra field length         2 bytes
  $cent .= pack('v', 0); // file comment length        2 bytes
  $cent .= pack('v', 0); // disk number start          2 bytes
  $cent .= pack('v', 0); // internal file attributes   2 bytes
  $cent .= pack('V', 0); // external file attributes   4 bytes

  // relative offset of local header 4 bytes
  $cent .= pack('V', $offset);
  $cent .= $filename; // 

  $offset = strlen($file);
}

?>

メモ

以下、いくつかメモです。

  • 私のサイトのHTMLが怪しいので、そのままePub化できない記事が多い
  • Content-Typeだけではなく、Content-Dispositionを使ってダウンロード時のファイル名も指定した方が良い
  • ePubバリデータ便利 http://threepress.org/document/epub-validate
  • RSSとかPodcast URLでサイトでのePub更新を受け取れる方法をそのうち考える予定
  • 個人的には「電子書籍」というよりもWebへの出力フォーマットの一つという位置づけ
  • そのうち、各個別記事からePubフォーマットへのリンクを張る予定
  • Amazon Kindleで読めるフォーマット用のスクリプトも作りたい
  • この記事自身もePubで読めます

おまけ

というか、本当は原稿書いてないといけないんですけど。。。 現実逃避です。はい。

最近のエントリ

過去記事

過去記事一覧

IPv6基礎検定

YouTubeチャンネルやってます!