feat(monospace): embed TTF into pandoc-generated DOCX

This commit is contained in:
2026-07-23 09:34:57 +05:30
parent 001c9463e3
commit 992c6b72d6
2 changed files with 111 additions and 0 deletions
+46
View File
@@ -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 `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:font w:name="${family}">
<w:panose1 w:val="020F0502020204030204"/>
<w:charset w:val="00"/>
<w:family w:val="modern"/>
<w:pitch w:val="fixed"/>
<w:embedRegular r:id="rId1"/>
</w:font>
</w:fonts>`;
}
function fontTableRels(fontFilename) {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" Target="${fontFilename}"/>
</Relationships>`;
}
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 };
@@ -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',
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>'
);
zip.file(
'_rels/.rels',
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>'
);
zip.file('word/document.xml', '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>');
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/);
});
});