ブログをePub化してみた
ブログをePub化しようとしています。 過去に書いた記事等を、そのままePubに出来そうだったので、HTMLファイルを読み込んでePub化するPHPスクリプトを書いてみました。
まだ、全部うまくいっているわけではありませんし、色々問題が残っているのですが、いまのところこんな感じということで。 公開してみることにしました。 たとえば、次のような感じになります。
ePub版:[新連載]インターネット技術妄想論 [第1回] 結局、IPv6ってどうなのよ?! (参考:Web版)
基本的にURLで渡す引数で過去の記事をePub化していますが、過去のHTMLがいい加減なので、結構駄目です。 今後、ePubにも出来るHTMLを心がけながらブログを書くという感じですかね。
ePubはxhtmlファイルなどをZIPで固めたもので、mimetypeのファイルがZIPの先頭で無圧縮状態で格納されている必要があるというフォーマットです。 今回は、以下の情報を参考にしながらePubファイルを作ってみました。
- Open Publication Structure (OPS) 2.0 v0.9871.0(日本語訳版)
- .ZIP File Format Specification
- phpmyadminに含まれているzip.lib.php
サンプルコード
ブログを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で読めます
おまけ
というか、本当は原稿書いてないといけないんですけど。。。 現実逃避です。はい。
最近のエントリ
- ShowNet 2025のルーティングをざっくり紹介
- RoCEとUltra Ethernetの検証:ShowNet 2025
- ソフトルータ推進委員会のスタンプラリー
- 800G関連の楽しい雑談@Interop Tokyo 2025
- VXLAN Group Based Policyを利用したマネージメントセグメント
- ShowNet伝送2025
過去記事
