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

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

AozoraEpub3JSの外字フォントの追加

外字の情報はsrc\info\GaijiInfo.jsに入っていて、ファイル名をCSSやOPFに書き込む際のデータの取り方に苦戦した。

You:

FileNameがGaijiInfoから取得できない

// GaijiInfo.js

import path from 'path';

export default class GaijiInfo {
    /**
     * @param {string} className - クラス名
     * @param {string} gaijiFile - 外字フォントのファイルパス
     */
    constructor(className, gaijiFile) {
        this.className = className;
        this.gaijiFile = gaijiFile;
    }

    /**
     * ファイル名を取得する
     * @returns {string}
     */
    getFileName() {
        return path.basename(this.gaijiFile);
    }

    /**
     * ファイルパスを取得する
     * @returns {string}
     */
    getFile() {
        return this.gaijiFile;
    }

    /**
     * ファイルパスを設定する
     * @param {string} gaijiFile
     */
    setFile(gaijiFile) {
        this.gaijiFile = gaijiFile;
    }

    /**
     * クラス名を取得する
     * @returns {string}
     */
    getClassName() {
        return this.className;
    }

    /**
     * クラス名を設定する
     * @param {string} className
     */
    setClassName(className) {
        this.className = className;
    }
}

ChatGPT:

これもパターンはほぼ特定できます 👍
結論からいきます。


🎯 結論

👉 gaijiFile が undefined / null になっている


🔍 あなたのコード

JavaScript

getFileName() {  
    return path.basename(this.gaijiFile);  
}

❗ 問題が起きる条件

JavaScript

this.gaijiFile \=== undefined

このとき👇

JavaScript

path.basename(undefined)

👉 💥 エラー or 期待しない値


🔥 よくある原因(かなり当たりやすい)

① コンストラクタに値が入ってない

JavaScript

new GaijiInfo(className) // ← gaijiFile渡してない

👉 this.gaijiFile = undefined


② 途中で消えてる

JavaScript

gaijiInfo.setFile(null);

③ JSON化で壊れてる(よくある)

JavaScript

const obj \= JSON.parse(JSON.stringify(gaijiInfo));

👉 一見OKだが…

JavaScript

obj.getFileName // undefined

👉 クラスじゃなくなる


④ Promiseのまま(前と同じ罠)

JavaScript

const gaijiInfo \= getGaijiInfo(); // async

👉 Promise


✅ まずこれを確認

JavaScript

console.log(this.gaijiFile);  
console.log(typeof this.gaijiFile);

✅ 安全な修正版(おすすめ)

JavaScript

getFileName() {  
    if (!this.gaijiFile) return null;  
    return path.basename(this.gaijiFile);  
}

🚀 さらに強化版(デバッグ付き)

JavaScript

getFileName() {  
    if (!this.gaijiFile) {  
        console.warn("gaijiFile is empty:", this);  
        return null;  
    }  
  
    try {  
        return path.basename(this.gaijiFile);  
    } catch (e) {  
        console.error("basename error:", this.gaijiFile, e);  
        return null;  
    }  
}

🎯 追加チェック(かなり重要)

呼び出し側で👇

JavaScript

console.log(gaijiInfo);  
console.log(gaijiInfo instanceof GaijiInfo);

結果の意味

結果 原因
true OK
false JSON化 or 別オブジェクト

💡 AozoraEpub3移植あるある

今回の流れかなり典型👇

  • Java:型が保証される

  • JS:簡単に壊れる

👉 特にこれ危険

JavaScript

map / reduce / JSON / spread

👍 改善版(最終おすすめ)

JavaScript

getFileName() {  
    if (typeof this.gaijiFile !== "string") return null;  
    return path.basename(this.gaijiFile);  
}

🚀 もしまだダメなら

これ貼ってください👇

  • GaijiInfo を生成してるコード

  • getFileName 呼んでる箇所

👉 どこで壊れてるか100%特定できます

You:

[ GaijiInfo { className: 'u3042-u3099', gaijiFile: 'gaiji\\dakuten\\u3042-u3099.ttf' }, GaijiInfo { className: 'u3044-u3099', gaijiFile: 'gaiji\\dakuten\\u3044-u3099.ttf' }, GaijiInfo { className: 'u3048-u3099', gaijiFile: 'gaiji\\dakuten\\u3048-u3099.ttf' }, GaijiInfo { className: 'u304a-u3099', gaijiFile: 'gaiji\\dakuten\\u304a-u3099.ttf' } ]

ChatGPT:

このログを見る限り👇

JavaScript

GaijiInfo {  
  className: 'u3042-u3099',  
  gaijiFile: 'gaiji\\\\dakuten\\\\u3042-u3099.ttf'  
}

