Rustでeguiで電子書籍(ePub)を作成する

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


 cargo build --releaseというコマンドで最適化してコンパイルすると、処理が速いですが、そうではない場合は処理が遅いです。


 2022年2月4日時点で、eguiはファイル ダイアログにrfd外部ライブラリ クレートを利用していますが、著者の環境ではatk-sysが無いというエラー メッセージのコンパイル エラーが発生しました。

 著者には解決できなかったので、当アプリケーションでは、ファイル ダイアログを利用できませんでした。


Cargo.toml

――――――――――――――――――――

[package]

name = "epub_maker"

version = "0.1.0"

edition = "2021"


[dependencies]

regex = "1"

chrono = "0.4"

tempfile = "3"

zip = "0.5"

eframe = "0.16"

dirs = "4.0"


[dependencies.uuid]

version = "1.0.0-alpha.1"

features = ["v4", "fast-rng", "macro-diagnostics"]

――――――――――――――――――――


main.rs

――――――――――――――――――――

use eframe::epi::App;

use eframe::egui::CtxRef;

use eframe::epi::Frame;

use eframe::epi::Storage;

use eframe::egui::Vec2;

use eframe::egui::FontDefinitions;

use eframe::egui::FontData;

use eframe::egui::FontFamily;

use eframe::egui::CentralPanel;

use eframe::NativeOptions;

use std::path::PathBuf;

use chrono::Local;

use std::fs;

use regex::Regex;

use tempfile::tempdir;

use std::fs::File;

use std::io::BufWriter;

use std::io::Write;

use uuid::Uuid;

use std::io::BufReader;

use std::io::Read;

use std::io::BufRead;

use zip::ZipWriter;

use zip::write::FileOptions;

use zip::CompressionMethod;


//HTMLエスケープ

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

