表示調整
閉じる
挿絵表示切替ボタン
▼配色
▼行間
▼文字サイズ
▼メニューバー
×閉じる

ブックマークに追加しました

設定
0/400
設定を保存しました
エラーが発生しました
※文字以内
ブックマークを解除しました。

エラーが発生しました。

エラーの原因がわからない場合はヘルプセンターをご確認ください。

ブックマーク機能を使うにはログインしてください。
3/3

縦書きの電子書籍(ePub)を作成するPythonのコード

 不具合が有るかもしれないので利用は自己責任でお願いいたします。

 コピペする場合は、2文字の全角空白を4文字の半角空白に、全角縦線を|に、置換してください。

 UTF-8という文字コードで、例えば、MakeVerticalWritingEPub.pyというファイル名で保存して、python3 MakeVerticalWritingEPub.pyというコマンドで実行します。

 EPUBCheck 3.0 beta(validator.idpf.org)のチェックを通過しました。

 Linux MintからWine経由でWindows版KindlePreviewer内のKindleGen(kindlegen.exe)を起動して、.mobiファイルへの変換も正常終了しKindleで.mobiファイルの表示を確認しました。

 動作確認環境はLinux Mint20.1 MATEです。

 Windowsでも動くかもしれませんが、確認していません。


 横書き版のコードと同じ仕様のディレクトリとファイルから縦書きの電子書籍(ePub)を作成できます。


 Githubで縦書きの電子書籍(ePub)を作成するPythonのソース コードなどをパブリック ドメインで公開しております。

 マイクロソフトのBing検索エンジンで「github Eliphas1810 free-epub-maker」などで検索してみてください。

 残念ながらグーグル検索エンジンでは検索できません。

import tkinter

from tkinter import filedialog

from tkinter import messagebox

import os

import re

import tempfile

import datetime

import uuid

import shutil

import zipfile


root = tkinter.Tk()

root.geometry("800x300")

root.title("縦書きの電子書籍(ePub)作成")


in_dir_string_var = tkinter.StringVar()

in_dir_string_var.set("")


in_dir_label = tkinter.Label(root, textvariable=in_dir_string_var)

in_dir_label.place(x=10, y=40)


# 読み込みディレクトリ選択ボタンが押された時の処理

def select_in_dir():

  try:

    in_dir_string_var.set(filedialog.askdirectory(initialdir=os.path.expanduser("~")))

  except Exception as exception:

    messagebox.showerror(exception.__class__.__name__, str(exception))

    raise


in_dir_select_button = tkinter.Button(root, text="読み込みディレクトリ選択", command=select_in_dir)

in_dir_select_button.place(x=10, y=10)


out_dir_string_var = tkinter.StringVar()

out_dir_string_var.set("")


out_dir_label = tkinter.Label(root, textvariable=out_dir_string_var)

out_dir_label.place(x=10, y=110)


# 書き込みディレクトリ選択ボタンが押された時の処理

def select_out_dir():

  try:

    out_dir_string_var.set(filedialog.askdirectory(initialdir=os.path.expanduser("~")))

  except Exception as exception:

    messagebox.showerror(exception.__class__.__name__, str(exception))

    raise


out_dir_select_button = tkinter.Button(root, text="書き込みディレクトリ選択", command=select_out_dir)

out_dir_select_button.place(x=10, y=80)


author_label = tkinter.Label(text="著者")

author_label.place(x=10, y=150)


author_textbox = tkinter.Entry(width=80)

author_textbox.place(x=50, y=150)


message_string_var = tkinter.StringVar()

message_string_var.set("")


message_label = tkinter.Label(root, textvariable=message_string_var)

message_label.place(x=10, y=220)


# HTMLエスケープ

# HTMLエスケープは'を'に置換