👉 データ自体は完全に正常です(原因は別)


🎯 結論(ほぼ確定)

👉 GaijiInfoの配列に対して getFileName() を呼んでいる


🔥 典型的なミス

たぶんどこかで👇やってます

JavaScript

gaijiInfos.getFileName()

でも実際は👇

JavaScript

gaijiInfos \= \[ GaijiInfo, GaijiInfo, ... \]

👉 配列なのでメソッドなし ❌


✅ 正しい使い方

✔ 1件ずつ処理

JavaScript

for (const gaijiInfo of gaijiInfos) {  
    console.log(gaijiInfo.getFileName());  
}

✔ map

JavaScript

const names \= gaijiInfos.map(g => g.getFileName());

✔ 1つだけ

JavaScript

gaijiInfos\[0\].getFileName();

🔍 もう1つの可能性(これも多い)

👉 関数に渡してる引数がズレてる

JavaScript

function write(gaijiInfo) {  
    gaijiInfo.getFileName(); // ← ここ  
}

でも実際👇

JavaScript

write(gaijiInfos); // ← 配列渡してる

🧠 今回の状況まとめ

状態 結果
中身 正しい(GaijiInfo)
配列 OK
呼び方 ❌ 間違ってる

🔍 一発で特定する方法

エラー直前に👇

JavaScript

console.log(Array.isArray(gaijiInfo));

👉 true なら確定


🚀 修正ポイント

エラー箇所たぶん👇

JavaScript

gaijiInfo.getFileName()

👉 修正:

JavaScript

gaijiInfos\[i\].getFileName()

または

JavaScript

for (const g of gaijiInfos) {  
    g.getFileName();  
}

👍 まとめ

👉 今回の本質

「オブジェクト」と「配列」を取り違えている」


💡 おまけ(安全にするなら)

JavaScript

if (Array.isArray(gaijiInfo)) {  
    throw new Error("gaijiInfo is array!");  
}

もしよければ
👉 getFileName() を呼んでる箇所貼ってください

ピンポイントで1行修正出せます 👍

You:

/** 外字用フォントを追加 */ addGaijiFont(className, gaijiFile) { if (this.gaijiNameSet.has(className)) return; this.vecGaijiInfo.push(new GaijiInfo(className, gaijiFile)); this.gaijiNameSet.add(className); }

ChatGPT:

このコード自体は 完全に正しいです 👍(問題なし)

JavaScript

addGaijiFont(className, gaijiFile) {  
    if (this.gaijiNameSet.has(className)) return;  
    this.vecGaijiInfo.push(new GaijiInfo(className, gaijiFile));  
    this.gaijiNameSet.add(className);  
}

🎯 じゃあなぜ「FileNameが取れない」のか

ここまでの情報を全部つなげると👇

  • vecGaijiInfo の中身 → 正常(あなたのログで確認済み)

  • GaijiInfo → 正常

  • gaijiFile → 正常

👉 原因は100%これです


🔥 原因(確定)

👉 vecGaijiInfo(配列)に対して getFileName() を呼んでる


❌ やっている可能性が高いコード

JavaScript

this.vecGaijiInfo.getFileName()

✅ 正しいコード

✔ ループする

JavaScript

for (const g of this.vecGaijiInfo) {  
    console.log(g.getFileName());  
}

✔ map

JavaScript

const fileNames \= this.vecGaijiInfo.map(g => g.getFileName());

✔ 1件だけ

JavaScript

this.vecGaijiInfo\[0\].getFileName();

🔍 ここがズレてる可能性大

例えば👇

JavaScript

writeGaijiFont(gaijiInfo) {  
    gaijiInfo.getFileName(); // ← ここ  
}

呼び出し👇

JavaScript

writeGaijiFont(this.vecGaijiInfo); // ← 配列渡してる

👉 💥 これが今回のバグ


🧠 なぜ起きたか(Aozora移植あるある)

Javaだと👇

Java

List<GaijiInfo\>

型があるから防げる

でもJSだと👇

JavaScript

any

👉 配列でも単体でも通る


🚀 一発で確認する方法

エラー箇所の直前で👇

JavaScript

console.log("isArray:", Array.isArray(gaijiInfo));  
console.log("value:", gaijiInfo);

🎯 まとめ

今回のバグはこれ👇

👉 GaijiInfo単体のつもりで配列を扱っている


💡 さらに安全にするなら

JavaScript

getFileName() {  
    if (!this.gaijiFile) return null;  
    return path.basename(this.gaijiFile);  
}

JavaScript

