ひとそろいの画像ファイルから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 を使った。標準添付されてるのはいいけど、変数を値に置き換えるだけしかできないんじゃ、用途は限られるよなぁ。

Poppler for WindowsでPDFをPNGに変換する

PDF をページごとの画像ファイルに変換したくて、はじめは Python でできないか調べてた。そしたら↓のページで pdf2image という(Pythonの)ライブラリを紹介しているのを見つけた。

ところが記事を読んでみるとこう書いてある:

pdf2imageは「Poppler」というフリーのPDFコマンドラインツールを背後で用います。そのため、Popplerをダウンロードしておく必要があります。

それなら Poppler をそのまま使えばいいじゃん。

というわけで、Poppler for Windows をダウンロードした。

バージョンは 0.68.0 (poppler-0.68.0_x86.7z)。7zip なので 7z コマンドをインストールしてから展開した。

展開したファイルを眺めてみると、bin フォルダの中に pdfimages.exe という実行ファイルがある。これが使えそうだ。PATH を通してとりあえずヘルプを見てみた。

takatoh@montana: tmp > pdfimages -h
pdfimages version 0.68.0
Copyright 2005-2018 The Poppler Developers - http://poppler.freedesktop.org
Copyright 1996-2011 Glyph & Cog, LLC
Usage: pdfimages [options] <PDF-file> <image-root>
  -f <int>       : first page to convert
  -l <int>       : last page to convert
  -png           : change the default output format to PNG
  -tiff          : change the default output format to TIFF
  -j             : write JPEG images as JPEG files
  -jp2           : write JPEG2000 images as JP2 files
  -jbig2         : write JBIG2 images as JBIG2 files
  -ccitt         : write CCITT images as CCITT files
  -all           : equivalent to -png -tiff -j -jp2 -jbig2 -ccitt
  -list          : print list of images instead of saving
  -opw <string>  : owner password (for encrypted files)
  -upw <string>  : user password (for encrypted files)
  -p             : include page numbers in output file names
  -q             : don't print any messages or errors
  -v             : print copyright and version info
  -h             : print usage information
  -help          : print usage information
  --help         : print usage information
  -?             : print usage information

出力は JPEG が欲しかったので -j オプションを指定。<image-root> が何を指すのかよくわからないけどテキトーに。そしたらこうなった。

takatoh@montana: tmp > pdfimages -j sample.pdf foo
takatoh@montana: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24     8:53        5364971 foo-000.ppm
-a---          2020/10/24     8:53        5364971 foo-001.ppm
-a---          2020/10/24     8:53        5364971 foo-002.ppm
-a---          2020/10/24     8:53        5364971 foo-003.ppm
-a---          2020/10/24     8:53        5364971 foo-004.ppm
-a---          2020/10/24     8:53        5364971 foo-005.ppm
-a---          2020/10/24     8:53        5364971 foo-006.ppm
-a---          2020/10/24     8:53        5364971 foo-007.ppm
-a---          2020/03/28     9:12        4082441 sample.pdf

.ppm って!いまどき .ppm ファイルなんて何で見ればいいんだ。なら PNG でいいや。あと、<image-root> は出力ファイルのプレフィックスみたいだな。なので .ppm ファイルは削除してやり直した。

takatoh@montana: tmp > pdfimages -png sample.pdf sample
takatoh@montana: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/03/28     9:12        4082441 sample.pdf
-a---          2020/10/24     9:01        1269290 sample-000.png
-a---          2020/10/24     9:01         446405 sample-001.png
-a---          2020/10/24     9:01         893712 sample-002.png
-a---          2020/10/24     9:01        1258104 sample-003.png
-a---          2020/10/24     9:01        1301072 sample-004.png
-a---          2020/10/24     9:01        1344592 sample-005.png
-a---          2020/10/24     9:01        1157016 sample-006.png
-a---          2020/10/24     9:01         755768 sample-007.png

これで OK。だけど、欲を言えば出力されるファイルをひとつのフォルダに入れたい。それらしいオプションは見当たらないけど、プレフィックスにパスを含めてやればできた。フォルダは先に作っておくこと。

takatoh@montana: tmp > mkdir out


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/24    11:29                out

