KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

10月22日から10月28日までキンドル新刊ピックアップ

10月22日発売の本

10月22日発売のそのほかの本 kyukyunyorituryo.github.io

10月23日発売の本

10月23日発売のそのほかの本 kyukyunyorituryo.github.io

10月24日発売の本

10月24日発売のそのほかの本 kyukyunyorituryo.github.io

10月25日発売の本

10月25日発売のそのほかの本 kyukyunyorituryo.github.io

10月26日発売の本

10月26日発売のそのほかの本 kyukyunyorituryo.github.io

10月27日発売の本

10月27日発売のそのほかの本 kyukyunyorituryo.github.io

10月28日発売の本

10月28日発売のそのほかの本 kyukyunyorituryo.github.io

10月15日から10月21日までキンドル新刊ピックアップ

10月15日発売の本

10月15日発売のそのほかの本 kyukyunyorituryo.github.io

10月16日発売の本

10月16日発売のそのほかの本 kyukyunyorituryo.github.io

10月17日発売の本

10月17日発売のそのほかの本 kyukyunyorituryo.github.io

10月18日発売の本

10月18日発売のそのほかの本 kyukyunyorituryo.github.io

10月19日発売の本

10月19日発売のそのほかの本 kyukyunyorituryo.github.io

10月20日発売の本

[asin:B0HN3VT8JV:detail]

10月20日発売のそのほかの本 kyukyunyorituryo.github.io

10月21日発売の本

10月21日発売のそのほかの本 kyukyunyorituryo.github.io

数式EPUB出力に目次の順番通りに表示する機能をつけた

markdown texと出力されるEPUBを比較しながらテストしていると、突然消えている箇所があったので調べた。そうすると半角のカッコがmarkdownかtexの部分で誤認識されているようなので全角カッコにしたら治った。

markdownパーサーやkatexの変換の違いでVscodeなら変換される部分もmarkdown-it-texmathでは違う変換され方だったり色々ある。

はじめはmarkdownを探してファイル名順に並べていたが、ファイル名の自由がなくなるので変更した。

  filenames = (await fs.readdir(inputDir))
    .filter(
      (name) =>
        name.toLowerCase().endsWith(".md") &&
        name.toLowerCase() !== "toc.md"
    )
    .sort((a, b) => a.localeCompare(b, "en"));

表組みに枠線がなかったのでCSSで枠線をつけた。スタイルシートでは文字幅をepubでemで指定することが多いので修正が必要かも。

body {
  font-family: serif;
  line-height: 1.75;
  margin: 4%;
  overflow-wrap: break-word;
}

h1, h2, h3, h4 {
  line-height: 1.4;
  page-break-after: avoid;
  break-after: avoid;
}

h1 {
  font-size: 1.6em;
  margin-top: 0;
}

h2 {
  font-size: 1.3em;
  margin-top: 1.8em;
}

p {
  margin: 0.7em 0;
}

table {
  border-collapse: collapse;
  width: 100%;
  margin: 1em 0;
  font-size: 0.9em;
}

th, td {
  border: 1px solid #888;
  padding: 0.35em;
  text-align: center;   /* 左右の中央寄せ */
  vertical-align: middle; /* 上下の中央寄せ */
}

th {
  font-weight: bold;
}

img, svg {
  max-width: 100%;
  height: auto;
}

pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.katex-display {
  margin: 1em 0;
  overflow-x: auto;
  overflow-y: hidden;
  text-align: center;
}

blockquote {
  margin: 1em 0;
  padding-left: 1em;
  border-left: 3px solid #888;
}

MarkdownとTeXからMathML EPUBを作る

chatgptに相談しながら作っているとすぐにできてしまったので、修正するだけで完成しそう。キンドルで販売できるか、国内のEPUBビュワーで問題ないかの確認が必要だが予想より早い。mathmlに変換する前のtexの書き方でエラーが出ているが仕様の問題である。

github.com

仕様をどうするか問題

  • markdownと画像のファイル名をそのままEPUBに入れるか。
  • 目次を入れて順番通りにするか。
  • 電書連仕様のEPUBにするか。

現状はほぼ簡素だがEPUBチェックは警告は出るけどエラーはでないレベル。 エラー内容は「mathmlに代替画像を入れたほうがいい」というもの。

キンドルプレビュワーでは数式がやや上側による。表組みは線に太さをつけたほうがいいかも。

数式EPUB

数式EPUBを作るコードを書いた

