diff --git a/src/main/DocxFontEmbedder.js b/src/main/DocxFontEmbedder.js
new file mode 100644
index 0000000..8ff701a
--- /dev/null
+++ b/src/main/DocxFontEmbedder.js
@@ -0,0 +1,46 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const JSZip = require('jszip');
+
+const FONT_TABLE_PATH = 'word/fontTable.xml';
+const FONT_TABLE_RELS_PATH = 'word/_rels/fontTable.xml.rels';
+
+function fontTableXml(family) {
+ return `
+
+
+
+
+
+
+
+
+`;
+}
+
+function fontTableRels(fontFilename) {
+ return `
+
+
+`;
+}
+
+async function embedDocxFont(inputDocxPath, outputDocxPath, ttfPath, fontFamily) {
+ if (!ttfPath || !fs.existsSync(ttfPath)) {
+ throw new Error(`DocxFontEmbedder: TTF not found at ${ttfPath}`);
+ }
+ const inputBytes = fs.readFileSync(inputDocxPath);
+ const zip = await JSZip.loadAsync(inputBytes);
+ const safeFamily = String(fontFamily).replace(/[^A-Za-z0-9]/g, '');
+ const fontFilename = `${safeFamily}.ttf`;
+ const fontBytes = fs.readFileSync(ttfPath);
+ zip.file(`word/fonts/${fontFilename}`, fontBytes);
+ zip.file(FONT_TABLE_PATH, fontTableXml(safeFamily));
+ zip.file(FONT_TABLE_RELS_PATH, fontTableRels(fontFilename));
+ const out = await zip.generateAsync({ type: 'nodebuffer' });
+ fs.writeFileSync(outputDocxPath, out);
+}
+
+module.exports = { embedDocxFont };
diff --git a/tests/unit/main/monospace/DocxFontEmbedder.test.js b/tests/unit/main/monospace/DocxFontEmbedder.test.js
new file mode 100644
index 0000000..c52b93c
--- /dev/null
+++ b/tests/unit/main/monospace/DocxFontEmbedder.test.js
@@ -0,0 +1,65 @@
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const JSZip = require('jszip');
+
+const { embedDocxFont } = require('../../../../src/main/DocxFontEmbedder');
+
+async function makeMinimalDocx(outPath) {
+ const zip = new JSZip();
+ zip.file(
+ '[Content_Types].xml',
+ ''
+ );
+ zip.file(
+ '_rels/.rels',
+ ''
+ );
+ zip.file('word/document.xml', '');
+ const buf = await zip.generateAsync({ type: 'nodebuffer' });
+ fs.writeFileSync(outPath, buf);
+}
+
+describe('DocxFontEmbedder', () => {
+ let tmpDir;
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mono-docx-'));
+ });
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test('embeds TTF into word/fonts and updates rels', async () => {
+ const input = path.join(tmpDir, 'in.docx');
+ const output = path.join(tmpDir, 'out.docx');
+ const ttf = path.join(tmpDir, 'JetBrainsMono-Regular.ttf');
+ await makeMinimalDocx(input);
+ fs.writeFileSync(ttf, Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00]));
+
+ await embedDocxFont(input, output, ttf, 'JetBrainsMono');
+
+ const result = await JSZip.loadAsync(fs.readFileSync(output));
+ const fontFiles = Object.keys(result.files).filter((n) => n.startsWith('word/fonts/'));
+ expect(fontFiles.length).toBeGreaterThan(0);
+ const rels = result.file('word/_rels/fontTable.xml.rels');
+ expect(rels).toBeTruthy();
+ const relsContent = await rels.async('string');
+ expect(relsContent).toContain('.ttf');
+ });
+
+ test('returns rejected promise for invalid docx input', async () => {
+ const input = path.join(tmpDir, 'invalid.docx');
+ const output = path.join(tmpDir, 'out.docx');
+ const ttf = path.join(tmpDir, 'JetBrainsMono-Regular.ttf');
+ fs.writeFileSync(input, Buffer.from('not a docx'));
+ fs.writeFileSync(ttf, Buffer.from([0x01]));
+ await expect(embedDocxFont(input, output, ttf, 'JetBrainsMono')).rejects.toThrow();
+ });
+
+ test('throws if ttf missing', async () => {
+ const input = path.join(tmpDir, 'in.docx');
+ const output = path.join(tmpDir, 'out.docx');
+ await makeMinimalDocx(input);
+ await expect(embedDocxFont(input, output, '/nope.ttf', 'JetBrainsMono')).rejects.toThrow(/TTF/);
+ });
+});