if (Array.isArray(gaijiInfo)) {  
    throw new Error("Expected GaijiInfo but got array");  
}

👍 次やるといいこと

もし可能なら👇貼ってください

👉 getFileName() を呼んでるコード

1行で完全修正出せます

You:

// 外字のcssを格納 this.ejsData.vecGaijiInfo = this.vecGaijiInfo; // メンバ変数解放 this.velocityContext = null; this.bookInfo = null; this.imageInfoReader = null; if (!bookInfo.imageOnly) { const textCssEntry = ${Epub3Writer.OPS_PATH}${Epub3Writer.CSS_PATH}${Epub3Writer.TEXT_CSS}; let textcss = fs.readFileSync(path.resolve(__dirname, ${this.templatePath}${Epub3Writer.OPS_PATH}${Epub3Writer.CSS_PATH}${Epub3Writer.TEXT_CSS_EJS}), 'utf-8'); const text_css = ejs.render(textcss, this.ejsData) this.zos.file(textCssEntry, text_css, { compression: "DEFLATE", compressionOptions: { level: 9 } }); }

ChatGPT:

ここまでで原因は完全に繋がりました 👍
犯人はこの行です👇

JavaScript

this.ejsData.vecGaijiInfo \= this.vecGaijiInfo;

🎯 結論

👉 EJS側で vecGaijiInfo(配列)を単体として扱っている


🔥 何が起きているか

JS側:

JavaScript

this.vecGaijiInfo \= \[ GaijiInfo, GaijiInfo, ... \];  
this.ejsData.vecGaijiInfo \= this.vecGaijiInfo;

👉 問題なし


でもEJSテンプレート側で👇やってるはず

ejs

<%= vecGaijiInfo.getFileName() %>

👉 ❌ 配列に対して呼んでる


✅ 正しいEJSの書き方

✔ ループする

ejs

<% vecGaijiInfo.forEach(function(g) { %>  
  <%= g.getFileName() %>  
<% }) %>

✔ map風

ejs

<%= vecGaijiInfo.map(g => g.getFileName()).join(",") %>

🧠 ここが重要(今回の本質)

JSでは👇

JavaScript

Array<GaijiInfo\>

EJSでは👇

👉 ただの配列

なので👇はダメ

ejs

vecGaijiInfo.getFileName()

🔍 ほぼ確実にあるテンプレートバグ

AozoraEpub3的にありがち👇

ejs

@font-face {  
  src: url("<%= vecGaijiInfo.getFileName() %>");  
}

👉 ❌


✅ 正しい書き方(CSS生成)

ejs

<% vecGaijiInfo.forEach(function(g) { %>  
@font-face {  
  font-family: "<%= g.getClassName() %>";  
  src: url("../fonts/<%= g.getFileName() %>");  
}  
<% }) %>

🚀 デバッグ方法(確定させる)

EJS内に一時的に👇

ejs

<%= Array.isArray(vecGaijiInfo) %>

👉 true が出る


💡 さらに安全にするなら(JS側)

JavaScript

this.ejsData.vecGaijiInfo \= this.vecGaijiInfo ?? \[\];

👍 まとめ

今回のバグ:

👉 「EJSで配列を単体として扱っている」


🎯 修正箇所

👉 TEXT_CSS_EJS の中


🚀 次にやるといい

もしよければ👇貼ってください

  • TEXT_CSS_EJS の中身

👉 完全に動く形に書き直します(Aozora互換) 👍

You:

/** 外字フォント */ <% vecGaijiInfo.forEach(gaijiInfo => { %> @font-face { font-family: "<%= gaijiInfo.className %>"; src: url(../gaiji/<%= gaijiInfo.fileName %>); } .<%= gaijiInfo.className %> { font-family: "<%= gaijiInfo.className %>"; } <% }); %>

ChatGPT:

原因はここで確定です 👍


🎯 問題の行

ejs

<%= gaijiInfo.fileName %>

👉 fileName プロパティは存在しない


🔍 あなたのクラス

JavaScript

getFileName() {  
    return path.basename(this.gaijiFile);  
}

👉 メソッドはある
👉 でもプロパティは無い


❌ 今の状態

ejs

gaijiInfo.fileName // undefined

✅ 修正方法(2パターン)


✔ 方法①(シンプル)

👉 メソッドを呼ぶ

ejs

<%= gaijiInfo.getFileName() %>

✔ 方法②(おすすめ:Java互換)

👉 getter追加(JS側)

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると👇

ejs

<%= gaijiInfo.fileName %>

がそのまま使える


🚀 修正後のEJS(完成版)

ejs