calibreのEPUBが嫌いすぎて、chatgptと対話しながらEPUB作成ソフトを作るなどをしていた。エラーはありつつEPUB作成まではできた・。

you: node.jsでmarkdown texをEPUBに変換するコードを書くには

chatgpt :

Node.js で Markdown + TeX(LaTeX 数式)を EPUB に変換するなら、Markdown を HTML に変換し、数式を MathML に変換してから EPUB 3 を生成する構成がおすすめです。

電気工学の問題集のように、見出し・箇条書き・表・数式・SVG 図を含む教材を作る場合も、この構成なら拡張できます。

1. 変換の構成

Markdown ファイル(.md)

見出し・表・LaTeX 数式・SVG

Markdown → HTML

markdown-it + 数式プラグイン

LaTeX → MathML

MathJax(数式を EPUB 内で表示)

EPUB 3 の生成

章分割・目次・CSS・画像・数式

完成した .epub

重要なのは、EPUB 内で JavaScript の数式描画に依存しないことです。MathJax で MathML に変換して EPUB に格納すれば、対応するリーダーでは数式を直接表示できます。ただし、MathML の対応状況は EPUB リーダーによって異なります。

2. 必要なパッケージをインストールする

まずは、1つの Markdown ファイルを EPUB に変換する最小構成から作ります。

mkdir md-tex-epub
cd md-tex-epub
npm init -y
npm install markdown-it markdown-it-texmath katex epub-gen

各ライブラリの役割は次のとおりです。

ライブラリ 役割
markdown-it Markdown を HTML に変換
markdown-it-texmath $...$ や $$...$$ の数式を処理
katex 数式を HTML・MathML に描画
epub-gen HTML から EPUB を生成

epub-gen は HTML を入力にして EPUB を生成できる Node.js 用ライブラリです。

image

GitHub

3. Markdown ファイルを用意する

sample.md を作成します。

# 電気工学の基礎

## 1. オームの法則

電圧 $V$、電流 $I$、抵抗 $R$ の間には、
次の関係が成り立つ。

$$
V = RI
$$

## 2. 電力

電力 $P$ は次の式で求められる。

$$
P = VI = RI^2 = \frac{V^2}{R}
$$

## 3. 比較表

| 物理量 | 記号 | 単位 |
|---|---|---|
| 電圧 | $V$ | V |
| 電流 | $I$ | A |
| 抵抗 | $R$ | Ω |

## 4. まとめ

オームの法則を利用すると、電圧・電流・抵抗を
相互に計算できる。

4. EPUB 変換コードを書く

convert.mjs を作成します。

import fs from "node:fs/promises";
import path from "node:path";
import MarkdownIt from "markdown-it";
import texmath from "markdown-it-texmath";
import katex from "katex";
import Epub from "epub-gen";

const inputFile = process.argv[2] ?? "sample.md";
const outputFile = process.argv[3] ?? "sample.epub";

async function main() {
  const source = await fs.readFile(inputFile, "utf8");

  const md = new MarkdownIt({
    html: false,
    breaks: false,
    linkify: true,
  });

  // LaTeX 数式を MathML を含む HTML に変換
  md.use(texmath, {
    engine: katex,
    delimiters: "dollars",
    katexOptions: {
      output: "htmlAndMathml",
      throwOnError: false,
    },
  });

  const html = md.render(source);

  const epubCss = `
    body {
      font-family: serif;
      line-height: 1.8;
      margin: 5%;
    }

    h1, h2, h3 {
      line-height: 1.4;
      page-break-after: avoid;
    }

    p {
      margin: 0.6em 0;
    }

    table {
      border-collapse: collapse;
      width: 100%;
      margin: 1em 0;
    }

    th, td {
      border: 1px solid #888;
      padding: 0.35em;
    }

    img, svg {
      max-width: 100%;
      height: auto;
    }

    pre, code {
      white-space: pre-wrap;
      overflow-wrap: anywhere;
    }

    .katex-display {
      overflow-x: auto;
      overflow-y: hidden;
      margin: 1em 0;
    }
  `;

  const epub = new Epub(
    {
      title: path.basename(inputFile, path.extname(inputFile)),
      author: "著者名",
      lang: "ja",
      tocTitle: "目次",
      css: epubCss,
      content: [
        {
          title: "本文",
          data: `<?xml version="1.0" encoding="UTF-8"?>
            <!DOCTYPE html>
            <html xmlns="http://www.w3.org/1999/xhtml"
                  xmlns:epub="http://www.idpf.org/2007/ops"
                  lang="ja" xml:lang="ja">
              <head>
                <meta charset="UTF-8"/>
                <title>本文</title>
              </head>
              <body>
                ${html}
              </body>
            </html>`,
        },
      ],
    },
    outputFile
  );

  await epub.promise;
  console.log(`EPUBを生成しました: ${outputFile}`);
}