takatoh@montana: tmp > pdfimages -png sample.pdf out/sample
takatoh@montana: tmp > ls out


    Directory: C:\Users\takatoh\Documents\tmp\out

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24    11:29        1269290 sample-000.png
-a---          2020/10/24    11:29         446405 sample-001.png
-a---          2020/10/24    11:29         893712 sample-002.png
-a---          2020/10/24    11:29        1258104 sample-003.png
-a---          2020/10/24    11:29        1301072 sample-004.png
-a---          2020/10/24    11:29        1344592 sample-005.png
-a---          2020/10/24    11:29        1157016 sample-006.png
-a---          2020/10/24    11:29         755768 sample-007.png

できた。

補足。

-j オプションの説明は write JPEG images as JPEG files となっている。PDFの中身が JPEG なら JPEG で出力するってことのようだ。

Excel VBAの配列のインデックスは0からはじまる

他人の書いた VBA のコードを調べていて初めて知った。

今までずっと VBA の配列のインデックスは 1 始まりだと思ってたんだ。だからたとえば3個の要素を持つ配列についての繰り返しはこんなふうに書いてた。

Dim arr(3)

...

For i = 1 To 3
    MsgBox arr(i)
Next i

これで期待通り動いてたんだから何の疑いも持たずにいた。数あるプログラミング言語の中で 1始まりは少数派かもしれないけど、n 個の要素を持つ配列のインデックスが 1 ~ n というのはわかりやすいともいえる。

だけど、実はインデックス 0 も使えるという。

え?
ということは、配列を Dim arr(3) って宣言するとインデックス 0 から 3 までの4要素の配列ができるってこと?

実際にやってみよう。

Sub TEST()
    Dim arr(3) As String
    
    arr(0) = "Andy"
    arr(1) = "Bill"
    arr(2) = "Charlie"
    arr(3) = "Dave"
    
    For i = 0 To 3
        MsgBox arr(i)
    Next i
End Sub

この TEST マクロを実行すると、たしかに Andy、Bill、Charlie、Dave の4つが順にメッセージボックスに表示される。

うへぇ、なにこの変な仕様。

Ubuntu 20.04に最新のGolangをインストール

といっても公式サイトからダウンロードして展開して PATH を通すだけ。バージョンは 1.15.2 だった。

インストール手順。まずはダウンロードした tarball を /usr/local 以下に展開。

takatoh@apostrophe:Downloads$ sudo tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz

/usr/loca/go/bin に PATH を通す。

export PATH=$PATH:/usr/local/go/bin

ついでに GOPATH を設定しておく。

export GOPATH=$HOME/go/thirdparty:$HOME/go/projects

go version コマンドでバージョンが表示されれば OK。

takatoh@apostrophe:~$ go version
go version go1.15.2 linux/amd64

すんごい楽だな。

Windows10のiTunesからMacのミュージックアプリへ音楽の移行は出来ない

出来ない、というか少なくともウチの環境では出来なかった。

移行アシスタント

Windows10 の iTunes にある音楽を新しい iMac のミュージックアプリにどうやって移行するのか、ググってみたところで見つかったのが「移行アシスタント」だ。これは同じネットワーク内にある Windows 側と iMac 側の両方で移行アシスタントを実行すると簡単にデータ(今回の目的は音楽だけだけど、これに限らない)を移行できるというもの。少なくとも Apple のページにはそう書いてある。

ところが、実際にページに書いてある手順でやろうとしたところ、そもそも Windows 版の移行アシスタントがインストールできない。ファイルをダウンロードし直したり、PC を再起動したりと色々やってみたけど、どうにもならない。移行アシスタントアプリのバージョンのせいなのかそれとも Windows の環境のせいなのか、何度やってもダメだった。

USBメモリでファイルを移動

仕方がないので、Windows 側の iTunes のデータフォルダを丸ごと USB メモリにコピーして iMac に挿した。で、ミュージックアプリにインポート。時間はかかったけど、とりあえず曲の移行はできた。

が、だ。

このやり方はつまり、ミュージックアプリに新しい曲をファイルから追加しただけなので、再生回数みたいなメタデータやプレイリストは移行されてない。

これらをどうやったら Windows から持ってくれるか、調べてみたけどいい方法が見つからなかった。もしかしたらやり方があるのかもしれないけど。

で、いつまでもこれだけに構っていられないので、もう諦めることにした。プレイリストもいくつか程度しか作ってないし、作り直せばいいだろ。

というわけで、何の役にも立たない記事だな。