fn escape_html(str: &str) -> String {

  let mut string = str.to_string();

  string = Regex::new("&").unwrap().replace_all(&string, "&").to_string();

  string = Regex::new("\"").unwrap().replace_all(&string, """).to_string();

  string = Regex::new("'").unwrap().replace_all(&string, "'").to_string();

  string = Regex::new("<").unwrap().replace_all(&string, "&lt;").to_string();

  string = Regex::new(">").unwrap().replace_all(&string, "&gt;").to_string();

  return string;

}


//XMLエスケープ

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

fn escape_xml(str: &str) -> String {

  let mut string = str.to_string();

  string = Regex::new("&").unwrap().replace_all(&string, "&amp;").to_string();

  string = Regex::new("\"").unwrap().replace_all(&string, "&quot;").to_string();

  string = Regex::new("'").unwrap().replace_all(&string, "&apos;").to_string();

  string = Regex::new("<").unwrap().replace_all(&string, "&lt;").to_string();

  string = Regex::new(">").unwrap().replace_all(&string, "&gt;").to_string();

  return string;

}


struct AppImpl {

  in_dir: String,

  author: String,

  out_dir: String,

  message: String,

}


impl App for AppImpl {


  //ウィンドウのタイトルなどに利用されます。

  fn name(&self) -> &str {

    return "電子書籍(ePub)作成";

  }


  //描画の前に1回だけ起動されます。

  fn setup(&mut self, ctx: &CtxRef, _frame: &Frame, _storage: Option<&dyn Storage>) {


    //日本語のフォントを設定

    //.ttfか.otf

    let mut font_definitions = FontDefinitions::default();

    font_definitions.font_data.insert(

      "ja".to_owned(),

      FontData::from_static(include_bytes!("/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf"))

    );

    font_definitions.fonts_for_family.get_mut(&FontFamily::Proportional).unwrap().insert(0, "ja".to_owned());

    font_definitions.fonts_for_family.get_mut(&FontFamily::Monospace).unwrap().insert(0, "ja".to_owned());

    ctx.set_fonts(font_definitions);


    //ウィンドウのサイズを設定

    _frame.set_window_size(Vec2::new(800.0, 500.0));

  }


  //描画のたびに起動されます。

  fn update(&mut self, ctx: &CtxRef, _frame: &Frame) {


    //拡大

    ctx.set_pixels_per_point(1.5);


    CentralPanel::default().show(ctx, |ui| {


      ui.horizontal(|ui| {

        ui.label("読み込みディレクトリ");

        ui.text_edit_singleline(&mut self.in_dir);

      });


      ui.horizontal(|ui| {

        ui.label("著者");

        ui.text_edit_singleline(&mut self.author);

      });


      ui.horizontal(|ui| {

        ui.label("書き込みディレクトリ");

        ui.text_edit_singleline(&mut self.out_dir);

      });


      if ui.button("電子書籍(ePub)作成").clicked() {


        let in_dir = self.in_dir.clone();

        let author = self.author.clone();

        let out_dir = self.out_dir.clone();


        self.message = String::from("");


        if in_dir.is_empty() {

          self.message = String::from("読み込みディレクトリを記入してください。");

          return;

        }

        if author.is_empty() {

          self.message = String::from("著者を記入してください。");

          return;

        }

        if out_dir.is_empty() {

          self.message = String::from("書き込みディレクトリを記入してください。");

          return;

        }

        if PathBuf::from(&in_dir).is_dir() == false {

          self.message = String::from("読み込みディレクトリに存在するディレクトリを記入してください。");

          return;

        }

        if PathBuf::from(&out_dir).is_dir() == false {

          self.message = String::from("書き込みディレクトリに存在するディレクトリを記入してください。");

          return;

        }


        //XMLの.opfファイルの出版日としても利用する、XMLの.opfファイルの最終更新日時

        let epub_make_date_time_xml = Local::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();


        let title = PathBuf::from(&in_dir).file_name().unwrap().to_str().unwrap().to_string();


        let mut text_file_vec = fs::read_dir(&in_dir).unwrap()

          .map(|result| result.unwrap().path())

          .filter(|path_buf| path_buf.is_file() && Regex::new(r"^.+\.[tT][xX][tT]$").unwrap().is_match(path_buf.file_name().unwrap().to_str().unwrap()))

          .collect::<Vec<PathBuf>>();


        text_file_vec.sort();


        let mut image_file_vec = fs::read_dir(&in_dir).unwrap()

          .map(|result| result.unwrap().path())

          .filter(|path_buf| path_buf.is_file() && Regex::new(r"^.+\.[jJ][pP][eE]?[gG]$|^.+\.[pP][nN][gG]$").unwrap().is_match(path_buf.file_name().unwrap().to_str().unwrap()))

          .collect::<Vec<PathBuf>>();


        image_file_vec.sort();


        let mut cover_image_path_buf_option = None;

        let cover_image_file_regex = Regex::new(r"^cover\.[jJ][pP][eE]?[gG]$|^cover\.[pP][nN][gG]$").unwrap();

        for path_buf in &image_file_vec {

          let file_name = path_buf.file_name().unwrap().to_str().unwrap();

          if cover_image_file_regex.is_match(file_name) {

            cover_image_path_buf_option = Some(path_buf);

            break;

          }

        }


        if text_file_vec.len() == 0 {

          self.message = String::from("読み込みディレクトリに.txtファイルが有りません。");

          return;

        }


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

        let temp_dir = tempdir().unwrap();


        //mimetypeファイルを新規作成

        let mut mimetype_buf_writer = BufWriter::new(File::create(temp_dir.path().join("mimetype")).unwrap());

        mimetype_buf_writer.write("application/epub+zip".as_bytes()).unwrap();

        mimetype_buf_writer.flush().unwrap();


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

        fs::create_dir(temp_dir.path().join("META-INF")).unwrap();


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

        let mut container_buf_writer = BufWriter::new(File::create(temp_dir.path().join("META-INF").join("container.xml")).unwrap());

        container_buf_writer.write("\

<?xml version=\"1.0\" encoding=\"UTF-8\"?>

<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">

  <rootfiles>

    <rootfile full-path=\"opf.opf\" media-type=\"application/oebps-package+xml\"/>

  </rootfiles>

</container>\n\

        ".as_bytes()).unwrap();

        container_buf_writer.flush().unwrap();


        //opf.opfを新規作成

        let mut opf_buf_writer = BufWriter::new(File::create(temp_dir.path().join("opf.opf")).unwrap());

        opf_buf_writer.write(vec![

               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",

               "<package unique-identifier=\"pub-id\" version=\"3.0\" xmlns=\"http://www.idpf.org/2007/opf\">",

               "  <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">",

          &format!("    <dc:identifier id=\"pub-id\">urn:uuid:{}</dc:identifier><!-- UUID -->", &Uuid::new_v4().to_string().to_uppercase()),

          &format!("    <dc:title>{}</dc:title>", &escape_xml(&title)),

               "    <dc:language>ja</dc:language>",

          &format!("    <meta property=\"dcterms:modified\">{}</meta><!-- 最終更新日時 -->", epub_make_date_time_xml),

               "\n",

          &format!("    <dc:creator id=\"creator1\">{}</dc:creator><!-- 著者の名前 -->", &escape_xml(&author)),

               "    <meta refines=\"#creator1\" property=\"role\" scheme=\"marc:relators\" id=\"role\">aut</meta>",

               "\n",

          &format!("    <dc:publisher>{}</dc:publisher><!-- 出版者か出版社 -->", &escape_xml(&author)),

          &format!("    <dc:date>{}</dc:date><!-- 出版日 -->", epub_make_date_time_xml),

               "  </metadata>",

               "  <manifest>",

               "    <item id=\"css\" href=\"common.css\" media-type=\"text/css\"/>",

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

        ].join("\n").as_bytes()).unwrap();


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

        if cover_image_path_buf_option != None {

          let cover_image_file_name = cover_image_path_buf_option.unwrap().file_name().unwrap().to_str().unwrap();

          if cover_image_file_name.is_empty() == false {

            //cover.jpgの場合

            if Regex::new(r"^cover\.[jJ][pP][eE]?[gG]$").unwrap().is_match(cover_image_file_name) {

              opf_buf_writer.write(&format!("    <item id=\"cover\" href=\"{}\" media-type=\"image/jpeg\" properties=\"cover-image\"/>\n", cover_image_file_name).as_bytes()).unwrap();

            //cover.pngの場合

            } else {

              opf_buf_writer.write(&format!("    <item id=\"cover\" href=\"{}\" media-type=\"image/png\" properties=\"cover-image\"/>\n", cover_image_file_name).as_bytes()).unwrap();

            }

          }

        }


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

        for number in 1..(text_file_vec.len() + 1) {

          opf_buf_writer.write(&format!("    <item id=\"index{}\" href=\"index{}.xhtml\" media-type=\"application/xhtml+xml\"/>\n", number, number).as_bytes()).unwrap();

        }


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


        //cover.jpgとcover.png以外の画像ファイルの数だけ、くり返し

        for path_buf in &image_file_vec {

          let file_name = path_buf.file_name().unwrap().to_str().unwrap();

          if Regex::new(r"^cover\.[jJ][pP][eE]?[gG]$|^cover\.[pP][nN][gG]$").unwrap().is_match(file_name) == false {

            //JPEG画像ファイルの場合

            if Regex::new(r"^.+\.[jJ][pP][eE]?[gG]$").unwrap().is_match(file_name) {

              opf_buf_writer.write(

                &format!(

                  "    <item id=\"{}\" href=\"{}\" media-type=\"image/jpeg\"/>\n",

                  &escape_xml(&Regex::new(r"\.[jJ][pP][eE]?[gG]$").unwrap().replace(file_name, "").to_string()),

                  &escape_xml(file_name)

                ).as_bytes()

              ).unwrap();

            //PNG画像ファイルの場合

            } else {

              opf_buf_writer.write(

                &format!(

                  "    <item id=\"{}\" href=\"{}\" media-type=\"image/png\"/>\n",

                  &escape_xml(&Regex::new(r"\.[pP][nN][gG]$").unwrap().replace(file_name, "").to_string()),

                  &escape_xml(file_name)

                ).as_bytes()

              ).unwrap();

            }

          }

        }


        opf_buf_writer.write("  </manifest>\n".as_bytes()).unwrap();

        opf_buf_writer.write("  <spine>\n".as_bytes()).unwrap();


        for number in 1..(text_file_vec.len() + 1) {

          opf_buf_writer.write(&format!("    <itemref idref=\"index{}\"/>\n", number).as_bytes()).unwrap();

        }


        opf_buf_writer.write("  </spine>\n".as_bytes()).unwrap();

        opf_buf_writer.write("</package>\n".as_bytes()).unwrap();

        opf_buf_writer.flush().unwrap();


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

        let mut css_buf_writer = BufWriter::new(File::create(temp_dir.path().join("common.css")).unwrap());

        css_buf_writer.write("\

h1 {

  margin: 0;

  display: block;

  width: 100%;

  text-align: center;

  font-size: 2rem;

  font-weight: bold;

}


h2 {

  margin: 0;

  margin-bottom: 1.5rem;

  page-break-before: always;

  display: block;

  width: 100%;

  text-align: center;

  font-size: 1.5rem;

  font-weight: bold;

}


p {

  margin: 0;

}


img {

  display: block;

  height: 90%;

  margin: auto;

}\n\

        ".as_bytes()).unwrap();

        css_buf_writer.flush().unwrap();


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

        let mut nav_buf_writer = BufWriter::new(File::create(temp_dir.path().join("nav.xhtml")).unwrap());

        nav_buf_writer.write("\

<?xml version=\"1.0\" encoding=\"UTF-8\"?>

<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" lang=\"ja\" xml:lang=\"ja\">

  <head>

    <title>目次</title>

    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>

  </head>

  <body>

    <nav epub:type=\"toc\">

      <ol>\n\

        ".as_bytes()).unwrap();


        for index in 0..text_file_vec.len() {

          let file_name = text_file_vec[index].file_name().unwrap().to_str().unwrap();

          nav_buf_writer.write(

            &format!(

              "        <li><a href=\"index{}.xhtml#episode{}\">{}</a></li>\n",

              index + 1,

              index + 1,

              &escape_html(&Regex::new(r"^[0-9]*[  ]*|\.[tT][xX][tT]$").unwrap().replace_all(file_name, "").to_string())

            ).as_bytes()

          ).unwrap();

        }


        nav_buf_writer.write("\

      </ol>

    </nav>

  </body>

</html>\n\

        ".as_bytes()).unwrap();

        nav_buf_writer.flush().unwrap();


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

        for path_buf in &image_file_vec {

          let file_name = path_buf.file_name().unwrap().to_str().unwrap();

          fs::copy(PathBuf::from(&in_dir).join(file_name), temp_dir.path().join(file_name)).unwrap();

        }


        //index番号.xhtmlの作成

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

        for index in 0..text_file_vec.len() {

          let file_name = text_file_vec[index].file_name().unwrap().to_str().unwrap();

          let mut buf_writer = BufWriter::new(File::create(temp_dir.path().join(format!("index{}.xhtml", index + 1))).unwrap());

          buf_writer.write(vec![

                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",

                "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" lang=\"ja\" xml:lang=\"ja\">",

                "  <head>",

            &format!("    <title>{}</title>", &escape_html(&title)),

                "    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>",

                "    <link href=\"common.css\" rel=\"stylesheet\" type=\"text/css\"/>",

                "  </head>",

                "  <body>\n"

          ].join("\n").as_bytes()).unwrap();



          if index == 0 {

            buf_writer.write(format!("    <h1>{}</h1>\n", &escape_html(&title)).as_bytes()).unwrap();

          }


          buf_writer.write(

            &format!("    <h2 id=\"episode{}\">{}</h2>\n",

            index + 1,

            &escape_html(&Regex::new(r"^[0-9]*[  ]*|\.[tT][xX][tT]$").unwrap().replace_all(file_name, "").to_string())

            ).as_bytes()

          ).unwrap();


          let mut buf_reader = BufReader::new(File::open(PathBuf::from(&in_dir).join(file_name)).unwrap());

          let mut string = String::new();

          let line_separator_regex = Regex::new("\r?\n$|\r$").unwrap();

          while 1 <= buf_reader.read_line(&mut string).unwrap() { //read_lineは改行(\r\nか\nか\r)を返します。

            let mut line = line_separator_regex.replace(&string, "").to_string();


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

            if Regex::new(r"^.+\.[jJ][pP][eE]?[gG]$|^.+\.[pP][nN][gG]$").unwrap().is_match(&line) {

              buf_writer.write(

                &format!("    <img id=\"{}\" src=\"{}\" />\n",

                &escape_html(&Regex::new(r"\.[jJ][pP][eE]?[gG]$|\.[pP][nN][gG]$").unwrap().replace(&line, "").to_string()),

                &escape_html(&line)

                ).as_bytes()

              ).unwrap();


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

            } else if line.is_empty() {

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

              buf_writer.write("    <p> </p>\n".as_bytes()).unwrap();

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

            } else {


              //HTMLエスケープ

              line = escape_html(&line);


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

              line = Regex::new(r"([一-鿋々]+)\(([ぁ-ゖァ-ヺー]+)\)").unwrap().replace_all(&line, "<ruby>$1<rt>$2</rt></ruby>").to_string();


              //全角縦線 二重山括弧開き以外の文字 二重山括弧開き 二重山括弧閉じ以外の文字 二重山括弧閉じ をルビに置換

              line = Regex::new("全角縦線([^《]+)《([^》]+)》").unwrap().replace_all(&line, "<ruby>$1<rt>$2</rt></ruby>").to_string();


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

              line = Regex::new("([一-鿋々]+)《([ぁ-ゖァ-ヺー]+)》").unwrap().replace_all(&line, "<ruby>$1<rt>$2</rt></ruby>").to_string();


              buf_writer.write(&format!("    <p>{}</p>\n", &line).as_bytes()).unwrap();

            }

            string.clear();

          }


          buf_writer.write("\

  </body>

</html>\n\

          ".as_bytes()).unwrap();

          buf_writer.flush().unwrap();

        }


        let mut zip_writer = ZipWriter::new(File::create(PathBuf::from(&out_dir).join(&format!("{}.epub", &title))).unwrap());

        let file_options = FileOptions::default().compression_method(CompressionMethod::Stored);

        let mut bytes_vec = Vec::new();


        zip_writer.start_file("mimetype", file_options).unwrap();

        let mut mimetype_buf_reader = BufReader::new(File::open(temp_dir.path().join("mimetype")).unwrap());

        mimetype_buf_reader.read_to_end(&mut bytes_vec).unwrap();

        zip_writer.write(&bytes_vec).unwrap();

        zip_writer.flush().unwrap();

        bytes_vec.clear();


        zip_writer.start_file("META-INF/container.xml", file_options).unwrap();

        let mut container_buf_reader = BufReader::new(File::open(temp_dir.path().join("META-INF").join("container.xml")).unwrap());

        container_buf_reader.read_to_end(&mut bytes_vec).unwrap();

        zip_writer.write(&bytes_vec).unwrap();

        zip_writer.flush().unwrap();

        bytes_vec.clear();


        zip_writer.start_file("opf.opf", file_options).unwrap();

        let mut opf_buf_reader = BufReader::new(File::open(temp_dir.path().join("opf.opf")).unwrap());

        opf_buf_reader.read_to_end(&mut bytes_vec).unwrap();

        zip_writer.write(&bytes_vec).unwrap();

        zip_writer.flush().unwrap();

        bytes_vec.clear();


        zip_writer.start_file("common.css", file_options).unwrap();

        let mut css_buf_reader = BufReader::new(File::open(temp_dir.path().join("common.css")).unwrap());

        css_buf_reader.read_to_end(&mut bytes_vec).unwrap();

        zip_writer.write(&bytes_vec).unwrap();

        zip_writer.flush().unwrap();

        bytes_vec.clear();


        zip_writer.start_file("nav.xhtml", file_options).unwrap();

        let mut nav_buf_reader = BufReader::new(File::open(temp_dir.path().join("nav.xhtml")).unwrap());

        nav_buf_reader.read_to_end(&mut bytes_vec).unwrap();

        zip_writer.write(&bytes_vec).unwrap();

        zip_writer.flush().unwrap();

        bytes_vec.clear();


        for path_buf in &image_file_vec {

          let file_name = path_buf.file_name().unwrap().to_str().unwrap();

          zip_writer.start_file(file_name, file_options).unwrap();

          let mut buf_reader = BufReader::new(File::open(temp_dir.path().join(file_name)).unwrap());

          buf_reader.read_to_end(&mut bytes_vec).unwrap();

          zip_writer.write(&bytes_vec).unwrap();

          zip_writer.flush().unwrap();

          bytes_vec.clear();

        }


        for number in 1..(text_file_vec.len() + 1) {

          zip_writer.start_file(&format!("index{}.xhtml", number), file_options).unwrap();

          let mut buf_reader = BufReader::new(File::open(temp_dir.path().join(&format!("index{}.xhtml", number))).unwrap());

          buf_reader.read_to_end(&mut bytes_vec).unwrap();

          zip_writer.write(&bytes_vec).unwrap();

          zip_writer.flush().unwrap();

          bytes_vec.clear();

        }


        zip_writer.finish().unwrap();


        fs::remove_file(temp_dir.path().join("mimetype")).unwrap();

        fs::remove_file(temp_dir.path().join("META-INF").join("container.xml")).unwrap();

        fs::remove_dir(temp_dir.path().join("META-INF")).unwrap();

        fs::remove_file(temp_dir.path().join("opf.opf")).unwrap();

        fs::remove_file(temp_dir.path().join("common.css")).unwrap();

        fs::remove_file(temp_dir.path().join("nav.xhtml")).unwrap();


        for path_buf in &image_file_vec {

          let file_name = path_buf.file_name().unwrap().to_str().unwrap();

          fs::remove_file(temp_dir.path().join(file_name)).unwrap();

        }


        for number in 1..(text_file_vec.len() + 1) {

          fs::remove_file(temp_dir.path().join(&format!("index{}.xhtml", number))).unwrap();

        }


        temp_dir.close().unwrap();


        self.message = String::from("電子書籍(ePub)の作成が完了しました。");

      }



      ui.label(&self.message);

    });

  }

}


fn main() {

  eframe::run_native(

    Box::new(

      AppImpl{

        in_dir: dirs::desktop_dir().unwrap().to_str().unwrap().to_string() + "/",

        author: String::from(""),

        out_dir: dirs::desktop_dir().unwrap().to_str().unwrap().to_string() + "/",

        message: String::from(""),

      }

    ),

    NativeOptions::default()

  );

}

――――――――――――――――――――

 コピペする場合は、2文字の全角空白を4文字の半角空白に、全角縦線を|に、/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttfを適切な日本語のフォントのファイルのパスに、置換してください。


 読み込みディレクトリ(フォルダ)の名前を電子書籍のタイトルにします。

 読み込みディレクトリ(フォルダ)直下の.txtファイル名の先頭から半角数字の連続と半角空白を除去した名前を各話の見出しにします。

 例えば、0001 第一話.txtファイルが存在する場合は第一話を見出しにします。

 読み込みディレクトリ(フォルダ)直下の.txtファイル名の文字順で並べるので、例えば0001 第一話.txt、0002 第二話.txtのように、半角数字の番号 半角空白 順番の各話の見出し.txtといった規則で.txtファイル名をつけてください。

 .txtファイルの文字コードはUTF-8にしてください。

 書き込みディレクトリ(フォルダ)に電子書籍のタイトル.epubという名前で電子書籍(ePub)を作成します。

 読み込みディレクトリ(フォルダ)直下の.txtファイルと、.jpgファイルまたは.pngファイルから電子書籍(ePub)を作成します。

 .jpgファイル名と.pngファイル名は、XHTMLのidとして利用するため、半角英数字と半角ハイフン(-)と半角下線(_)による名前にしてください。

 .jpgファイル名と.pngファイル名は、XHTMLのidとして利用するため、最初の文字は半角英字にしてください。

 縦長の画像を用意してください。

 cover.jpgファイルかcover.pngファイルを電子書籍(ePub)の表紙にします。

 .txtファイルの、ある行が.jpgか.pngで終わる場合は画像ファイル名の指定とみなしてHTMLのimgタグに置換します。

 漢字 半角括弧開き ひらがなかカタカナ 半角括弧閉じ をHTMLのrubyタグに置換します。

 漢字 二重山括弧開き ひらがなかカタカナ 二重山括弧閉じ をHTMLのrubyタグに置換します。

 全角縦線 二重山括弧開き以外の文字 二重山括弧開き 二重山括弧閉じ以外の文字 二重山括弧閉じ をHTMLのrubyタグに置換します。

 空行を<p>全角空白</p>に置換します。

 記入された著者を著者と出版者とします。

 Pythonのコードが実行された日時を出版日とします。


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


 .epubファイルの名前と.epubファイル内のファイルの名前に<などの半角記号を可能な限り利用しないようにしてください。2021年10月7日の時点ではLinuxではKindle Previewer内のkindlegen.exeが壊れてしまいました。Windowsでも壊れる可能性が有ります。

 著者に'などの半角記号を可能な限り利用しないようにしてください。2021年10月7日の時点ではLinuxではKindle Previewer内のkindlegen.exeは'のXMLエスケープである&apos;に未対応のようです。Windowsでも未対応の可能性が有ります。

  • Twitterで共有
  • Facebookで共有
  • はてなブックマークでブックマーク

作者を応援しよう!

ハートをクリックで、簡単に応援の気持ちを伝えられます。(ログインが必要です)

応援したユーザー

応援すると応援コメントも書けます

Rustで電子書籍(ePub)を作成しよう! エリファス1810 @Eliphas1810

★で称える

この小説が面白かったら★をつけてください。おすすめレビューも書けます。

カクヨムを、もっと楽しもう

この小説のおすすめレビューを見る

この小説のタグ