Skip to content

Commit 0e1a38b

Browse files
committed
feat: 本地嵌入 Font Awesome 字体,修复 Brands 被映射成 Arial
本项目不实现通用字体嵌入(由云端完成),但 Font Awesome 的 icon 字形必须随 PPTX 走,否则在没装 FA 的机器上图标会变方块。 - 新增预构建的 PowerPoint 兼容 EOT(FA5/FA6: solid/regular/brands + v4compatibility),用 ../html2pptx 的 sanitize+sfnttool 一次性 生成,运行时不再做 ttf→eot 转换 - 新增 src/utils/fa-font-embedder.ts:扫描 PPTX XML 里的 typeface, 只嵌入 FA 字体,更新 [Content_Types].xml / presentation.xml / .rels - 在 convertHtmlToPptx 生成 PPTX 后调用嵌入(仅本地模式) - 修复 Brands 既有 bug:normalizeFontAwesomeFreeFamily 跳过 brands, 导致 --platform 启用时 fab 图标被错误映射成 Arial;新增 normalizeFontAwesomeFamily 统一处理 Solid/Regular/Brands/v4
1 parent 229f64e commit 0e1a38b

14 files changed

Lines changed: 378 additions & 4 deletions
79.7 KB
Binary file not shown.
14 KB
Binary file not shown.
82 KB
Binary file not shown.
114 KB
Binary file not shown.
24.7 KB
Binary file not shown.
144 KB
Binary file not shown.
4.96 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@deckflow/deckhtml",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Convert HTML to PPTX or PNG presentations",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -14,6 +14,7 @@
1414
"scripts": {
1515
"build": "tsc",
1616
"build:font-index": "npx tsx src/bin/build-embed-font-index.ts",
17+
"build:fa-eot": "node scripts/build-fa-eot.mjs",
1718
"dev": "tsc --watch",
1819
"clean": "rm -rf dist/",
1920
"link:dev": "npm run build && npm link",

scripts/build-fa-eot.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// One-time script: generate PowerPoint-compatible EOTs from Font Awesome TTFs.
2+
// Uses html2pptx's sanitize + sfnttool pipeline. Output is committed to deckhtml.
3+
// Run from deckhtml root: node scripts/build-fa-eot.mjs
4+
import { execFileSync } from 'child_process';
5+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
6+
import os from 'os';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const DECKHTML_ROOT = path.resolve(__dirname, '..');
12+
const HTML2PPTX_ROOT = path.resolve(DECKHTML_ROOT, '../html2pptx');
13+
const FONTS_ROOT = path.resolve(DECKHTML_ROOT, '../../fonts/fonts/font-awesome');
14+
const OUT_ROOT = path.resolve(DECKHTML_ROOT, 'assets/fonts/font-awesome');
15+
16+
const SANITIZE_SCRIPT = path.join(HTML2PPTX_ROOT, 'tools/sanitize-ttf-for-eot.py');
17+
const SFNTTOOL_JAR = path.join(HTML2PPTX_ROOT, 'tools/sfnttool.jar');
18+
19+
const TARGETS = [
20+
{ ver: '6', src: 'fa-solid-900.ttf', out: 'fa-solid-900.eot' },
21+
{ ver: '6', src: 'fa-regular-400.ttf', out: 'fa-regular-400.eot' },
22+
{ ver: '6', src: 'fa-brands-400.ttf', out: 'fa-brands-400.eot' },
23+
{ ver: '6', src: 'fa-v4compatibility.ttf', out: 'fa-v4compatibility.eot' },
24+
{ ver: '5', src: 'fa-solid-900.ttf', out: 'fa-solid-900.eot' },
25+
{ ver: '5', src: 'fa-regular-400.ttf', out: 'fa-regular-400.eot' },
26+
{ ver: '5', src: 'fa-brands-400.ttf', out: 'fa-brands-400.eot' },
27+
];
28+
29+
function run(cmd, args, opts = {}) {
30+
execFileSync(cmd, args, { stdio: 'inherit', timeout: 300_000, ...opts });
31+
}
32+
33+
function buildOne({ ver, src, out }) {
34+
const ttfIn = path.join(FONTS_ROOT, ver, src);
35+
if (!existsSync(ttfIn)) {
36+
console.warn(`skip (missing source TTF): ${ttfIn}`);
37+
return;
38+
}
39+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fa-eot-'));
40+
try {
41+
const sanitizedTtf = path.join(tmp, 'sanitized.ttf');
42+
const eotOut = path.join(tmp, out);
43+
run('python3', [SANITIZE_SCRIPT, ttfIn, sanitizedTtf]);
44+
run('java', ['-jar', SFNTTOOL_JAR, '-e', '-x', '-h', sanitizedTtf, eotOut]);
45+
const outDir = path.join(OUT_ROOT, ver);
46+
mkdirSync(outDir, { recursive: true });
47+
writeFileSync(path.join(outDir, out), readFileSync(eotOut));
48+
console.log(`✓ ${ver}/${out} (${readFileSync(eotOut).length} bytes)`);
49+
} finally {
50+
rmSync(tmp, { recursive: true, force: true });
51+
}
52+
}
53+
54+
import fs from 'fs';
55+
for (const t of TARGETS) buildOne(t);
56+
console.log('Done.');

src/api.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
isBold,
2222
isItalic,
2323
normalizeFontAwesomeFreeFamily,
24+
normalizeFontAwesomeFamily,
2425
parseScriptFontFaces,
2526
} from './utils/style';
2627
import {
@@ -29,6 +30,7 @@ import {
2930
} from './utils/textScript';
3031
import { buildPlatformFontContext, PlatformFontContext } from './utils/platformFontMap';
3132
import { runQuietly } from './utils/quiet';
33+
import { embedFontAwesomeFonts } from './utils/fa-font-embedder';
3234
import { buildElementStats, buildFontStats, buildSimplifiedStats } from './conversion-report';
3335

3436
/**
@@ -121,6 +123,11 @@ function collectFontsFromElements(
121123
registerFont(faFreeFace, styles, false);
122124
return;
123125
}
126+
const faFace = normalizeFontAwesomeFamily(styles.fontFamily, styles.fontWeight?.toString());
127+
if (faFace) {
128+
registerFont(faFace, styles, false);
129+
return;
130+
}
124131

125132
const scripts = new Set(
126133
texts.flatMap((text) => collectTextScripts(text, containerText))
@@ -483,10 +490,15 @@ export async function convertHtmlToPptx(
483490
const data = await generator.generate(mergedSlidesMap);
484491
const slideCount = generator.getSlideCount();
485492

493+
// Font Awesome is the only font family embedded locally — its icon glyphs must
494+
// travel with the PPTX or icons render as empty boxes on machines without FA.
495+
// All other font embedding is handled by the cloud pipeline.
496+
const dataWithFaFonts = await embedFontAwesomeFonts(data);
497+
486498
reportUsedFonts(usedFontsDeduped, fontStats);
487499

488500
return {
489-
data,
501+
data: dataWithFaFonts,
490502
usedFonts: usedFontsDeduped,
491503
slideCount,
492504
stats: {

0 commit comments

Comments
 (0)