diff --git a/src/main/EpubFontEmbedder.js b/src/main/EpubFontEmbedder.js
new file mode 100644
index 0000000..deb2b29
--- /dev/null
+++ b/src/main/EpubFontEmbedder.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const JSZip = require('jszip');
+
+function withEpubEmbedFontArgs(pandocArgs, ttfPath, _fontFamily) {
+ return [`--epub-embed-font=${ttfPath}`, ...pandocArgs];
+}
+
+async function embedEpubFont(epubPath, ttfPath, _fontFamily) {
+ if (!ttfPath || !fs.existsSync(ttfPath)) {
+ throw new Error(`EpubFontEmbedder: TTF not found at ${ttfPath}`);
+ }
+ const bytes = fs.readFileSync(epubPath);
+ const zip = await JSZip.loadAsync(bytes);
+ const ttfBytes = fs.readFileSync(ttfPath);
+ const fontName = path.basename(ttfPath);
+ zip.file(`OEBPS/${fontName}`, ttfBytes);
+ const opfPath = 'OEBPS/content.opf';
+ const opfFile = zip.file(opfPath);
+ if (!opfFile) {
+ fs.writeFileSync(epubPath, await zip.generateAsync({ type: 'nodebuffer' }));
+ return;
+ }
+ let opf = await opfFile.async('string');
+ const manifestEntry = ` `;
+ if (!opf.includes('manifest')) {
+ opf = opf.replace('', `${manifestEntry}`);
+ } else if (!opf.includes(fontName)) {
+ opf = opf.replace('', `${manifestEntry}`);
+ }
+ zip.file(opfPath, opf);
+ fs.writeFileSync(epubPath, await zip.generateAsync({ type: 'nodebuffer' }));
+}
+
+module.exports = { withEpubEmbedFontArgs, embedEpubFont };
diff --git a/tests/unit/main/monospace/EpubFontEmbedder.test.js b/tests/unit/main/monospace/EpubFontEmbedder.test.js
new file mode 100644
index 0000000..b5ee766
--- /dev/null
+++ b/tests/unit/main/monospace/EpubFontEmbedder.test.js
@@ -0,0 +1,53 @@
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const JSZip = require('jszip');
+
+const { withEpubEmbedFontArgs, embedEpubFont } = require('../../../../src/main/EpubFontEmbedder');
+
+async function makeMinimalEpub(outPath) {
+ const zip = new JSZip();
+ zip.file(
+ 'META-INF/container.xml',
+ ''
+ );
+ zip.file('OEBPS/content.opf', '');
+ fs.writeFileSync(outPath, await zip.generateAsync({ type: 'nodebuffer' }));
+}
+
+describe('EpubFontEmbedder', () => {
+ let tmpDir;
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mono-epub-'));
+ });
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test('withEpubEmbedFontArgs prepends --epub-embed-font', () => {
+ const args = withEpubEmbedFontArgs(['-o', 'out.epub', 'in.md'], '/tmp/font.ttf', 'JetBrains Mono');
+ expect(args).toContain('--epub-embed-font=/tmp/font.ttf');
+ expect(args.indexOf('--epub-embed-font=/tmp/font.ttf')).toBeLessThan(args.indexOf('-o'));
+ });
+
+ test('embedEpubFont adds font to OEBPS and updates manifest', async () => {
+ const epub = path.join(tmpDir, 'book.epub');
+ const ttf = path.join(tmpDir, 'JetBrainsMono-Regular.ttf');
+ await makeMinimalEpub(epub);
+ fs.writeFileSync(ttf, Buffer.from([0x01, 0x00]));
+
+ await embedEpubFont(epub, ttf, 'JetBrains Mono');
+
+ const zip = await JSZip.loadAsync(fs.readFileSync(epub));
+ const fontFile = zip.file('OEBPS/JetBrainsMono-Regular.ttf');
+ expect(fontFile).toBeTruthy();
+ const opf = await zip.file('OEBPS/content.opf').async('string');
+ expect(opf).toContain('JetBrainsMono-Regular.ttf');
+ });
+
+ test('embedEpubFont rejects missing TTF', async () => {
+ const epub = path.join(tmpDir, 'book.epub');
+ await makeMinimalEpub(epub);
+ await expect(embedEpubFont(epub, '/nope.ttf', 'JetBrains Mono')).rejects.toThrow(/TTF/);
+ });
+});