ブラウザだけでHTMLとJavaScriptで電子書籍(ePub)を作成しよう!
エリファス1810
ブラウザだけでHTMLとJavaScriptで電子書籍(ePub)を作成しよう!
Githubでブラウザだけで電子書籍(ePub)を作成する(JavaScriptと)HTMLのソース コードをパブリック ドメインで公開しております。
マイクロソフトのBing検索エンジンで「github free-epub-maker」などで検索してみてください。
残念ながらグーグル検索エンジンでは検索できません。
「小説家になろう」にも投稿しています。
不具合が有るかもしれないので利用は自己責任でお願いいたします。
2024年2月1日時点の最新のChromeとFirefoxで動作を確認しました。
pagina EPUB-Checkerのチェックを通過しました。
コピペする場合は、2文字の全角空白を4文字の半角空白に、全角縦線を|に、置換してください。
ePubMaker.htmlなどの適当な名前の空からの.htmlファイルの内容にコピペしてから、UTF-8という文字コードで保存し、ブラウザで.htmlファイルを見ると、利用できます。
ePubMaker.html
――――――――――――――――――――
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Free ePub Maker by JavaScript in Browser Only(ブラウザだけでJavaScriptで無料で電子書籍を作成)</title>
<!-- This application is under the "Public Domain". -->
</head>
<body>
<div>
<input type="radio" name="language" id="en"></input>English
<input type="radio" name="language" id="ja" checked="checked"></input>日本語
</div>
<br />
<div>
<input type="radio" name="horizontalAndVerticalWriting" id="horizontalWriting" checked="checked"></input><span id="horizontalWritingLabel">横書き</span>
<input type="radio" name="horizontalAndVerticalWriting" id="verticalWriting"></input><span id="verticalWritingLabel">縦書き</span>
</div>
<br />
<div>
<span id="filesLabel">テキストファイル(と画像ファイル)を選択</span> <input type="file" id="file" accept=".txt, .jpeg, .jpg, .png" multiple />
</div>
<br />
<div>
<span id="titleLabel">電子書籍のタイトル</span> <input type="text" id="title" />
</div>
<br />
<div>
<span id="authorLabel">著者</span> <input type="text" id="author" />
</div>
<br />
<div>
<button type="button" id="makeEPub">電子書籍(ePub)を作成</button>
</div>
<br />
<p id="message"></p>
<br />
<br />
<p>From selected one or more text files( and image files), this application makes ePub file.</p>
<p>This application supports .txt, .jpeg or .jpg and .png.</p>
<br />
<p>By the name inputted in the "Book Title", this application make the .epub file.</p>
<br />
<p>One or more numeric characters and a space character of the head of each text file name are deleted, and the name without the filename extension is used as a title of a episode.</p>
<p>For example, the "Episode 1" of "0001 Episode 1.txt" is used as a title of a episode.</p>
<br />
<p>This application sorts text files in alphabetical order of the text file name.</p>
<p>So, like, for example, "0001 Episode1.txt" and "0002 Episode2.txt" and so on, by the rule, "TheEpisodeNumber TheTitleOfTheEpisode.txt", each text file name must be named.</p>
<br />
<p>The character code of each text file must be UTF-8.</p>
<br />
<p>Each image file name is used as the "id" attribute value of XHTML tag.</p>
<p>So, character types of each image file name must be alphabetic characters and numeric characters and the hyphen(-) and the underscore(_).</p>
<p>And, the character type of the first character of each image file name must be a alphabetic character.</p>
<br />
<p>Vertically long image files are recommended.</p>
<br />
<p>If "cover.jpg" or "cover.jpeg" or "cover.png" exists, the image file is used as the cover image of the ePub file to be made.</p>
<br />
<p>If a line in each text file ends ".jpg" or ".jpeg" or ".png", the text of the line is considered as the specification of the image file and converted to the HTML img tag.</p>
<p>For example, "episode1image1.png" is converted to "<img id="episode1image1" src="episode1image1.png" />".</p>
<br />
<p>If a line in each text file is "Kanji(HiraganaOrKatakana)", it is converted to the HTML ruby tag.</p>
<br />
<p>If a line in each text file is "全角縦線Text《Ruby》", it is converted to the HTML ruby tag.</p>
<p>The "全角縦線" of the above is the Full-width Vertical Line.</p>
<p>For example, "全角縦線Text《Ruby》" is converted to "<ruby>Text<rt>Ruby</rt></ruby>".</p>
<br />
<p>If a line in each text file is "Kanji《HiraganaOrKatakana》", it is converted to the HTML ruby tag.</p>
<br />
<p>A empty line in each text file is converted to "<p> </p>".</p>
<p>The " " of the above is the Full-width Space Character.</p>
<br />
<p>In each runtime, a new version 4 UUID is generated.</p>
<p>The name inputted in "Author" is used as the author and the publisher of the ePub.</p>
<p>The date and time of the runtime is used as the "date and time of publication" and the "last modified date and time" of ePub.</p>
<p></p>
<p>At 3/13/2021, if text and the image of "height: 100%;" are mixed, the "Kindle" on "FireHD8Plus" did not show the image.</p>
<p>So, this application set "height: 90%;" to HTML img tags.</p>
<br />
<p>This application does not run on some server.</p>
<p>The browser on your computer or smartphone or some device runs this application.</p>
<p>This application runs in offline.</p>
<br />
<br />
<br />
<br />
<p> 選択した.txtファイル(と.jpgファイル、.jpegファイル、.pngファイル)から電子書籍(ePub)を作成します。</p>
<br />
<p> 「電子書籍のタイトル」に指定した名前で電子書籍の.epubファイルを作成します。</p>
<p> 例えば、「電子書籍のタイトル」に「正法眼蔵の現代語訳」を指定した場合は、「正法眼蔵の現代語訳.epub」という名前のファイルを作成します。</p>
<br />
<p> .txtファイル名の先頭から、半角数字の連続と半角空白を除去した名前を、各話の見出しにします。</p>
<p> 例えば、「0001 第一話.txtファイル」が存在する場合は「第一話」を見出しにします。</p>
<br />
<p> .txtファイル名の文字順で並べるので、例えば「0001 第一話.txt」、「0002 第二話.txt」のように、「半角数字の番号 順番の各話の見出し.txt」といった規則で.txtファイル名をつけてください。</p>
<br />
<p> .txtファイルの文字コードはUTF-8にしてください。</p>
<br />
<p> .jpgファイル名、.jpegファイル名、.pngファイル名は、XHTMLのidとして利用するため、半角英数字と半角ハイフン(-)と半角下線(_)による名前にしてください。</p>
<p> .jpgファイル名、.jpegファイル名、.pngファイル名は、XHTMLのidとして利用するため、最初の文字は半角英字にしてください。</p>
<br />
<p> 縦長の画像を用意してください。</p>
<br />
<p> 「cover.jpg」ファイルか「cover.jpeg」ファイルか「cover.png」ファイルを電子書籍(ePub)の表紙画像にします。</p>
<br />
<p> .txtファイル内の、ある行が「.jpg」か「.jpeg」か「.png」で終わる場合は、画像ファイル名の指定とみなして、HTMLのimgタグに置換します。</p>
<p> 例えば、ある行が「episode1image1.png」の場合は、「<img id="episode1image1" src="episode1image1.png" />」に置換します。</p>
<br />
<p> 漢字 半角括弧開き ひらがなかカタカナ 半角括弧閉じ を「<ruby>漢字<rt>ひらがなかカタカナ</rt></ruby>」に置換します。</p>
<br />
<p> 「全角縦線テキスト《ルビ》」を「<ruby>テキスト<rt>ルビ</rt></ruby>」に置換します。</p>
<p> 上記の「全角縦線」は全角縦線です。</p>
<p> 半角縦線(|)の場合は、HTMLのrubyタグに置換しません。</p>
<br />
<p> 漢字 二重山括弧開き ひらがなかカタカナ 二重山括弧閉じ を「<ruby>漢字<rt>ひらがなかカタカナ</rt></ruby>」に置換します。</p>
<br />
<p> 空行を「<p> </p>」に置換します。</p>
<p> 上記の「 」は全角空白です。</p>
<br />
<p> 電子書籍(ePub)を作成するたびに新たにバージョン4のUUIDを生成します。</p>
<p> 記入された著者を著者と出版者とします。</p>
<p> 実行された日時を最終更新日時と出版日とします。</p>
<br />
<p> 2021年3月13日時点で、FireHD8PlusタブレットのKindleアプリは文字とheight: 100%;の画像が混在していると画像を表示してくれない事が有るので、画像とheight: 90%;に設定しています。</p>
<br />
<p> 何らかのサーバー上で動作する訳ではありません。</p>
<p> あなたのパソコンやスマホなどのブラウザーが動作させます。</p>
<p> オフラインでも動きます。</p>
<script>
//In HTML escape, 'must be converted to ' .
function escapeHtml(string) {
string = string.replaceAll(/&/g, "&");
string = string.replaceAll(/"/g, """);
string = string.replaceAll(/'/g, "'");
string = string.replaceAll(/</g, "<");
string = string.replaceAll(/>/g, ">");
return string;
}
//In XML escape, 'must be converted to ' .
function escapeXml(string) {
string = string.replaceAll(/&/g, "&");
string = string.replaceAll(/"/g, """);
string = string.replaceAll(/'/g, "'");
string = string.replaceAll(/</g, "<");
string = string.replaceAll(/>/g, ">");
return string;
}
function $(id) {
return document.getElementById(id);
}
function readAsArrayBufferSync(file) {
return new Promise(function (resolve, reject) {
var fileReader = new FileReader();
fileReader.onload = function () { resolve(fileReader.result); };
fileReader.onerror = function () { reject(fileReader.error); };
fileReader.readAsArrayBuffer(file);
});
}
function readAsTextSync(file) {
return new Promise(function (resolve, reject) {
var fileReader = new FileReader();
fileReader.onload = function () { resolve(fileReader.result); };
fileReader.onerror = function () { reject(fileReader.error); };
fileReader.readAsText(file);
});
}
function uuidVersion4() {
//128bit = 16Byteの乱数を生成
var uuidVersion4Uint8Array = new Uint8Array(16);
crypto.getRandomValues(uuidVersion4Uint8Array);
//RFC4122のvariantを表すために、9バイト目の2進数の4の位に0を、8の位に1を設定
//2進数の下2桁は乱数のまま
uuidVersion4Uint8Array[8] = (uuidVersion4Uint8Array[8] & 0x3F) | 0x80;
//7バイト目の2進数の下4桁であるUUIDのバージョン ビットにバージョン4の4を設定
uuidVersion4Uint8Array[6] = (uuidVersion4Uint8Array[6] & 0x0F) | 0x40;
var uuidVersion4String = "";
for (var index = 0; index < uuidVersion4Uint8Array.length; index++) {
uuidVersion4String = uuidVersion4String + uuidVersion4Uint8Array[index].toString(16).toUpperCase();
switch (index) {
case 3:
case 5:
case 7:
case 9:
uuidVersion4String = uuidVersion4String + "-";
break;
default:
}
}
return uuidVersion4String;
}
function crc32(array) {
/*
var crc32Table = [];
for (var index = 0; index < 256; index++) {
var output = index;
for (var bit = 0; bit < 8; bit++) {
if ((output & 0x1) == 0) {
output = output >>> 1;
} else {
output = (output >>> 1) ^ 0xEDB88320;
}
}
crc32Table.push(output >>> 0);
}
*/
var crc32Table = [
0x00000000,
0x77073096,
0xEE0E612C,
0x990951BA,
0x076DC419,
0x706AF48F,
0xE963A535,
0x9E6495A3,
0x0EDB8832,
0x79DCB8A4,
0xE0D5E91E,
0x97D2D988,
0x09B64C2B,
0x7EB17CBD,
0xE7B82D07,
0x90BF1D91,
0x1DB71064,
0x6AB020F2,
0xF3B97148,
0x84BE41DE,
0x1ADAD47D,
0x6DDDE4EB,
0xF4D4B551,
0x83D385C7,
0x136C9856,
0x646BA8C0,
0xFD62F97A,
0x8A65C9EC,
0x14015C4F,
0x63066CD9,
0xFA0F3D63,
0x8D080DF5,
0x3B6E20C8,
0x4C69105E,
0xD56041E4,
0xA2677172,
0x3C03E4D1,
0x4B04D447,
0xD20D85FD,
0xA50AB56B,
0x35B5A8FA,
0x42B2986C,
0xDBBBC9D6,
0xACBCF940,
0x32D86CE3,
0x45DF5C75,
0xDCD60DCF,
0xABD13D59,
0x26D930AC,
0x51DE003A,
0xC8D75180,
0xBFD06116,
0x21B4F4B5,
0x56B3C423,
0xCFBA9599,
0xB8BDA50F,
0x2802B89E,
0x5F058808,
0xC60CD9B2,
0xB10BE924,
0x2F6F7C87,
0x58684C11,
0xC1611DAB,
0xB6662D3D,
0x76DC4190,
0x01DB7106,
0x98D220BC,
0xEFD5102A,
0x71B18589,
0x06B6B51F,
0x9FBFE4A5,
0xE8B8D433,
0x7807C9A2,
0x0F00F934,
0x9609A88E,
0xE10E9818,
0x7F6A0DBB,
0x086D3D2D,
0x91646C97,
0xE6635C01,
0x6B6B51F4,
0x1C6C6162,
0x856530D8,
0xF262004E,
0x6C0695ED,
0x1B01A57B,
0x8208F4C1,
0xF50FC457,
0x65B0D9C6,
0x12B7E950,
0x8BBEB8EA,
0xFCB9887C,
0x62DD1DDF,
0x15DA2D49,
0x8CD37CF3,
0xFBD44C65,
0x4DB26158,
0x3AB551CE,
0xA3BC0074,
0xD4BB30E2,
0x4ADFA541,
0x3DD895D7,
0xA4D1C46D,
0xD3D6F4FB,
0x4369E96A,
0x346ED9FC,
0xAD678846,
0xDA60B8D0,
0x44042D73,
0x33031DE5,
0xAA0A4C5F,
0xDD0D7CC9,
0x5005713C,
0x270241AA,
0xBE0B1010,
0xC90C2086,
0x5768B525,
0x206F85B3,
0xB966D409,
0xCE61E49F,
0x5EDEF90E,
0x29D9C998,
0xB0D09822,
0xC7D7A8B4,
0x59B33D17,
0x2EB40D81,
0xB7BD5C3B,
0xC0BA6CAD,
0xEDB88320,
0x9ABFB3B6,
0x03B6E20C,
0x74B1D29A,
0xEAD54739,
0x9DD277AF,
0x04DB2615,
0x73DC1683,
0xE3630B12,
0x94643B84,
0x0D6D6A3E,
0x7A6A5AA8,
0xE40ECF0B,
0x9309FF9D,
0x0A00AE27,
0x7D079EB1,
0xF00F9344,
0x8708A3D2,
0x1E01F268,
0x6906C2FE,
0xF762575D,
0x806567CB,
0x196C3671,
0x6E6B06E7,
0xFED41B76,
0x89D32BE0,
0x10DA7A5A,
0x67DD4ACC,
0xF9B9DF6F,
0x8EBEEFF9,
0x17B7BE43,
0x60B08ED5,
0xD6D6A3E8,
0xA1D1937E,
0x38D8C2C4,
0x4FDFF252,
0xD1BB67F1,
0xA6BC5767,
0x3FB506DD,
0x48B2364B,
0xD80D2BDA,
0xAF0A1B4C,
0x36034AF6,
0x41047A60,
0xDF60EFC3,
0xA867DF55,
0x316E8EEF,
0x4669BE79,
0xCB61B38C,
0xBC66831A,
0x256FD2A0,
0x5268E236,
0xCC0C7795,
0xBB0B4703,
0x220216B9,
0x5505262F,
0xC5BA3BBE,
0xB2BD0B28,
0x2BB45A92,
0x5CB36A04,
0xC2D7FFA7,
0xB5D0CF31,
0x2CD99E8B,
0x5BDEAE1D,
0x9B64C2B0,
0xEC63F226,
0x756AA39C,
0x026D930A,
0x9C0906A9,
0xEB0E363F,
0x72076785,
0x05005713,
0x95BF4A82,
0xE2B87A14,
0x7BB12BAE,
0x0CB61B38,
0x92D28E9B,
0xE5D5BE0D,
0x7CDCEFB7,
0x0BDBDF21,
0x86D3D2D4,
0xF1D4E242,
0x68DDB3F8,
0x1FDA836E,
0x81BE16CD,
0xF6B9265B,
0x6FB077E1,
0x18B74777,
0x88085AE6,
0xFF0F6A70,
0x66063BCA,
0x11010B5C,
0x8F659EFF,
0xF862AE69,
0x616BFFD3,
0x166CCF45,
0xA00AE278,
0xD70DD2EE,
0x4E048354,
0x3903B3C2,
0xA7672661,
0xD06016F7,
0x4969474D,
0x3E6E77DB,
0xAED16A4A,
0xD9D65ADC,
0x40DF0B66,
0x37D83BF0,
0xA9BCAE53,
0xDEBB9EC5,
0x47B2CF7F,
0x30B5FFE9,
0xBDBDF21C,
0xCABAC28A,
0x53B39330,
0x24B4A3A6,
0xBAD03605,
0xCDD70693,
0x54DE5729,
0x23D967BF,
0xB3667A2E,
0xC4614AB8,
0x5D681B02,
0x2A6F2B94,
0xB40BBE37,
0xC30C8EA1,
0x5A05DF1B,
0x2D02EF8D
];
var output = 0xFFFFFFFF;
for (var index = 0; index < array.length; index++) {
output = ((output >>> 8) ^ crc32Table[array[index] ^ (output & 0xFF)]) >>> 0; //Firefoxが浮動小数点数と誤解しないように>>> 0
}
return (~output) >>> 0; //Firefoxが浮動小数点数と誤解しないように>>> 0
}
function StoredZip() {
this.fileArray = [];
this.add = function (file) {
this.fileArray.push(file);
};
this.zip = async function () {
var fileArray = this.fileArray;
var numberOfFiles = fileArray.length;
var date = new Date();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
var zipTime = (hours << 11) + (minutes << 5) + Math.floor(seconds / 2);
var year = date.getFullYear();
var month = (date.getMonth() + 1); //1以上12以下
var dayOfMonth = date.getDate();
var zipDate = ((year - 1980) << 9) + (month << 5) + dayOfMonth;
//zipのローカル ファイル ヘッダーは30バイト + ファイル名のサイズ + ファイルの内容のサイズ
//zipのセントラル ディレクトリ ヘッダーは46バイト + ファイル名のサイズ
//zipのセントラル ディレクトリの終端レコードは22バイト
var zipLocalFileHeaderStartIndex = 0;
var zipCentralDirectoryHeaderTotalSize = 0;
var zipFileSize = 22;
for (var fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {
var file = fileArray[fileIndex];
file.zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex;
var fileName = file.name;
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
var fileNameByteArray = new TextEncoder("utf-8").encode(fileName);
var fileNameSize = fileNameByteArray.length;
//ファイルの場合
if (fileName.endsWith("/") == false) {
var fileContentArrayBuffer = await readAsArrayBufferSync(file);
var fileContentSize = fileContentArrayBuffer.byteLength;
zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex + 30 + fileNameSize + fileContentSize;
zipCentralDirectoryHeaderTotalSize = zipCentralDirectoryHeaderTotalSize + 46 + fileNameSize;
zipFileSize = zipFileSize + 76 + (fileNameSize * 2) + fileContentSize;
//ディレクトリの場合
} else {
zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex + 30 + fileNameSize;
zipCentralDirectoryHeaderTotalSize = zipCentralDirectoryHeaderTotalSize + 46 + fileNameSize;
zipFileSize = zipFileSize + 76 + (fileNameSize * 2);
}
}
var zipCentralDirectoryHeaderStartIndex = zipLocalFileHeaderStartIndex;
var arrayBuffer = new ArrayBuffer(zipFileSize);
var dataView = new DataView(arrayBuffer);
var byteIndex = 0;
for (var fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {
var file = fileArray[fileIndex];
var fileName = file.name;
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
var fileNameByteArray = new TextEncoder("utf-8").encode(fileName);
var fileNameSize = fileNameByteArray.length;
var fileContentArrayBuffer = await readAsArrayBufferSync(file);
var fileContentSize = fileContentArrayBuffer.byteLength;
var fileContentByteArray = new Uint8Array(fileContentArrayBuffer);
var fileContentCrc32 = crc32(fileContentByteArray);
//zipのローカル ファイル ヘッダー
var zipLocalFileHeader = [
//ローカル ファイル ヘッダーを表す固定値
0x50,
0x4B,
0x03,
0x04,
//当zipの展開に必要な最小バージョン
//
//バージョン1.0は16進数表記で順に0A、00。
//
0x0A,
0x00,
//当zipの汎用ビット フラグ
//
//当zip内のファイル名の文字コードがUTF-8の場合は16進数表記で順に00、08。
//
0x00,
0x08,
//当zipの圧縮方法
//
//無圧縮のzipのSTOREDは16進数表記で順に00、00。
//
0x00,
0x00,
//当zip内の当ファイルの最終更新時刻
((zipTime << 8) >>> 8),
(zipTime >>> 8),
//当zip内の当ファイルの最終更新日
((zipDate << 8) >>> 8),
(zipDate >>> 8),
//当zip内の当ファイルの内容のCRC32
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentCrc32 << 24) >>> 24),
((fileContentCrc32 << 16) >>> 24),
((fileContentCrc32 << 8) >>> 24),
(fileContentCrc32 >>> 24),
//圧縮後の当zip内の当ファイルのファイル サイズ
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentSize << 24) >>> 24),
((fileContentSize << 16) >>> 24),
((fileContentSize << 8) >>> 24),
(fileContentSize >>> 24),
//圧縮前の当zip内の当ファイルのファイル サイズ
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentSize << 24) >>> 24),
((fileContentSize << 16) >>> 24),
((fileContentSize << 8) >>> 24),
(fileContentSize >>> 24),
//当zip内の当ファイルの名前のサイズ
((fileNameSize << 8) >>> 8),
(fileNameSize >>> 8),
//zipのエクストラ フィールド
0x00,
0x00
];
//当zip内の当ファイルの名前
//
//ディレクトリの場合はディレクトリ名 + 半角スラッシュ記号(/)
//
//ディレクトリ内のファイルの場合はディレクトリ名 + 半角スラッシュ記号(/) + ファイル名
//
for (var index = 0; index < fileNameByteArray.length; index++) {
zipLocalFileHeader.push(fileNameByteArray[index]);
}
//当zip内の当ファイルの内容
//
//ディレクトリの場合は無し
//
//ファイルの場合
//
//ファイルの名前の最後の文字が半角スラッシュ記号ではない場合
//
if (fileName.endsWith("/") == false) {
for (var index = 0; index < fileContentByteArray.length; index++) {
zipLocalFileHeader.push(fileContentByteArray[index]);
}
}
for (var index = 0; index < zipLocalFileHeader.length; index++) {
dataView.setUint8(byteIndex, zipLocalFileHeader[index], /* リトル エンディアン */ true);
byteIndex++;
}
}
for (var fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {
var file = fileArray[fileIndex];
var zipLocalFileHeaderStartIndex = file.zipLocalFileHeaderStartIndex;
var fileName = file.name;
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
var fileNameByteArray = new TextEncoder("utf-8").encode(fileName);
var fileNameSize = fileNameByteArray.length;
var fileContentArrayBuffer = await readAsArrayBufferSync(file);
var fileContentSize = fileContentArrayBuffer.byteLength;
var fileContentByteArray = new Uint8Array(fileContentArrayBuffer);
var fileContentCrc32 = crc32(fileContentByteArray);
//zipのセントラル ディレクトリ ヘッダー
var zipCentralDirectoryHeader = [
//セントラル ディレクトリ ヘッダーを表す固定値
0x50,
0x4B,
0x01,
0x02,
//当zipを作成したアプリケーションが対応可能なzipのバージョン
0x0A,
//当zipを作成したOS
//
//Unixは16進数表記で03
//
0x03,
//当zipの展開に必要な最小バージョン
//
//バージョン1.0は16進数表記で順に0A、00。
//
0x0A,
0x00,
//当zipの汎用ビット フラグ
//
//当zip内のファイル名の文字コードがUTF-8の場合は16進数表記で順に00、08。
//
0x00,
0x08,
//当zipの圧縮方法
//
//無圧縮のzipのSTOREDは16進数表記で順に00、00。
//
0x00,
0x00,
//当zip内の当ファイルの最終更新時刻
((zipTime << 8) >>> 8),
(zipTime >>> 8),
//当zip内の当ファイルの最終更新日
((zipDate << 8) >>> 8),
(zipDate >>> 8),
//当zip内の当ファイルの内容のCRC32
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentCrc32 << 24) >>> 24),
((fileContentCrc32 << 16) >>> 24),
((fileContentCrc32 << 8) >>> 24),
(fileContentCrc32 >>> 24),
//圧縮後の当zip内の当ファイルのファイル サイズ
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentSize << 24) >>> 24),
((fileContentSize << 16) >>> 24),
((fileContentSize << 8) >>> 24),
(fileContentSize >>> 24),
//圧縮前の当zip内の当ファイルのファイル サイズ
//
//ディレクトリの場合は16進数表記で順に00、00、00、00。
//
((fileContentSize << 24) >>> 24),
((fileContentSize << 16) >>> 24),
((fileContentSize << 8) >>> 24),
(fileContentSize >>> 24),
//当zip内の当ファイルの名前のサイズ
((fileNameSize << 8) >>> 8),
(fileNameSize >>> 8),
//zipのエクストラ フィールド
0x00,
0x00,
//zipのファイルのコメントのサイズ
//
//zipのファイルのコメントを利用しない場合は16進数表記で順に00、00。
//
0x00,
0x00,
//対応するローカル ファイル ヘッダーが存在するディスクの番号
//
//ディスク分割していない場合は16進数表記で順に00、00。
//
0x00,
0x00,
//(zip)内的ファイル属性
//
//画像などのバイナリーデータの場合は16進数表記で順に00、00。
//
//画像などのテキスト データの場合は16進数表記で順に01、00。
//
0x00,
0x00,
//当zipを作成したOS依存の(zip)外的ファイル属性
0x00,
0x00,
0x00,
0x00,
//対応するローカル ファイル ヘッダーの開始バイトのインデックス番号
((zipLocalFileHeaderStartIndex << 24) >>> 24),
((zipLocalFileHeaderStartIndex << 16) >>> 24),
((zipLocalFileHeaderStartIndex << 8) >>> 24),
(zipLocalFileHeaderStartIndex >>> 24)
];
//当zip内の当ファイルの名前
//
//ディレクトリの場合はディレクトリ名 + 半角スラッシュ記号(/)
//
//ディレクトリ内のファイルの場合はディレクトリ名 + 半角スラッシュ記号(/) + ファイル名
//
for (var index = 0; index < fileNameByteArray.length; index++) {
zipCentralDirectoryHeader.push(fileNameByteArray[index]);
}
for (var index = 0; index < zipCentralDirectoryHeader.length; index++) {
dataView.setUint8(byteIndex, zipCentralDirectoryHeader[index], /* リトル エンディアン */ true);
byteIndex++;
}
}
//zipのセントラル ディレクトリの終端レコード
var zipEndOfCentralDirectoryRecord = [
//セントラル ディレクトリの終端レコードを表す固定値
0x50,
0x4B,
0x05,
0x06,
//当ディスクの番号
//
//ディスク分割していない場合は16進数表記で順に00、00。
//
0x00,
0x00,
//セントラル ディレクトリが開始されるディスクの番号
//
//ディスク分割していない場合は16進数表記で順に00、00。
//
0x00,
0x00,
//(当ディスクに存在する)セントラル ディレクトリの総数
((numberOfFiles << 8) >>> 8),
(numberOfFiles >>> 8),
//セントラル ディレクトリの総数
((numberOfFiles << 8) >>> 8),
(numberOfFiles >>> 8),
//セントラル ディレクトリの総サイズ
((zipCentralDirectoryHeaderTotalSize << 24) >>> 24),
((zipCentralDirectoryHeaderTotalSize << 16) >>> 24),
((zipCentralDirectoryHeaderTotalSize << 8) >>> 24),
(zipCentralDirectoryHeaderTotalSize >>> 24),
//セントラル ディレクトリの開始バイトのインデックス番号
((zipCentralDirectoryHeaderStartIndex << 24) >>> 24),
((zipCentralDirectoryHeaderStartIndex << 16) >>> 24),
((zipCentralDirectoryHeaderStartIndex << 8) >>> 24),
(zipCentralDirectoryHeaderStartIndex >>> 24),
//当zipのコメントのサイズ
0x00,
0x00
];
for (var index = 0; index < zipEndOfCentralDirectoryRecord.length; index++) {
dataView.setUint8(byteIndex, zipEndOfCentralDirectoryRecord[index], /* リトル エンディアン */ true);
byteIndex++;
}
return new Uint8Array(arrayBuffer);
};
}
$("en").onclick = function () {
if ($("en").checked) {
$("horizontalWritingLabel").innerHTML = "Horizontal Writing";
$("verticalWritingLabel").innerHTML = "Vertical Writing (* Not Recommended for English)";
$("filesLabel").innerHTML = "Select One or More Text Files( and Image Files)";
$("titleLabel").innerHTML = "Book Title";
$("authorLabel").innerHTML = "Author";
$("makeEPub").innerHTML = "Make ePub";
$("horizontalWriting").checked = true;
}
};
$("ja").onclick = function () {
if ($("ja").checked) {
$("horizontalWritingLabel").innerHTML = "横書き";
$("verticalWritingLabel").innerHTML = "縦書き";
$("filesLabel").innerHTML = "1つ以上のテキストファイル(と画像ファイル)を選択";
$("titleLabel").innerHTML = "電子書籍のタイトル";
$("authorLabel").innerHTML = "著者";
$("makeEPub").innerHTML = "電子書籍(ePub)を作成";
}
};
$("makeEPub").onclick = async function () {
$("message").innerHTML = "";
var title = $("title").value;
var author = $("author").value;
var isJa = $("ja").checked;
var isVerticalWriting = $("verticalWriting").checked;
if (title == "") {
if (isJa) {
$("message").innerHTML = "電子書籍のタイトルを記入してください。";
} else {
$("message").innerHTML = "Could you input the book title?";
}
return;
}
if (author == "") {
if (isJa) {
$("message").innerHTML = "著者を記入してください。";
} else {
$("message").innerHTML = "Could you input the author?";
}
return;
}
//For the "Date and Time of Publication" and the "Last Modified Date and Time" in the ".opf" file.
//The milli seconds must be deleted.
var epubMakeDateTimeXml = new Date().toISOString().replace(/\.[0-9]{3}/g, "");
var textFileArray = [];
var imageFileArray = [];
var coverImageFile = null;
var files = $("file").files;
for (var index = 0; index < files.length; index++) {
var file = files[index];
var name = file.name;
var type = file.type;
if (type == "text/plain") {
textFileArray.push(file);
} else if (type == "image/jpeg" || type == "image/png") {
imageFileArray.push(file);
if (name.match(/^cover\.jpe?g$|^cover\.png$/gi) != null) {
coverImageFile = file;
}
}
}
var compare = function (a, b) {
if (a.name == b.name) {
return 0;
}
if (a.name < b.name) {
return -1;
}
return 1;
};
textFileArray.sort(compare);
imageFileArray.sort(compare);
if (textFileArray.length == 0) {
if (isJa) {
$("message").innerHTML = ".txtファイルを1つ以上、選択してください。";
} else {
$("message").innerHTML = 'Could you select one or more text files?';
}
return;
}
var storedZip = new StoredZip();
var mimetypeBlob = new Blob([new TextEncoder("utf-8").encode("application/epub+zip")]);
mimetypeBlob.name = "mimetype";
storedZip.add(mimetypeBlob);
var containerXmlBlob = new Blob([new TextEncoder("utf-8").encode([
'<?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'
].join("\n"))]);
containerXmlBlob.name = "META-INF/container.xml";
storedZip.add(containerXmlBlob);
var opfArray = [
'<?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/">',
' <dc:identifier id="pub-id">urn:uuid:' + escapeXml(uuidVersion4()) + '</dc:identifier>',
' <dc:title>' + escapeXml(title) + '</dc:title>'
];
if (isJa) {
opfArray.push(' <dc:language>ja</dc:language>');
} else {
opfArray.push(' <dc:language>en</dc:language>');
}
opfArray.push(' <meta property="dcterms:modified">' + epubMakeDateTimeXml + '</meta>');
opfArray.push('');
opfArray.push(' <dc:creator id="creator1">' + escapeXml(author) + '</dc:creator>');
opfArray.push(' <meta refines="#creator1" property="role" scheme="marc:relators" id="role">aut</meta>');
opfArray.push('');
opfArray.push(' <dc:publisher>' + escapeXml(author) + '</dc:publisher>');
opfArray.push(' <dc:date>' + epubMakeDateTimeXml + '</dc:date>');
opfArray.push(' </metadata>');
opfArray.push(' <manifest>');
opfArray.push(' <item id="css" href="common.css" media-type="text/css"/>');
opfArray.push(' <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>');
//If "cover.jpg" or "cover.jpeg" or "cover.png" exists,
if (coverImageFile != null) {
opfArray.push(' <item id="cover" href="' + coverImageFile.name + '" media-type="' + coverImageFile.type + '" properties="cover-image"/>');
}
//The following is repeated the number of text files.
for (var index = 0; index < textFileArray.length; index++) {
opfArray.push(' <item id="index' + (index + 1) + '" href="index' + (index + 1) + '.xhtml" media-type="application/xhtml+xml"/>');
}
//The following is repeated the number of image files.
for (var index = 0; index < imageFileArray.length; index++) {
var name = imageFileArray[index].name;
//The "cover.jpg" or "cover.jpeg" or "cover.png" is excluded.
if (name.match(/^cover\.jpe?g$|^cover\.png$/gi) == null) {
var type = imageFileArray[index].type;
opfArray.push(' <item id="' + escapeXml(name.replace(/\.jpe?g$|\.png$/gi, "")) + '" href="' + escapeXml(name) + '" media-type="' + type + '"/>');
}
}
opfArray.push(' </manifest>');
if (isVerticalWriting) {
opfArray.push(' <spine page-progression-direction="rtl">');
} else {
opfArray.push(' <spine>');
}
//The following is repeated the number of text files.
for (var index = 0; index < textFileArray.length; index++) {
opfArray.push(' <itemref idref="index' + (index + 1) + '"/>');
}
opfArray.push(' </spine>');
opfArray.push('</package>\n');
var opfBlob = new Blob([new TextEncoder("utf-8").encode(opfArray.join("\n"))]);
opfBlob.name = "opf.opf";
storedZip.add(opfBlob);
if (isVerticalWriting) {
var cssBlob = new Blob([new TextEncoder("utf-8").encode([
"html {",
" -webkit-writing-mode: vertical-rl;",
" -webkit-text-orientation: mixed;",
"",
" -epub-writing-mode: vertical-rl;",
" -epub-text-orientation: mixed;",
"",
" writing-mode: vertical-rl;",
" text-orientation: mixed;",
"}",
"",
"h1 {",
" margin: 0;",
" margin-top: 2rem;",
" margin-left: 2rem;",
" font-size: 2rem;",
" font-weight: bold;",
"}",
"",
"h2 {",
" margin: 0;",
" margin-top: 1.5rem;",
" margin-left: 1.5rem;",
" page-break-before: always;",
" font-size: 1.5rem;",
" font-weight: bold;",
"}",
"",
"h2#episode1 {",
" page-break-before: avoid;",
"}",
"",
"p {",
" margin: 0;",
"}",
"",
"img {",
" display: block;",
" height: 90%;",
" margin: auto;",
"}\n"
].join("\n"))]);
cssBlob.name = "common.css";
storedZip.add(cssBlob);
} else {
var cssBlob = new Blob([new TextEncoder("utf-8").encode([
"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"
].join("\n"))]);
cssBlob.name = "common.css";
storedZip.add(cssBlob);
}
var navArray = [
'<?xml version="1.0" encoding="UTF-8"?>'
];
if (isJa) {
navArray.push('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="ja" xml:lang="ja">');
} else {
navArray.push('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en">');
}
navArray.push(' <head>');
if (isJa) {
navArray.push(' <title>目次</title>');
} else {
navArray.push(' <title>The Table of Contents</title>');
}
navArray.push(' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>');
navArray.push(' </head>');
navArray.push(' <body>');
navArray.push(' <nav epub:type="toc">');
navArray.push(' <ol>');
//The following is repeated the number of text files.
for (var index = 0; index < textFileArray.length; index++) {
var name = textFileArray[index].name;
navArray.push(' <li><a href="index' + (index + 1) + '.xhtml#episode' + (index + 1) + '">' + escapeHtml(name.replaceAll(/^[0-9]*[ ]*|\.txt$/gi, "")) + '</a></li>');
}
navArray.push(' </ol>');
navArray.push(' </nav>');
navArray.push(' </body>');
navArray.push('</html>');
var navBlob = new Blob([new TextEncoder("utf-8").encode(navArray.join("\n"))]);
navBlob.name = "nav.xhtml";
storedZip.add(navBlob);
//Image files is zipped.
//The following is repeated the number of image files.
for (var index = 0; index < imageFileArray.length; index++) {
var name = imageFileArray[index].name;
var data = await readAsArrayBufferSync(imageFileArray[index]);
var blob = new Blob([data]);
blob.name = name;
storedZip.add(blob);
}
//The following is repeated the number of text files.
for (var index = 0; index < textFileArray.length; index++) {
var name = textFileArray[index].name;
var indexXhtmlArray = [
'<?xml version="1.0" encoding="UTF-8"?>'
];
if (isJa) {
indexXhtmlArray.push('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="ja" xml:lang="ja">');
} else {
indexXhtmlArray.push('<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en">');
}
indexXhtmlArray.push(' <head>');
indexXhtmlArray.push(' <title>' + escapeHtml(title) + '</title>');
indexXhtmlArray.push(' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>');
indexXhtmlArray.push(' <link href="common.css" rel="stylesheet" type="text/css"/>');
indexXhtmlArray.push(' </head>');
indexXhtmlArray.push(' <body>');
if (index == 0) {
indexXhtmlArray.push(' <h1>' + escapeHtml(title) + '</h1>');
}
indexXhtmlArray.push(' <h2 id="episode' + (index + 1) + '">' + escapeHtml(name.replaceAll(/^[0-9]*[ ]*|\.txt$/gi, "")) + '</h2>');
var text = await readAsTextSync(textFileArray[index]);
text = text.replaceAll(/\r\n/g, "\n");
text = text.replaceAll(/\r/g, "\n");
var lineArray = text.split("\n");
for (var i = 0; i < lineArray.length; i++) {
var line = lineArray[i];
//If the readed line ends ".jpg" or "jpeg" or ".png",
if (line.match(/^.+\.jpe?g$|^.+\.png$/gi) != null) {
//the readed line is converted to HTML img tag.
indexXhtmlArray.push(' <img id="' + escapeHtml(line.replace(/\.jpe?g$|\.png$/gi, "")) + '" src="' + escapeHtml(line) + '" />');
//If the readed line is empty,
} else if (line == "") {
//<p> </p> is outputted.
// of the above is the Full-width Space Character.
indexXhtmlArray.push(' <p> </p>');
//If the readed line is the ordinary text,
} else {
//HTML escape
var string = escapeHtml(line);
//Kanji(HiraganaOrKatakana) is converted to <ruby>Kanji<rt>HiraganaOrKatakana</rt></ruby> .
string = string.replaceAll(/([一-鿋々]+)\(([ぁ-ゖァ-ヺー]+)\)/g, '<ruby>$1<rt>$2</rt></ruby>');
//全角縦線Text《Ruby》 is converted to <ruby>Text<rt>Ruby</rt></ruby> .
//全角縦線 is the "Full-width Vertical Line".
string = string.replaceAll(/全角縦線([^《]+)《([^》]+)》/g, '<ruby>$1<rt>$2</rt></ruby>');
//Kanji《HiraganaOrKatakana》 is converted to <ruby>Kanji<rt>HiraganaOrKatakana</rt></ruby> .
string = string.replaceAll(/([一-鿋々]+)《([ぁ-ゖァ-ヺー]+)》/g, '<ruby>$1<rt>$2</rt></ruby>');
indexXhtmlArray.push(' <p>' + string + '</p>');
}
}
indexXhtmlArray.push(' </body>');
indexXhtmlArray.push('</html>\n');
var blob = new Blob([new TextEncoder("utf-8").encode(indexXhtmlArray.join("\n"))]);
blob.name = "index" + (index + 1) + ".xhtml";
storedZip.add(blob);
}
var blob = new Blob([await storedZip.zip()], {type: "application/zip"});
var url = URL.createObjectURL(blob);
var aTagElement = document.createElement("a");
aTagElement.href = url;
aTagElement.download = title + ".epub";
aTagElement.click();
URL.revokeObjectURL(url);
if (isJa) {
$("message").innerHTML = "電子書籍(ePub)の作成が完了しました。";
} else {
$("message").innerHTML = "The makeing of the ePub file is completed.";
}
};
</script>
</body>
</html>
――――――――――――――――――――
.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>に置換します。
記入された著者を著者と出版者とします。
実行された日時を出版日とします。
2021年3月13日時点で、FireHD8PlusタブレットのKindleアプリは文字とheight: 100%;の画像が混在していると画像を表示してくれない事が有るので、height: 90%;に設定しています。
.epubファイルの名前と.epubファイル内のファイルの名前に<などの半角記号を可能な限り利用しないようにしてください。
著者に'などの半角記号を可能な限り利用しないようにしてください。
ブラウザだけでHTMLとJavaScriptで電子書籍(ePub)を作成しよう! エリファス1810 @Eliphas1810
★で称える
この小説が面白かったら★をつけてください。おすすめレビューも書けます。
カクヨムを、もっと楽しもう
カクヨムにユーザー登録すると、この小説を他の読者へ★やレビューでおすすめできます。気になる小説や作者の更新チェックに便利なフォロー機能もお試しください。
新規ユーザー登録(無料)簡単に登録できます
この小説のタグ
関連小説
アンドロイド スマホのアプリを作ろう!/エリファス1810
★0 エッセイ・ノンフィクション 完結済 10話
ビューワー設定
文字サイズ
背景色
フォント
組み方向
機能をオンにすると、画面の下部をタップする度に自動的にスクロールして読み進められます。
応援すると応援コメントも書けます