あ、あとこれだけは書いておく。USB メモリを Windows と Mac の両方で使うには exFAT でフォーマットしておく必要がある。

iMacを手に入れた。コマンドは?

とうとうやってしまった。何をって、タイトルの通り iMac を買ってしまったんだ。21.5 インチ Retina 4K ディスプレイモデル。今月初めに注文して、今日届いた。

今、とりあえずのセットアップが終わったところ。この記事も iMac で書いてる。スペックを書いておくと、次の通り:

  • macOS Catalina 10.15.7 (セットアップしてすぐアップデートした)
  • 3.2GHz 6コア第8世代Intel Core i7
  • 16GB メモリ
  • 1TB Fusion Drive ストレージ
  • Radeon Pro 560X (4GBメモリ)
  • Magic Keybord (テンキー付き)
  • Magic Mouse

これから使うツールやら何やらをインストールしていく。まずは Homebrew かな。作業が進んだら追記する。

Homebrew

公式サイトにインストール用のコマンドが載っているので、それをターミナルにコピペする。

takatoh@MISHIMAnoiMac ~ % /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

RubyとGit

インストールされてた。

takatoh@MISHIMAnoiMac ~ % ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]
takatoh@MISHIMAnoiMac ~ % git --version
git version 2.24.3 (Apple Git-128)

ホスト名を変更

セットアップ時にテキトーにつけられたホスト名が気に入らないので変更。

「bigswifty」にした。

Python

これも入ってた。

takatoh@bigswifty ~ % python -V
Python 2.7.16

ひとまず

こんなところか。

あとは、Windows の iTunes にある音楽を iMac に移動する。これは記事を分けよう。

ISBNを操作するgem petrarcaをリリースした

はてさて、前の記事からもうひと月も経ってしまった。

その、ひと月まえの記事に書いた、Ruby 用の ISBN を操作するライブラリを RubyGems.org にリリースした。名前は petrarca。

最初のリリース(v0.2.0)が8月30日で、その後バージョンアップして現在は v0.4.0(9月8日)。とりあえず自分では満足しているので、しばらくは大きな更新はないはず。

他のライブラリにはない(ざっと調べた限りではなさそうな)機能としては、ハイフン無しの ISBN をハイフンつきに変換する Petrarca.hyphenate メソッドが日本だけでなく世界中の ISBN に対応していること。こんな感じ。

irb(main):001:0> Petrarca.hyphenate("9780061052811")
=> "978-0-06-105281-1"

国や出版社の番号はそれぞれで桁数が違うわけだけど、International ISBN Agency のページから xml ファイルでダウンロードできるのでそれを利用している。このデータはときどき更新されるらしいので、バージョンアップするとしたらその対応だな。

RubyGems.orgにおける車輪の大発明的な話

大発明じゃなくて再発明、な。

ISBN を操作するための Ruby のライブラリを作った。

ISBN (International Standard Book Number)っていうのは、Wikipedia によると、図書の識別に使われる国際規格コードで、日本語では「国際標準図書番号」という。

別に新しいものでもなく、Ruby 用のライブラリも RubyGems.org で検索すればすでにいくつも存在することがわかる。チェックディジットのアルゴリズムも簡単だし、ちょっとやってみるのにはお手軽なのかも。そのせいかどうかは知らないけど、どのライブラリ(gem)をみても俺のニーズとずれている(約30分の調査による)。要するに気に入らない。俺のほしいのは:

  • 妥当性の検証
  • チェックディジットの計算
  • 現行規格(ISBN13)と旧規格(ISBN10)の相互変換
  • ハイフネーション

が簡潔にできる機能であって、こういうのはモジュール関数でやればいいんだよ。わざわざ ISBN クラスなんて作る必要ないんだ。

というわけで、RubyGems.org に登録されてないのも含めると何百回目だか何千回目だかわからない車輪の再発明をしたわけだ。コードは GitHub に上げてある。

で、せっかく作ったんだから RubyGems.org にも上げておこう(みんなそう思ったんだろうな)として rake release したら「そっくりな名前の gem がすでにあるんやで」みたいなメッセージが出て拒否された。一応、既存の gem とはかぶらないことを確認して isbn_utils っていう名前にしたんだけど、似ているだけでもダメみたいだ。そうなのか。知らなかった。

そういうわけなので、とりあえず RubyGems.org での公開は保留。いい名前が思いついたら再チャレンジする。かも。