def escape_html(text):

  text = re.sub("&", "&", text)

  text = re.sub('"', """, text)

  text = re.sub("'", "'", text)

  text = re.sub("<", "&lt;", text)

  text = re.sub(">", "&gt;", text)

  return text


# XMLエスケープ

# XMLエスケープは'を&apos;に置換

def escape_xml(text):

  text = re.sub("&", "&amp;", text)

  text = re.sub('"', "&quot;", text)

  text = re.sub("'", "&apos;", text)

  text = re.sub("<", "&lt;", text)

  text = re.sub(">", "&gt;", text)

  return text


# 縦書きの電子書籍(ePub)作成ボタンが押された時の処理

def make_epub():

  try:


    in_dir = in_dir_string_var.get()

    out_dir = out_dir_string_var.get()

    author = author_textbox.get()


    if in_dir == "":

      message_string_var.set("読み込みディレクトリを選択してください。")

      return

    if out_dir == "":

      message_string_var.set("書き込みディレクトリを選択してください。")

      return

    if author == "":

      message_string_var.set("著者を記入してください。")

      return

    if os.path.exists(in_dir) == False:

      message_string_var.set("読み込みディレクトリを選択し直してください。")

      return

    if os.path.exists(out_dir) == False:

      message_string_var.set("書き込みディレクトリを選択し直してください。")

      return


    # .opfファイルの最終更新日時と出版日

    epub_make_datetime_xml = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")


    title = os.path.basename(in_dir)


    file_name_list = os.listdir(in_dir)

    text_file_name_list = []

    image_file_name_list = []

    cover_image_file_name = ""

    for file_name in file_name_list:

      # ファイルの場合

      if os.path.isfile(os.path.join(in_dir, file_name)):

        # .txtファイルの場合

        if re.match("^.+\\.[tT][xX][tT]$", file_name):

          text_file_name_list.append(file_name)

        # .jpgファイルか.pngファイルの場合

        elif re.match("^.+\\.[jJ][pP][eE]?[gG]$|^.+\\.[pP][nN][gG]$", file_name):

          image_file_name_list.append(file_name)

          # cover.jpgかcover.pngの場合

          if re.match("^cover\\.[jJ][pP][eE]?[gG]$|^cover\\.[pP][nN][gG]$", file_name):

            cover_image_file_name = file_name


    text_file_name_list.sort()

    image_file_name_list.sort()


    if len(text_file_name_list) == 0:

      message_string_var.set("読み込みディレクトリに.txtファイルが有りません。")

      return


    # 一時ディレクトリを作成

    temp_dir = tempfile.TemporaryDirectory()


    # mimetypeファイルを新規作成

    with open(os.path.join(temp_dir.name, "mimetype"), "w", encoding="utf-8", newline="\n") as file:

      file.write('application/epub+zip')


    # META-INFディレクトリを新規作成

    meta_inf_dir = os.path.join(temp_dir.name, "META-INF")

    os.mkdir(meta_inf_dir)


    # META-INF/container.xmlを新規作成

    with open(os.path.join(meta_inf_dir, "container.xml"), "w", encoding="utf-8", newline="\n") as file:

      file.write('<?xml version="1.0" encoding="UTF-8"?>\n')

      file.write('<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n')

      file.write('  <rootfiles>\n')

      file.write('    <rootfile full-path="opf.opf" media-type="application/oebps-package+xml"/>\n')

      file.write('  </rootfiles>\n')

      file.write('</container>\n')


    # opf.opfファイルを新規作成

    with open(os.path.join(temp_dir.name, "opf.opf"), "w", encoding="utf-8", newline="\n") as file:

      file.write('<?xml version="1.0" encoding="UTF-8"?>\n')

      file.write('<package unique-identifier="pub-id" version="3.0" xmlns="http://www.idpf.org/2007/opf">\n')

      file.write('  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n')

      file.write('    <dc:identifier id="pub-id">urn:uuid:' + str(uuid.uuid4()).upper() + '</dc:identifier><!-- UUID -->\n')

      file.write('    <dc:title>' + escape_xml(title) + '</dc:title>\n')

      file.write('    <dc:language>ja</dc:language>\n')

      file.write('    <meta property="dcterms:modified">' + epub_make_datetime_xml + '</meta><!-- 最終更新日時 -->\n')

      file.write('\n')

      file.write('    <dc:creator id="creator1">' + escape_xml(author) + '</dc:creator><!-- 著者の名前 -->\n')

      file.write('    <meta refines="#creator1" property="role" scheme="marc:relators" id="role">aut</meta>\n')

      file.write('\n')

      file.write('    <dc:publisher>' + escape_xml(author) + '</dc:publisher><!-- 出版者か出版社 -->\n')

      file.write('    <dc:date>' + epub_make_datetime_xml + '</dc:date><!-- 出版日 -->\n')

      file.write('  </metadata>\n')

      file.write('  <manifest>\n')

      file.write('    <item id="css" href="common.css" media-type="text/css"/>\n')

      file.write('    <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>\n')

      # cover.jpgかcover.pngが存在する場合

      if cover_image_file_name != "":

        # cover.jpgの場合

        if re.match("^cover\\.[jJ][pP][eE]?[gG]$", cover_image_file_name):

          file.write('    <item id="cover" href="' + cover_image_file_name + '" media-type="image/jpeg" properties="cover-image"/>\n')

        # cover.pngの場合

        elif re.match("^cover\\.[pP][nN][gG]$", cover_image_file_name):

          file.write('    <item id="cover" href="' + cover_image_file_name + '" media-type="image/png" properties="cover-image"/>\n')

      # .txtファイルの数だけ、くり返し

      for number in range(1, len(text_file_name_list) + 1):

        file.write('    <item id="index' + str(number) + '" href="index' + str(number) + '.xhtml" media-type="application/xhtml+xml"/>\n')

      # 画像ファイルの数だけ、くり返し

      for image_file_name in image_file_name_list:

        # cover.jpgではない場合かcover.pngではない場合

        if image_file_name != cover_image_file_name:

          # .jpgファイルの場合

          if re.match("^.+\\.[jJ][pP][eE]?[gG]$", image_file_name):

            file.write('    <item id="' + escape_xml(os.path.splitext(image_file_name)[0]) + '" href="' + escape_xml(image_file_name) + '" media-type="image/jpeg"/>\n')

          # .pngファイルの場合

          elif re.match("^.+\\.[pP][nN][gG]$", image_file_name):

            file.write('    <item id="' + escape_xml(os.path.splitext(image_file_name)[0]) + '" href="' + escape_xml(image_file_name) + '" media-type="image/png"/>\n')

      file.write('  </manifest>\n')

      file.write('  <spine page-progression-direction="rtl">\n')

      # .txtファイルの数だけ、くり返し

      for number in range(1, len(text_file_name_list) + 1):

        file.write('    <itemref idref="index' + str(number) + '"/>\n')

      file.write('  </spine>\n')

      file.write('</package>\n')


    # common.cssファイルを新規作成

    with open(os.path.join(temp_dir.name, "common.css"), "w", encoding="utf-8", newline="\n") as file:

      file.write('html {\n')

      file.write('  -webkit-writing-mode: vertical-rl;\n')

      file.write('  -webkit-text-orientation: mixed;\n')

      file.write('  -epub-writing-mode: vertical-rl;\n')

      file.write('  -epub-text-orientation: mixed;\n')

      file.write('  writing-mode: vertical-rl;\n')

      file.write('  text-orientation: mixed;\n')

      file.write('}\n')

      file.write('\n')

      file.write('h1 {\n')

      file.write('  margin: 0;\n')

      file.write('  margin-top: 2rem;\n')

      file.write('  margin-left: 2rem;\n')

      file.write('  font-size: 2rem;\n')

      file.write('  font-weight: bold;\n')

      file.write('}\n')

      file.write('\n')

      file.write('h2 {\n')

      file.write('  margin: 0;\n')

      file.write('  margin-top: 1.5rem;\n')

      file.write('  margin-left: 1.5rem;\n')

      file.write('  page-break-before: always;\n')

      file.write('  font-size: 1.5rem;\n')

      file.write('  font-weight: bold;\n')

      file.write('}\n')

      file.write('\n')

      file.write('h2#episode1 {\n')

      file.write('  page-break-before: avoid;\n')

      file.write('}\n')

      file.write('\n')

      file.write('p {\n')

      file.write('  margin: 0;\n')

      file.write('}\n')

      file.write('\n')

      file.write('img {\n')

      file.write('  display: block;\n')

      file.write('  height: 90%;\n')

      file.write('  margin: auto;\n')

      file.write('}\n')


    # nav.xhtmlファイルを新規作成

    with open(os.path.join(temp_dir.name, "nav.xhtml"), "w", encoding="utf-8", newline="\n") as file:

      file.write('<?xml version="1.0" encoding="UTF-8"?>\n')

      file.write('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="ja" xml:lang="ja">\n')

      file.write('  <head>\n')

      file.write('    <title>目次</title>\n')

      file.write('    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n')

      file.write('  </head>\n')

      file.write('  <body>\n')

      file.write('    <nav epub:type="toc">\n')

      file.write('      <ol>\n')

      # .txtファイルの数だけ、くり返し

      for index, text_file_name in enumerate(text_file_name_list):

        file.write('        <li><a href="index' + str(index + 1) + '.xhtml#episode' + str(index + 1) + '">' + escape_html(re.sub("^[0-9]*[  ]*|\\.[tT][xX][tT]$", "", text_file_name)) + '</a></li>\n')

      file.write('      </ol>\n')

      file.write('    </nav>\n')

      file.write('  </body>\n')

      file.write('</html>\n')


    # 画像ファイルを読み込みディレクトリから一時ディレクトリへコピー

    for image_file_name in image_file_name_list:

      shutil.copyfile(os.path.join(in_dir, image_file_name), os.path.join(temp_dir.name, image_file_name))


    # index番号.xhtmlを作成

    for index, text_file_name in enumerate(text_file_name_list):

      with open(os.path.join(temp_dir.name, "index" + str(index + 1) + ".xhtml"), "w", encoding="utf-8", newline="\n") as file:

        file.write('<?xml version="1.0" encoding="UTF-8"?>\n')

        file.write('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="ja" xml:lang="ja">\n')

        file.write('  <head>\n')

        file.write('    <title>' + escape_html(title) + '</title>\n')

        file.write('    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n')

        file.write('    <link href="common.css" rel="stylesheet" type="text/css"/>\n')

        file.write('  </head>\n')

        file.write('  <body>\n')


        if index == 0:

          file.write('    <h1>' + escape_html(title) + '</h1>\n')


        file.write('    <h2 id="episode' + str(index + 1) + '">' + escape_html(re.sub("^[0-9]*[  ]*|\\.[tT][xX][tT]$", "", text_file_name)) + '</h2>\n')

        with open(os.path.join(in_dir, text_file_name), "r", encoding="utf-8") as f:

          while True:

            text = f.readline()

            if text == '':

              break

            # 文末の改行コードを削除

            text = re.sub("\n$", "", text)

            # 読み込んだ行が.jpgか.pngで終わる場合

            if re.match("^.+\\.[jJ][pP][eE]?[gG]$|^.+\\.[pP][nN][gG]$", text):

              # imgタグに置換

              file.write('    <img id="' + escape_html(re.sub("\\.[jJ][pP][eE]?[gG]$|\\.[pP][nN][gG]$", "", text)) + '" src="' + escape_html(text) + '" />\n')

            # 読み込んだ行が空行の場合

            elif text == "":

              # <p>全角空白</p>を出力

              file.write('    <p> </p>\n')

            # 読み込んだ行が普通のテキストの場合

            else:

              # HTMLエスケープ

              # HTMLエスケープは'を&#39;に置換

              text = escape_html(text)


              # 漢字 半角括弧開き ひらがなかカタカナ 半角括弧閉じ をルビに置換

              text = re.sub("([一-鿋々]+)\\(([ぁ-ゖァ-ヺー]+)\\)", "<ruby>\\1<rt>\\2</rt></ruby>", text)


              # 全角縦線 るび対象 二重山括弧開き ルビ 二重山括弧閉じ をルビに置換

              text = re.sub("全角縦線([^《]+)《([^》]+)》", "<ruby>\\1<rt>\\2</rt></ruby>", text)

              # 漢字 二重山括弧開き ひらがなかカタカナ 二重山括弧閉じ をルビに置換

              text = re.sub("([一-鿋々]+)《([ぁ-ゖァ-ヺー]+)》", "<ruby>\\1<rt>\\2</rt></ruby>", text)


              file.write('    <p>' + text + '</p>\n')

        file.write('  </body>\n')

        file.write('</html>\n')


    # .epubファイルを作成

    # zip圧縮

    with zipfile.ZipFile(os.path.join(out_dir, title + ".epub"), 'w', zipfile.ZIP_STORED) as zip_file:

      zip_file.write(os.path.join(temp_dir.name, "mimetype"), "mimetype")

      zip_file.write(os.path.join(meta_inf_dir, "container.xml"), "META-INF/container.xml")

      zip_file.write(os.path.join(temp_dir.name, "opf.opf"), "opf.opf")

      zip_file.write(os.path.join(temp_dir.name, "common.css"), "common.css")

      zip_file.write(os.path.join(temp_dir.name, "nav.xhtml"), "nav.xhtml")

      # 画像ファイルの数だけ、くり返し

      for image_file_name in image_file_name_list:

        zip_file.write(os.path.join(temp_dir.name, image_file_name), image_file_name)

      # .txtファイルの数だけ、くり返し

      for number in range(1, len(text_file_name_list) + 1):

        zip_file.write(os.path.join(temp_dir.name, "index" + str(number) + ".xhtml"), "index" + str(number) + ".xhtml")


    # 一時ディレクトリを削除

    temp_dir.cleanup()


    message_string_var.set("縦書きの電子書籍(ePub)の作成が完了しました。")


  except Exception as exception:

    messagebox.showerror(exception.__class__.__name__, str(exception))

    raise


make_epub_button = tkinter.Button(root, text="縦書きの電子書籍(ePub)作成", command=make_epub)

make_epub_button.place(x=10, y=180)


root.mainloop()

 tkinterというGUIライブラリがPythonには標準で含まれている……はずなのですが、著者の場合は、なぜかインストールする必要がありました。Python3-tkをインストールしました。


 2021年3月13日時点で、FireHD8PlusタブレットのKindleアプリは文字とheight: 100%;の画像が混在していると画像を表示してくれない事が有るので、height: 90%;に設定しています。


 ePub3.3の仕様では、電子書籍の内容である.epubファイル内の.xhtmlファイルの仕様は、旧「XHTML5」、2024年時点の現「HTML Living Standard」の「XML syntax」、「XML構文」だそうです。

 このため、ePub3.3の仕様では、電子書籍の内容である.epubファイル内の.xhtmlファイルを、電子書籍の内容の断片と見なしているのかもしれないですね。

 ちなみに、旧「HTML5」の仕様を策定していた2つの団体間の仕様の差異が大きく成って一方の団体の仕様へ一本化されたため、用語、名称が変化したらしいです。

 2024年時点の現「HTML Living Standard」の「XML構文」の仕様では、必要な場合は、「<!DOCTYPE html>」を書いても良いそうです。

 つまり、ePub3.3の仕様では、「<!DOCTYPE html>」を省略できるようです。

 そのため、当アプリケーションでは「<!DOCTYPE html>」を省略しております。

 一方、2024年時点の現「HTML Living Standard」の、「XML構文」ではなく、「HTML構文」の仕様では、「<!DOCTYPE html>」は全て英大文字である必要が有り、必ず書く必要が有ります。

評価をするにはログインしてください。
この作品をシェア
Twitter LINEで送る
ブックマークに追加
ブックマーク機能を使うにはログインしてください。
+注意+

特に記載なき場合、掲載されている作品はすべてフィクションであり実在の人物・団体等とは一切関係ありません。
特に記載なき場合、掲載されている作品の著作権は作者にあります(一部作品除く)。
作者以外の方による作品の引用を超える無断転載は禁止しており、行った場合、著作権法の違反となります。

この作品はリンクフリーです。ご自由にリンク(紹介)してください。
この作品はスマートフォン対応です。スマートフォンかパソコンかを自動で判別し、適切なページを表示します。

↑ページトップへ