main().catch((error) => {
  console.error("変換に失敗しました:", error);
  process.exitCode = 1;
});

実行方法は次のとおりです。

node convert.mjs sample.md sample.epub

注意点: これは最小構成の実装例です。実際の EPUB リーダーでの数式表示は別途確認してください。KaTeX の HTML 表示には CSS やフォントの同梱も必要で、上のコードでは KaTeX のフォントファイルを EPUB に格納していません。また、数式の出力方式や EPUB リーダーによって表示結果が異なります。

image

GitHub

+1

5. 本格的な変換ツールにするなら

今後、問題集や技術書を継続して作成するなら、次の機能を追加すると使いやすくなります。

  • 章分割: # や ## ごとに XHTML ファイルを生成し、目次を作成する。
  • 数式対応: インライン数式・独立数式・複数行数式・行列・場合分けを処理する。
  • 図表: Markdown の表、SVG、画像、コードブロックを EPUB に格納する。
  • 日本語対応: 縦書き・横書き、ルビ、禁則処理、改ページを CSS で設定する。
  • 互換性: EPUBCheck で EPUB 3 の構造を検証し、Kindle などの実機でも表示を確認する。

なお、最初から多くの機能を自作する必要はありません。日本語組版と数式を重視するなら、既存の Markdown → EPUB ツールである Kappan も参考になります。MathML や数式の出力方式、Kindle との互換性について実装例があります。

image

GitHub

確認したいのは1点です。 作りたいのはどちらでしょうか?

用途

電気工学の問題集・技術書(数式、表、SVG 図を重視)

小説(青空文庫形式、ルビ、縦書きを重視)

一般的な Markdown ファイルの EPUB 変換ツール

本文のレイアウト

横書き EPUB

縦書き EPUB

横書き・縦書きを切り替えられる

この条件で実装案を作る

you: Node.js の Markdown + TeX → EPUB 変換ツールを具体的に実装したい。用途: 電気工学の問題集・技術書。数式・表・SVG 図を重視。レイアウト: 横書き。実行可能な Node.js コード、必要な依存関係、EPUB 互換性を含めて提案して。

chatgpt: この用途なら、単純に epub-gen で HTML を包むだけでなく、数式の出力形式を選択できる構造にしておくのが重要です。

特に Kindle では MathML だけに頼ると数式が表示されない場合があります。KaTeX の HTML+MathML 出力を利用する方法や、数式を SVG に変換する方法を用意しておくと、用途に応じて対応できます。

image

GitHub

+1

また、Node.js で実装する場合、EPUB 生成ライブラリの内部処理にすべてを任せるより、Markdown の解析と EPUB のパッケージングを分離した方が、後から章分割や SVG 処理を改善しやすくなります。

推奨する実装方針

  • Markdown の見出しから章を分割し、EPUB の目次を生成する。
  • 数式は KaTeX で描画し、MathML と HTML の両方を保持する。
  • SVG は画像として参照するだけでなく、EPUB 内の画像ファイルとして格納する。
  • 数式・表が横幅を超えた場合の CSS を用意する。
  • 最後に EPUBCheck で構造を検証する。

    image

    W3C

1. プロジェクト構成

まずは Windows 上の Node.js で動作する、横書き技術書向けの変換ツールを作ります。

md-tex-epub/
├─ package.json
├─ convert.mjs
├─ book.json
├─ input/
│  ├─ 00-index.md
│  ├─ 01-electricity.md
│  ├─ 02-motors.md
│  └─ images/
│     └─ circuit.svg
└─ output/
   └─ textbook.epub

この実装では、入力ファイルを章ごとに分けます。章単位で Markdown ファイルを管理すると、長い問題集でも修正しやすくなります。

2. ライブラリのインストール

Node.js 22 以降を推奨します。KaTeX の現行ドキュメントでも、Node.js 環境での利用方法が案内されています。

image

KaTeX

mkdir md-tex-epub
cd md-tex-epub
npm init -y
npm install markdown-it markdown-it-texmath katex epub-gen

package.json に ESM の設定を追加します。

