ひとそろいの画像ファイルからEPUB(電子書籍)ファイルを作る

Qiita の↓の記事を読んで、EPUB って結構簡単(もちろん単純なものなら)なんだな、と思ったので Python でスクリプトを作ってみた。

作ったもの

フォルダに入った画像ファイル一式から EPUB ファイルを生成する。

  • 1ページ1画像のファイル一式
  • とりあえず PNG にだけ対応
  • 画像ファイルはファイル名でソートするので連番でなくても構わない
  • 目次とかそういうのはなし
  • 元データのフォルダ名が EPUB のタイトル、ファイル名になる

ファイル構成は次の通り:

takatoh@montana: img2epub > tree /f .
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB
│  .gitignore
│  img2epub.py
│
├─data
│      book.opf.template
│      chap1.xhtml.template
│      container.xml
│      nav.xhtml
│
└─sample
        sample-000.png
        sample-001.png
        sample-002.png
        sample-003.png
        sample-004.png
        sample-005.png
        sample-006.png
        sample-007.png

img2epub.py が Python で書いたスクリプト本体。data フォルダ以下のファイルは EPUB を構成するファイルあるいはそのテンプレート。スクリプトは次の通り:

#!/usr/bin/env python
# encoding: utf-8


import sys
import os
import shutil
import subprocess
from datetime import datetime, timezone
import uuid
import glob
from jinja2 import Template, Environment, FileSystemLoader


def main():
    src_dir = sys.argv[1]
    now = datetime.now(timezone.utc)
    tmp_dir_name = "tmp.epub.{time}".format(time=now.strftime("%Y%m%d%H%M%S"))

    make_dirs(tmp_dir_name)
    images = copy_images(src_dir, tmp_dir_name)
    images = [s.replace("\\","/") for s in sorted(images)]
    gen_mimetype(tmp_dir_name)
    copy_container(tmp_dir_name)
    book_opf_context = {
        "title": src_dir,
        "time": now.isoformat(),
        "images": images
    }
    gen_book_opf(tmp_dir_name, book_opf_context)
    copy_nav(tmp_dir_name)
    gen_chap1_xhtml(tmp_dir_name, book_opf_context)
    zip_epub(tmp_dir_name, src_dir)


def make_dirs(tmp_dir_name):
    os.makedirs(os.path.join(tmp_dir_name, "META-INF"))
    os.makedirs(os.path.join(tmp_dir_name, "EPUB"))


def copy_images(src_dir, tmp_dir_name):
    images_dir = os.path.join(tmp_dir_name, "EPUB/images")
    shutil.copytree(src_dir, images_dir)
    return glob.glob("{dir}/*".format(dir=images_dir))


def gen_mimetype(tmp_dir_name):
    with open(os.path.join(tmp_dir_name, "mimetype"), "w") as f:
        f.write("application/epub+zip")


def copy_container(tmp_dir_name):
    shutil.copyfile("data/container.xml", os.path.join(tmp_dir_name, "META-INF/container.xml"))


def gen_book_opf(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("book.opf.template")
    context["images"] = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    context["cover"] = context["images"][0]
    context["uuid"] = str(uuid.uuid4())
    with open(os.path.join(tmp_dir_name, "EPUB/book.opf"), "w") as f:
        f.write(template.render(context))


def copy_nav(tmp_dir_name):
    shutil.copyfile("data/nav.xhtml", os.path.join(tmp_dir_name, "EPUB/nav.xhtml"))


def gen_chap1_xhtml(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("chap1.xhtml.template")
    images = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    with open(os.path.join(tmp_dir_name, "EPUB/chap1.xhtml"), "w") as f:
        f.write(template.render(images=images))


def zip_epub(tmp_dir_name, title):
    epub_file_name = "../{title}.epub".format(title=title)
    os.chdir(tmp_dir_name)
    subprocess.run(["zip", "-X0", epub_file_name, "mimetype"], stdout=subprocess.DEVNULL)
    subprocess.run(["zip", "-r9", epub_file_name, "*", "-x", "mimetype"], stdout=subprocess.DEVNULL)
    os.chdir("..")



main()

で、sample フォルダ以下が元になる画像ファイル一式。

使い方

スクリプトの引数に画像一式が入っているフォルダを指定するだけ。

takatoh@montana: img2epub > python img2epub.py sample

そうすると、EPUB ファイル(今回は sample.epub)と、EPUB にまとめる前のファイル一式の入ったフォルダ(同じく tmp.epub.20201027130853)ができる。このフォルダはテンポラリなものなので消しちゃってもいいんだけど、今の段階ではまだ残している。

takatoh@montana: img2epub > ls


    Directory: C:\Users\takatoh\Documents\w\img2epub

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/27    20:07                data
d----          2020/10/27    20:09                sample
d----          2020/10/27    22:08                tmp.epub.20201027130853
-a---          2020/10/27    20:07              9 .gitignore
-a---          2020/10/27    20:07           2832 img2epub.py
-a---          2020/10/27    22:08        8011218 sample.epub

あとは出来上がった sample.epub ファイルを好きな EPUB ビューワで見ればいい。

EPUB のファイルの構成

EPUB のファイルっていうのは、基本的には使用で決められたファイルとコンテンツを zip で一つにまとめて、拡張子を .epub にしただけのファイルだ。単純に zip にしただけではないんだけど、そのへんはこの記事では触れない。冒頭の Qiita の記事か、EPUB 3.2 の仕様を参照のこと。

今回作ったスクリプト img2epub.py では、tmp.epub.* フォルダにその一式が入っている(つまりこれを zip 圧縮して .epub ファイルを作る)。フォルダの中身は次の通り:

takatoh@montana: img2epub > tree /f tmp.epub.20201027130853
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB\TMP.EPUB.20201027130853
│  mimetype
│
├─EPUB
│  │  book.opf
│  │  chap1.xhtml
│  │  nav.xhtml
│  │
│  └─images
│          sample-000.png
│          sample-001.png
│          sample-002.png
│          sample-003.png
│          sample-004.png
│          sample-005.png
│          sample-006.png
│          sample-007.png
│
└─META-INF
        container.xml

EPUB/images 以下の画像ファイルは、元のデータをコピーしたもの。そのほかのファイルはスクリプトが生成したファイルだ。詳しくは略。

余談

EPUB を構成するファイルの一部(book.opf や chap1.xhtml)を生成するためにテンプレートエンジンを使ってるんだけど、Python には string.Template というテンプレートエンジンが標準でついている。これ、今回調べてて初めて知った。

ところがこの string.Template、単純な値の挿入はできるけど繰り返しや条件分岐の機能がない。今回、条件分岐は使ってないけど繰り返すは必要だったので、結局 Jinja2 を使った。標準添付されてるのはいいけど、変数を値に置き換えるだけしかできないんじゃ、用途は限られるよなぁ。