mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-24 07:20:16 +05:30
feat(monospace): EPUB export embeds TTF via --epub-embed-font + manifest patch
This commit is contained in:
+40
@@ -9,6 +9,7 @@ const GitOperations = require('./main/GitOperations');
|
|||||||
const PdfFontHeader = require('./main/PdfFontHeader');
|
const PdfFontHeader = require('./main/PdfFontHeader');
|
||||||
const MonospaceFontConfig = require('./main/MonospaceFontConfig');
|
const MonospaceFontConfig = require('./main/MonospaceFontConfig');
|
||||||
const ExportCss = require('./main/ExportCss');
|
const ExportCss = require('./main/ExportCss');
|
||||||
|
const EpubFontEmbedder = require('./main/EpubFontEmbedder');
|
||||||
|
|
||||||
// Add MiKTeX to PATH for LaTeX support
|
// Add MiKTeX to PATH for LaTeX support
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@@ -2838,6 +2839,45 @@ function performExportWithOptions(format, options) {
|
|||||||
} else if (format === 'confluence' || format === 'jira') {
|
} else if (format === 'confluence' || format === 'jira') {
|
||||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`;
|
pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`;
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
|
} else if (format === 'epub') {
|
||||||
|
// Embed the active monospace TTF into EPUB so code blocks render in
|
||||||
|
// JetBrains Mono / Fira Code regardless of the reader's installed fonts.
|
||||||
|
if (pandocSupportsEpubEmbedFont()) {
|
||||||
|
const familyKey = readSettingsJsonCached().monospaceFont || 'jetbrains-mono';
|
||||||
|
const regular = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 400);
|
||||||
|
const bold = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 700);
|
||||||
|
[regular, bold].filter(Boolean).forEach((p) => {
|
||||||
|
pandocCmd += ` --epub-embed-font="${p}"`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runPandocCmd(pandocCmd, async (error) => {
|
||||||
|
if (error) {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
'Export Error',
|
||||||
|
sanitizeErrorMessage(`Failed to export EPUB: ${error.message}`)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Patch the OPF manifest so readers can locate the embedded font.
|
||||||
|
// Skip silently if the file is missing or pandoc didn't embed.
|
||||||
|
try {
|
||||||
|
const familyKey = readSettingsJsonCached().monospaceFont || 'jetbrains-mono';
|
||||||
|
const regular = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 400);
|
||||||
|
const fonts = [];
|
||||||
|
if (regular) fonts.push({ path: regular, family: familyKey === 'fira-code' ? 'Fira Code' : 'JetBrains Mono', weight: 400 });
|
||||||
|
if (fonts.length) {
|
||||||
|
const patched = await EpubFontEmbedder.patchManifest(outputFile, fonts);
|
||||||
|
try {
|
||||||
|
fs.renameSync(patched, outputFile);
|
||||||
|
} catch (renameErr) {
|
||||||
|
if (typeof console !== 'undefined') console.warn('[epub] could not overwrite with patched EPUB:', renameErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (patchErr) {
|
||||||
|
if (typeof console !== 'undefined') console.warn('[epub] manifest patch failed:', patchErr.message);
|
||||||
|
}
|
||||||
|
showExportSuccess(outputFile);
|
||||||
|
});
|
||||||
} else if (format === 'mobi') {
|
} else if (format === 'mobi') {
|
||||||
// First export to EPUB, then try ebook-convert if available
|
// First export to EPUB, then try ebook-convert if available
|
||||||
const epubFile = outputFile.replace(/\.mobi$/i, '.epub');
|
const epubFile = outputFile.replace(/\.mobi$/i, '.epub');
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const JSZip = require('jszip');
|
||||||
|
|
||||||
|
// Patches an EPUB produced by pandoc so it embeds the supplied TTF font files
|
||||||
|
// inside the OEBPS and registers them in the OPF manifest. Used after
|
||||||
|
// `pandoc --epub-embed-font=...` runs (which embeds the font data) but does
|
||||||
|
// not always add the manifest item we need for readers to discover the font.
|
||||||
|
//
|
||||||
|
// Writes a new file at `${epubPath}.patched.epub` and returns the patched path.
|
||||||
|
// Caller should overwrite the original after a successful export.
|
||||||
|
async function patchManifest(epubPath, fonts) {
|
||||||
|
const zip = await JSZip.loadAsync(fs.readFileSync(epubPath));
|
||||||
|
const opfPath = Object.keys(zip.files).find((f) => f.endsWith('content.opf'));
|
||||||
|
if (!opfPath) throw new Error('EPUB has no content.opf');
|
||||||
|
let opf = await zip.file(opfPath).async('string');
|
||||||
|
|
||||||
|
for (const { path: fontPath, family, weight } of fonts) {
|
||||||
|
const filename = path.basename(fontPath);
|
||||||
|
const inFontDir = `OEBPS/fonts/${filename}`;
|
||||||
|
zip.file(inFontDir, fs.readFileSync(fontPath));
|
||||||
|
if (!opf.includes(filename)) {
|
||||||
|
const safeFamily = String(family || 'Font').replace(/\s+/g, '-');
|
||||||
|
const item = `<item id="font-${safeFamily}-${weight}" href="${inFontDir}" media-type="application/x-font-ttf"/>`;
|
||||||
|
if (opf.includes('</manifest>')) {
|
||||||
|
opf = opf.replace('</manifest>', `${item}</manifest>`);
|
||||||
|
} else {
|
||||||
|
// OPF without a manifest element (unusual but tolerated): inject one
|
||||||
|
// just before </package> so the font item is still discoverable.
|
||||||
|
opf = opf.replace('</package>', `<manifest>${item}</manifest></package>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file(opfPath, opf);
|
||||||
|
const buf = await zip.generateAsync({ type: 'nodebuffer' });
|
||||||
|
const tmp = `${epubPath}.patched.epub`;
|
||||||
|
fs.writeFileSync(tmp, buf);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { patchManifest };
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const JSZip = require('jszip');
|
||||||
|
const EpubFontEmbedder = require('../src/main/EpubFontEmbedder');
|
||||||
|
|
||||||
|
describe('EpubFontEmbedder.patchManifest', () => {
|
||||||
|
const fixturesDir = path.join(__dirname, 'fixtures');
|
||||||
|
const epubPath = path.join(fixturesDir, 'fake.epub');
|
||||||
|
const fontPath = path.join(fixturesDir, 'fake.ttf');
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
fs.mkdirSync(fixturesDir, { recursive: true });
|
||||||
|
fs.writeFileSync(fontPath, 'fake-ttf-binary');
|
||||||
|
const zip = new JSZip();
|
||||||
|
zip.file('OEBPS/content.opf', '<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf"></package>');
|
||||||
|
fs.writeFileSync(epubPath, await zip.generateAsync({ type: 'nodebuffer' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => fs.rmSync(fixturesDir, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
test('adds a manifest entry referencing the TTF when missing', async () => {
|
||||||
|
const patched = await EpubFontEmbedder.patchManifest(epubPath, [
|
||||||
|
{ path: fontPath, family: 'JetBrains Mono', weight: 400 },
|
||||||
|
]);
|
||||||
|
const out = fs.readFileSync(patched);
|
||||||
|
const zip = await JSZip.loadAsync(out);
|
||||||
|
const opf = await zip.file('OEBPS/content.opf').async('string');
|
||||||
|
expect(opf).toMatch(/<item[^>]*href="OEBPS\/fonts\/fake\.ttf"/);
|
||||||
|
expect(opf).toMatch(/<item[^>]*media-type="application\/x-font-ttf"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user