{
  "name": "md-tex-epub",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "node convert.mjs"
  },
  "dependencies": {
    "epub-gen": "^0.1.0",
    "katex": "^0.16.0",
    "markdown-it": "^14.0.0",
    "markdown-it-texmath": "^1.0.0"
  }
}

上記のバージョン指定は初期設定の例です。実際にインストールされたバージョンは package-lock.json で固定してください。既存の package.json を使う場合は、手動でバージョンを置き換えるより、先ほどの npm install で生成された依存関係を使うのが確実です。

3. 書籍の設定ファイル

book.json を作成します。

{
  "title": "電気工学の基礎",
  "author": "著者名",
  "language": "ja",
  "inputDir": "input",
  "outputDir": "output",
  "outputFile": "textbook.epub"
}

この設定により、書名や著者名をソースコードから分離できます。

4. 変換プログラム

次のコードは、章ごとの Markdown 読み込み、数式処理、画像パスの解決、EPUB 生成をまとめた初期実装です。

convert.mjs

import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";

import MarkdownIt from "markdown-it";
import texmath from "markdown-it-texmath";
import katex from "katex";
import Epub from "epub-gen";

const ROOT = process.cwd();

const config = JSON.parse(
  await fs.readFile(path.join(ROOT, "book.json"), "utf8")
);

const inputDir = path.resolve(ROOT, config.inputDir);
const outputDir = path.resolve(ROOT, config.outputDir);
const outputFile = path.join(outputDir, config.outputFile);

await fs.mkdir(outputDir, { recursive: true });

const md = new MarkdownIt({
  html: false,
  linkify: true,
  typographer: false,
});

md.use(texmath, {
  engine: katex,
  delimiters: ["dollars", "brackets"],
  katexOptions: {
    output: "htmlAndMathml",
    throwOnError: false,
    strict: "warn",
  },
});

// Markdown 内の相対画像パスを file: URL に変換する。
// epub-gen がローカル画像を取り込めるようにする。
const defaultImageRender =
  md.renderer.rules.image ??
  ((tokens, idx, options, env, self) =>
    self.renderToken(tokens, idx, options));