/\*\* 外字フォント \*/  
<% vecGaijiInfo.forEach(gaijiInfo => { %>  
@font-face {  
    font-family: "<%= gaijiInfo.className %>";  
    src: url(../gaiji/<%= gaijiInfo.getFileName() %>);  
}  
.<%= gaijiInfo.className %> {  
    font-family: "<%= gaijiInfo.className %>";  
}  
<% }); %>

💡 なぜ起きたか

Java版だと👇

Java

getFileName() → テンプレートから property 的に見える

でもJS + EJSでは👇

👉 メソッドは明示的に呼ばないとダメ


👍 おすすめ最終形(これが一番安全)

GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

EJS(そのまま使える)

ejs

<%= gaijiInfo.fileName %>

🎯 まとめ

今回の原因👇

👉 「メソッド」と「プロパティ」を取り違えた」


🚀 次の一手

今のAozora移植だと👇

  • getXXX().xxx にしたほうが楽

👉 全体をJava風→JS風に整理もできます

必要ならまとめて最適化出します 👍

You:

<% for(var i = 0; i < vecGaijiInfo.length; i++) { -%> <% }; -%>

ChatGPT:

ここも同じ原因です 👍
完全に特定できます


🎯 問題の行

ejs

<%= vecGaijiInfo\[i\].FileName -%>

👉 ❌ FileName は存在しない(しかも大文字)


🔍 あなたのクラス

JavaScript

getFileName()

👉 メソッド名はこれだけ


❌ 現状

JavaScript

vecGaijiInfo\[i\].FileName // undefined

✅ 修正方法

✔ 方法①(そのまま直す)

ejs

<%= vecGaijiInfo\[i\].getFileName() -%>

✔ 方法②(おすすめ:互換対応)

👉 GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると👇

ejs

<%= vecGaijiInfo\[i\].fileName -%>

🚀 修正後コード(安全版)

ejs

<% for (var i = 0; i < vecGaijiInfo.length; i++) { -%>  
    <item id="gaiji\_<%= i + 1 -%>" href="gaiji/<%= vecGaijiInfo\[i\].getFileName() -%>" media-type="application/font-sfnt"/>  
<% } -%>

💡 さらに改善(読みやすい書き方)

ejs

<% vecGaijiInfo.forEach((g, i) => { -%>  
    <item id="gaiji\_<%= i + 1 -%>" href="gaiji/<%= g.getFileName() -%>" media-type="application/font-sfnt"/>  
<% }); -%>

🔥 今回の本質まとめ

今回のバグ全部同じ原因👇

  • fileName → ❌ プロパティじゃない

  • FileName → ❌ 大文字も違う

  • 正解 → getFileName()


👍 Aozora移植での鉄則

👉 JavaのgetterはJSではそのまま使えない


🚀 最終おすすめ(これやると全部解決)

GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると全部こう書ける

ejs

g.fileName

👉 Java感覚で書けるのでバグ激減


必要なら👇

👉 ImageInfo / GaijiInfo / BookInfo 全部Java互換プロパティ化
まとめてやるとかなり安定します 👍 [ GaijiInfo { className: 'u3042-u3099', gaijiFile: 'gaiji\dakuten\u3042-u3099.ttf' }, GaijiInfo { className: 'u3044-u3099', gaijiFile: 'gaiji\dakuten\u3044-u3099.ttf' }, GaijiInfo { className: 'u3048-u3099', gaijiFile: 'gaiji\dakuten\u3048-u3099.ttf' }, GaijiInfo { className: 'u304a-u3099', gaijiFile: 'gaiji\dakuten\u304a-u3099.ttf' } ]

/* 外字フォント / <% vecGaijiInfo.forEach(gaijiInfo => { %> @font-face { font-family: "<%= gaijiInfo.ClassName %>"; src: url(../gaiji/<%= gaijiInfo.FileName %>); } .<%= gaijiInfo.ClassName %> { font-family: "<%= gaijiInfo.ClassName %>"; } <% }); %>

Java版

/* 外字フォント / @font-face {font-family:"u3042-u3099"; src:url(../gaiji/u3042-u3099.ttf);} .u3042-u3099 {font-family:"u3042-u3099";} @font-face {font-family:"u3044-u3099"; src:url(../gaiji/u3044-u3099.ttf);} .u3044-u3099 {font-family:"u3044-u3099";} @font-face {font-family:"u3048-u3099"; src:url(../gaiji/u3048-u3099.ttf);} .u3048-u3099 {font-family:"u3048-u3099";} @font-face {font-family:"u304a-u3099"; src:url(../gaiji/u304a-u3099.ttf);} .u304a-u3099 {font-family:"u304a-u3099";}