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

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

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

エラーが発生しました。

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

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

【モダンJavaScript版】【deflate圧縮版】電子書籍(ePub)を作成するJavaScript(とHTML)のコード

 ブラウザだけでHTMLとJavaScriptだけで、電子書籍(ePub)を作成できるコードです。


 電書ラボチェッカーでチェックできます。


 zip圧縮で普通であるdeflate圧縮で圧縮できます。


 パブリック ドメインです。


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


 ePubMaker.htmlなどの適当な名前の空の.htmlファイルの内容にコピペしてから、UTF-8という文字コードで保存し、ChromeやFirefoxといったブラウザで.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 "&lt;img id=&quot;episode1image1&quot; src=&quot;episode1image1.png&quot; /&gt;".</p>

    <br />

    <p>If a line in each text file is "Kanji(HiraganaOrKatakana)", it is converted to the HTML ruby tag.</p>

    <p>For example, "漢字&#40;ひらがなかカタカナ&#41;" is converted to "&lt;ruby&gt;漢字&lt;rt&gt;ひらがなかカタカナ&lt;/rt&gt;&lt;/ruby&gt;".</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 "&lt;ruby&gt;Text&lt;rt&gt;Ruby&lt;/rt&gt;&lt;/ruby&gt;".</p>

    <br />

    <p>If a line in each text file is "Kanji《HiraganaOrKatakana》", it is converted to the HTML ruby tag.</p>

    <p>For example, "漢字&#12298;ひらがなかカタカナ&#12299;" is converted to "&lt;ruby&gt;漢字&lt;rt&gt;ひらがなかカタカナ&lt;/rt&gt;&lt;/ruby&gt;".</p>

    <br />

    <p>A empty line in each text file is converted to "&lt;p&gt; &lt;/p&gt;".</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」の場合は、「&lt;img id=&quot;episode1image1&quot; src=&quot;episode1image1.png&quot; /&gt;」に置換します。</p>

    <br />

    <p> 「漢字&#40;ひらがなかカタカナ&#41;」を「&lt;ruby&gt;漢字&lt;rt&gt;ひらがなかカタカナ&lt;/rt&gt;&lt;/ruby&gt;」に置換します。</p>

    <br />

    <p> 「全角縦線テキスト《ルビ》」を「&lt;ruby&gt;テキスト&lt;rt&gt;ルビ&lt;/rt&gt;&lt;/ruby&gt;」に置換します。</p>

    <p> 上記の「全角縦線」は全角縦線です。</p>

    <p> 半角縦線(|)の場合は、HTMLのrubyタグに置換しません。</p>

    <br />

    <p> 「漢字&#12298;ひらがなかカタカナ&#12299;」を「&lt;ruby&gt;漢字&lt;rt&gt;ひらがなかカタカナ&lt;/rt&gt;&lt;/ruby&gt;」に置換します。</p>

    <br />

    <p> 空行を「&lt;p&gt; &lt;/p&gt;」に置換します。</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 &#39; .