md.renderer.rules.image = (tokens, idx, options, env, self) => {
  const token = tokens[idx];
  const src = token.attrGet("src");

  if (
    src &&
    !/^(?:https?:|data:|file:|#)/i.test(src)
  ) {
    const sourceFile = env.sourceFile;
    const imagePath = path.resolve(
      path.dirname(sourceFile),
      decodeURIComponent(src.split("#")[0].split("?")[0])
    );

    token.attrSet("src", pathToFileURL(imagePath).href);
  }

  return defaultImageRender(tokens, idx, options, env, self);
};

function escapeHtml(value) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function getTitle(markdown, filename) {
  const match = markdown.match(/^#\s+(.+)$/m);

  return match
    ? match[1].trim()
    : path.basename(filename, ".md");
}

function renderChapter(markdown, filename) {
  const html = md.render(markdown, {
    sourceFile: filename,
  });

  return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      lang="ja" xml:lang="ja">
<head>
  <meta charset="UTF-8"/>
  <title>${escapeHtml(getTitle(markdown, filename))}</title>
</head>
<body>
${html}
</body>
</html>`;
}

const css = `
body {
  font-family: serif;
  line-height: 1.75;
  margin: 4%;
  overflow-wrap: break-word;
}

h1, h2, h3, h4 {
  line-height: 1.4;
  page-break-after: avoid;
  break-after: avoid;
}

h1 {
  font-size: 1.6em;
  margin-top: 0;
}

h2 {
  font-size: 1.3em;
  margin-top: 1.8em;
}

p {
  margin: 0.7em 0;
}

table {
  border-collapse: collapse;
  width: 100%;
  margin: 1em 0;
  font-size: 0.9em;
}

th, td {
  border: 1px solid #888;
  padding: 0.35em;
  vertical-align: top;
}

th {
  font-weight: bold;
}

img, svg {
  max-width: 100%;
  height: auto;
}

pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.katex-display {
  margin: 1em 0;
  overflow-x: auto;
  overflow-y: hidden;
  text-align: center;
}

blockquote {
  margin: 1em 0;
  padding-left: 1em;
  border-left: 3px solid #888;
}
`;

const filenames = (await fs.readdir(inputDir))
  .filter((name) => name.toLowerCase().endsWith(".md"))
  .sort((a, b) => a.localeCompare(b, "en"));

if (filenames.length === 0) {
  throw new Error(`Markdown ファイルがありません: ${inputDir}`);
}

const chapters = [];

for (const filename of filenames) {
  const fullPath = path.join(inputDir, filename);
  const markdown = await fs.readFile(fullPath, "utf8");

  chapters.push({
    title: getTitle(markdown, filename),
    data: renderChapter(markdown, fullPath),
  });

  console.log(`読み込み: ${filename}`);
}

const epub = new Epub(
  {
    title: config.title,
    author: config.author,
    lang: config.language ?? "ja",
    version: 3,
    tocTitle: "目次",
    appendChapterTitles: false,
    css,
    content: chapters,
  },
  outputFile
);

await epub.promise;

console.log(`EPUB を生成しました: ${outputFile}`);

実行します。

npm run build

これで output/textbook.epub が生成されます。

このコードは実装の土台です。実際の EPUB リーダーでの表示テストは必要で、特に数式フォントの同梱や SVG の取り込みについては次の手順で補強してください。

5. 数式・表・SVG の記法

数式

入力 Markdown では、一般的な LaTeX 記法を使います。

## 直流回路の電力

電力は電圧と電流の積である。

$$
P = VI
$$

オームの法則 $V=RI$ を代入すると、

$$
P = RI^2 = \frac{V^2}{R}
$$

複数行の式は次のように記述できる。

$$
\begin{aligned}
V &= RI \\
P &= VI \\
  &= RI^2
\end{aligned}
$$

markdown-it-texmath は KaTeX を使い、インライン数式・独立数式・複数行の数式環境を処理できます。

image

GitHub

表

| 物理量 | 記号 | 単位 |
|---|---|---|
| 電圧 | $V$ | V |
| 電流 | $I$ | A |
| 抵抗 | $R$ | $\Omega$ |
| 電力 | $P$ | W |

Markdown の表は HTML のテーブルとして出力されます。複雑な表や列数の多い表は、画面の幅に収まらない場合があるため、EPUB リーダーで確認してください。

SVG 図

Markdown から SVG ファイルを参照します。

## 直流回路

![抵抗を含む直流回路](images/circuit.svg)

circuit.svg は input/images/ に置きます。

SVG は EPUB 3 で使用できますが、リーダーごとに対応状況が異なります。SVG の取り込みが成功しても、図の内部に外部ファイルや外部フォントを参照する記述がある場合は、追加の処理が必要です。

6. EPUB 互換性で必ず対応したいこと

ここは重要です。上記のコードは EPUB を生成するところまでですが、生成できたことと、すべての端末で数式が正しく表示されることは別です。

項目 現状 本格運用での対応
EPUB 3 生成対象 EPUBCheck で検証
数式 KaTeX の HTML+MathML CSS・フォントを同梱
表 HTML テーブル 横幅と改ページを確認
SVG 画像参照 EPUB 内の格納・表示を確認
目次 章ファイル単位 必要なら節単位の目次も作る
Kindle 未検証 数式の SVG 化などを検討

KaTeX は CSS とフォントファイルを利用して数式を表示するため、これらを EPUB に含める処理を追加してください。

image

KaTeX

また、Kindle 対応を優先する場合は、数式を MathML だけで出力するのではなく、HTML と CSS を併記する方式、または数式 SVG を埋め込む方式を検討します。Kappan の技術書向け実装にも、数式の出力方式とリーダー別の互換性に関する説明があります。

image

GitHub

最後に EPUBCheck で検証します。

java -jar epubcheck.jar output/textbook.epub

EPUBCheck は EPUB 2 / EPUB 3 の構造を検証する公式ツールです。

image

W3C

7. さらに本格的に作る場合の選択肢

日本語の技術書を長期的に制作するなら、既存の Kappan も比較対象になります。Markdown から EPUB 3.3 を生成することを目的にしており、横組み技術書の例に KaTeX 数式・表・図・相互参照が含まれています。

image

GitHub

自作するメリットは、電気工学の問題集に合わせて、問題番号・解答・解説・数式番号・図番号・参照リンクなどを自由に追加できることです。

私なら、まず上記のコードをベースにして、次の順番で拡張します。

  1. KaTeX の CSS とフォントを EPUB 内に格納する。
  2. 数式を HTML+MathML と SVG のどちらで出力するか選択できるようにする。
  3. Markdown の見出しから章・節の目次を生成する。
  4. SVG と表の表示テストを追加する。
  5. EPUBCheck をビルド手順に組み込む。

これなら、単なる Markdown 変換ではなく、電気工学の問題集を継続的に出版するための EPUB 生成ツールへ発展させられます。