const escapeHtml = (string) => {

  return string

    .replaceAll(/&/g, "&amp;")

    .replaceAll(/"/g, "&quot;")

    .replaceAll(/'/g, "&#39;")

    .replaceAll(/</g, "&lt;")

    .replaceAll(/>/g, "&gt;")

  ;

};



//In XML escape, 'must be converted to &apos; .

const escapeXml = (string) => {

  return string

    .replaceAll(/&/g, "&amp;")

    .replaceAll(/"/g, "&quot;")

    .replaceAll(/'/g, "&apos;")

    .replaceAll(/</g, "&lt;")

    .replaceAll(/>/g, "&gt;")

  ;

};



const $ = (id) => {

  return document.getElementById(id);

};



const uuidVersion4 = () => {


  //128bit = 16Byteの乱数を生成

  const 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;


  let uuidVersion4String = "";

  for (let 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;

};



const crc32 = (uint8Array) => {

  /*

  const crc32Table = [];

  for (let index = 0; index < 256; index++) {

    let output = index;

    for (let bit = 0; bit < 8; bit++) {

      if ((output & 0x1) == 0) {

        output = output >>> 1;

      } else {

        output = (output >>> 1) ^ 0xEDB88320;

      }

    }

    crc32Table.push(output >>> 0);

  }

  */

  const 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

  ];

  let output = 0xFFFFFFFF;

  for (let index = 0; index < uint8Array.length; index++) {

    output = ((output >>> 8) ^ crc32Table[uint8Array[index] ^ (output & 0xFF)]) >>> 0; //Firefoxが浮動小数点数と誤解しないように>>> 0

  }

  return (~output) >>> 0; //Firefoxが浮動小数点数と誤解しないように>>> 0

};



class DeflateZip {


  fileArray = [];


  //deflate圧縮

  async deflateFile(file) {

    //一般的なdeflate圧縮の場合は、new CompressionStream("deflate")だが、

    //zipファイル内のデータのdeflate圧縮の場合は、new CompressionStream("deflate-raw")

    const compressionStream = new CompressionStream("deflate-raw");


    //const blob = new Blob([object]);

    //const readableStream = await blob.stream().pipeThrough(compressionStream);


    const readableStream = await file.stream().pipeThrough(compressionStream);

    const response = new Response(readableStream);

    const arrayBuffer = await response.arrayBuffer();

    return arrayBuffer;

  }


  add(file) {

    this.fileArray.push(file);

  }


  async zip() {


    const fileArray = this.fileArray;


    const numberOfFiles = fileArray.length;


    const date = new Date();


    const hours = date.getHours();

    const minutes = date.getMinutes();

    const seconds = date.getSeconds();


    const zipTime = (hours << 11) + (minutes << 5) + Math.floor(seconds / 2);


    const year = date.getFullYear();

    const month = (date.getMonth() + 1); //1以上12以下

    const dayOfMonth = date.getDate();


    const zipDate = ((year - 1980) << 9) + (month << 5) + dayOfMonth;



    //zipのローカル ファイル ヘッダーは30バイト + ファイル名のサイズ + ファイルの内容のサイズ

    //zipのセントラル ディレクトリ ヘッダーは46バイト + ファイル名のサイズ

    //zipのセントラル ディレクトリの終端レコードは22バイト

    let zipLocalFileHeaderStartIndex = 0;

    let zipCentralDirectoryHeaderTotalSize = 0;

    let zipFileSize = 22;

    for (let fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {

      const file = fileArray[fileIndex];


      file.zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex;


      let fileName = file.name;

      if (fileName.startsWith("/")) {

        fileName = fileName.substring(1);

      }

      const fileNameByteArray = new TextEncoder("utf-8").encode(fileName);

      const fileNameSize = fileNameByteArray.length;


      //ファイルの場合

      if (fileName.endsWith("/") == false) {


        //const fileContentArrayBuffer = await file.arrayBuffer();

        //const fileContentSize = fileContentArrayBuffer.byteLength;


        const fileCompressedContentArrayBuffer = await this.deflateFile(file);

        const fileCompressedContentSize = fileCompressedContentArrayBuffer.byteLength;


        zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex + 30 + fileNameSize + fileCompressedContentSize;


        zipCentralDirectoryHeaderTotalSize = zipCentralDirectoryHeaderTotalSize + 46 + fileNameSize;


        zipFileSize = zipFileSize + 76 + (fileNameSize * 2) + fileCompressedContentSize;


      //ディレクトリの場合

      } else {


        zipLocalFileHeaderStartIndex = zipLocalFileHeaderStartIndex + 30 + fileNameSize;


        zipCentralDirectoryHeaderTotalSize = zipCentralDirectoryHeaderTotalSize + 46 + fileNameSize;


        zipFileSize = zipFileSize + 76 + (fileNameSize * 2);

      }

    }

    const zipCentralDirectoryHeaderStartIndex = zipLocalFileHeaderStartIndex;



    const arrayBuffer = new ArrayBuffer(zipFileSize);

    const dataView = new DataView(arrayBuffer);

    let byteIndex = 0;



    for (let fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {

      const file = fileArray[fileIndex];


      let fileName = file.name;

      if (fileName.startsWith("/")) {

        fileName = fileName.substring(1);

      }

      const fileNameByteArray = new TextEncoder("utf-8").encode(fileName);

      const fileNameSize = fileNameByteArray.length;


      const fileContentArrayBuffer = await file.arrayBuffer();

      const fileContentSize = fileContentArrayBuffer.byteLength;

      const fileContentByteArray = new Uint8Array(fileContentArrayBuffer);


      const fileContentCrc32 = crc32(fileContentByteArray);


      const fileCompressedContentArrayBuffer = await this.deflateFile(file);

      const fileCompressedContentSize = fileCompressedContentArrayBuffer.byteLength;

      const fileCompressedContentByteArray = new Uint8Array(fileCompressedContentArrayBuffer);


      //zipのローカル ファイル ヘッダー

      const zipLocalFileHeader = [


        //ローカル ファイル ヘッダーを表す固定値

        0x50,

        0x4B,

        0x03,

        0x04,


        //当zipの展開に必要な最小バージョン

        //

        //無圧縮のSTOREDのバージョン1.0は16進数表記で順に0A、00。

        //deflate圧縮可能なバージョン2.0は16進数表記で順に14、00。

        //

        0x14,

        0x00,


        //当zipの汎用ビット フラグ

        //

        //当zip内のファイル名の文字コードがUTF-8の場合は16進数表記で順に00、08。

        //

        0x00,

        0x08,


        //当zipの圧縮方法

        //

        //無圧縮のzipのSTOREDは16進数表記で順に00、00。

        //deflate圧縮は16進数表記で順に08、00。

        //

        0x08,

        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。

        //

        ((fileCompressedContentSize << 24) >>> 24),

        ((fileCompressedContentSize << 16) >>> 24),

        ((fileCompressedContentSize << 8) >>> 24),

        (fileCompressedContentSize >>> 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 (let index = 0; index < fileNameByteArray.length; index++) {

        zipLocalFileHeader.push(fileNameByteArray[index]);

      }

      //当zip内の当ファイルの内容

      //

      //ディレクトリの場合は無し

      //

      //ファイルの場合

      //

      //ファイルの名前の最後の文字が半角スラッシュ記号ではない場合

      //

      if (fileName.endsWith("/") == false) {

        for (let index = 0; index < fileCompressedContentByteArray.length; index++) {

          zipLocalFileHeader.push(fileCompressedContentByteArray[index]);

        }

      }


      for (let index = 0; index < zipLocalFileHeader.length; index++) {

        dataView.setUint8(byteIndex, zipLocalFileHeader[index], /* リトル エンディアン */ true);

        byteIndex++;

      }

    }



    for (let fileIndex = 0; fileIndex < fileArray.length; fileIndex++) {

      const file = fileArray[fileIndex];


      const zipLocalFileHeaderStartIndex = file.zipLocalFileHeaderStartIndex;


      let fileName = file.name;

      if (fileName.startsWith("/")) {

        fileName = fileName.substring(1);

      }

      const fileNameByteArray = new TextEncoder("utf-8").encode(fileName);

      const fileNameSize = fileNameByteArray.length;


      const fileContentArrayBuffer = await file.arrayBuffer();

      const fileContentSize = fileContentArrayBuffer.byteLength;

      const fileContentByteArray = new Uint8Array(fileContentArrayBuffer);


      const fileContentCrc32 = crc32(fileContentByteArray);


      const fileCompressedContentArrayBuffer = await this.deflateFile(file);

      const fileCompressedContentSize = fileCompressedContentArrayBuffer.byteLength;

      const fileCompressedContentByteArray = new Uint8Array(fileCompressedContentArrayBuffer);


      //zipのセントラル ディレクトリ ヘッダー

      const zipCentralDirectoryHeader = [


        //セントラル ディレクトリ ヘッダーを表す固定値

        0x50,

        0x4B,

        0x01,

        0x02,


        //当zipを作成したアプリケーションが対応可能なzipのバージョン

        0x14,


        //当zipを作成したOS

        //

        //Unixは16進数表記で03

        //

        0x03,


        //当zipの展開に必要な最小バージョン

        //

        //無圧縮のSTOREDのバージョン1.0は16進数表記で順に0A、00。

        //deflate圧縮可能なバージョン2.0は16進数表記で順に14、00。

        //

        0x14,

        0x00,


        //当zipの汎用ビット フラグ

        //

        //当zip内のファイル名の文字コードがUTF-8の場合は16進数表記で順に00、08。

        //

        0x00,

        0x08,


        //当zipの圧縮方法

        //

        //無圧縮のzipのSTOREDは16進数表記で順に00、00。

        //deflate圧縮は16進数表記で順に08、00。

        //

        0x08,

        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。

        //

        ((fileCompressedContentSize << 24) >>> 24),

        ((fileCompressedContentSize << 16) >>> 24),

        ((fileCompressedContentSize << 8) >>> 24),

        (fileCompressedContentSize >>> 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,

        0xB4,

        0x81,


        //対応するローカル ファイル ヘッダーの開始バイトのインデックス番号

        ((zipLocalFileHeaderStartIndex << 24) >>> 24),

        ((zipLocalFileHeaderStartIndex << 16) >>> 24),

        ((zipLocalFileHeaderStartIndex << 8) >>> 24),

        (zipLocalFileHeaderStartIndex >>> 24)

      ];

      //当zip内の当ファイルの名前

      //

      //ディレクトリの場合はディレクトリ名 + 半角スラッシュ記号(/)

      //

      //ディレクトリ内のファイルの場合はディレクトリ名 + 半角スラッシュ記号(/) + ファイル名

      //

      for (let index = 0; index < fileNameByteArray.length; index++) {

        zipCentralDirectoryHeader.push(fileNameByteArray[index]);

      }


      for (let index = 0; index < zipCentralDirectoryHeader.length; index++) {

        dataView.setUint8(byteIndex, zipCentralDirectoryHeader[index], /* リトル エンディアン */ true);

        byteIndex++;

      }

    }


    //zipのセントラル ディレクトリの終端レコード

    const 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 (let index = 0; index < zipEndOfCentralDirectoryRecord.length; index++) {

      dataView.setUint8(byteIndex, zipEndOfCentralDirectoryRecord[index], /* リトル エンディアン */ true);

      byteIndex++;

    }


    return new Uint8Array(arrayBuffer);

  }

}



$("en").onclick = () => {

  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 = () => {

  if ($("ja").checked) {

    $("horizontalWritingLabel").innerHTML = "横書き";

    $("verticalWritingLabel").innerHTML = "縦書き";

    $("filesLabel").innerHTML = "1つ以上のテキストファイル(と画像ファイル)を選択";

    $("titleLabel").innerHTML = "電子書籍のタイトル";

    $("authorLabel").innerHTML = "著者";

    $("makeEPub").innerHTML = "電子書籍(ePub)を作成";

  }

};



$("makeEPub").onclick = async () => {


  $("message").innerHTML = "";


  const title = $("title").value;

  const author = $("author").value;


  const isJa = $("ja").checked;


  const 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.

  const epubMakeDateTimeXml = new Date().toISOString().replace(/\.[0-9]{3}/g, "");


  const textFileArray = [];

  const imageFileArray = [];

  let coverImageFile = null;


  const files = $("file").files;

  for (let index = 0; index < files.length; index++) {

    const file = files[index];


    const name = file.name;

    const 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;

      }

    }

  }


  const compare = (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;

  }



  const deflateZip = new DeflateZip();


  const mimetypeBlob = new Blob([new TextEncoder("utf-8").encode("application/epub+zip")]);

  mimetypeBlob.name = "mimetype";

  deflateZip.add(mimetypeBlob);



  const 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";

  deflateZip.add(containerXmlBlob);



  const 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 (let 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 (let index = 0; index < imageFileArray.length; index++) {

    const 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) {

      const 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 (let index = 0; index < textFileArray.length; index++) {

   opfArray.push('    <itemref idref="index' + (index + 1) + '"/>');

  }

  opfArray.push('  </spine>');

  opfArray.push('</package>\n');



  const opfBlob = new Blob([new TextEncoder("utf-8").encode(opfArray.join("\n"))]);

  opfBlob.name = "opf.opf";

  deflateZip.add(opfBlob);



  if (isVerticalWriting) {


    const 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";

    deflateZip.add(cssBlob);


  } else {


    const 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";

    deflateZip.add(cssBlob);

  }


  const 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 (let index = 0; index < textFileArray.length; index++) {

    const 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>');



  const navBlob = new Blob([new TextEncoder("utf-8").encode(navArray.join("\n"))]);

  navBlob.name = "nav.xhtml";

  deflateZip.add(navBlob);



  //Image files is zipped.

  //The following is repeated the number of image files.

  for (let index = 0; index < imageFileArray.length; index++) {

    const name = imageFileArray[index].name;


    const data = await imageFileArray[index].arrayBuffer();


    const blob = new Blob([data]);

    blob.name = name;

    deflateZip.add(blob);

  }



  //The following is repeated the number of text files.

  for (let index = 0; index < textFileArray.length; index++) {

    const name = textFileArray[index].name;

    const 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>');


    let text = await textFileArray[index].text();


    text = text.replaceAll(/\r\n/g, "\n");

    text = text.replaceAll(/\r/g, "\n");

    const lineArray = text.split("\n");

    for (let i = 0; i < lineArray.length; i++) {

      const 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

        let 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');


    const blob = new Blob([new TextEncoder("utf-8").encode(indexXhtmlArray.join("\n"))]);

    blob.name = "index" + (index + 1) + ".xhtml";

    deflateZip.add(blob);

  }



  const blob = new Blob([await deflateZip.zip()], {type: "application/epub+zip"});


  const url = URL.createObjectURL(blob);


  const 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>

評価をするにはログインしてください。
ブックマークに追加
ブックマーク機能を使うにはログインしてください。
+注意+

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

↑ページトップへ