当一份 Word 文档要在多台设备、多种环境下流转时,字体是最容易出问题的环节:排版好的文档换台机器打开,字形、字号甚至分页都可能变样。稳妥的做法是先把文档里实际用到的字体盘点清楚,再把必须保留的字体随文档一起嵌入。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
获取文档中使用的字体列表
获取字体列表的核心流程分为三个阶段:首先通过 FetchFileToVFS 将目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,逐层遍历「节 → 段落 → 子对象」,从每个 TextRange 的 CharacterFormat 中取出字体名称、字号与文字颜色,并按三者组合去重;最后把结果拼成文本写入 VFS,读取回来后封装为 Blob 生成下载链接。
去重这一步值得留意:Map 的键必须是能按值比较的原始类型。如果把 { size, name } 这样的对象直接当作键,每次循环新建的对象都是不同的引用,Map 无法命中已有项,去重会完全失效。因此这里把字体名、字号、颜色拼成一个字符串来作键。
function App() {
const GetListOfUsingFonts = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将目标 Word 文档载入 VFS
const inputFileName = "GetListOfUsingFonts.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 以「字体名|字号|颜色」作为键去重
const fontMap = new Map();
// 遍历每一节
for (let i = 0; i < doc.Sections.Count; i++) {
const section = doc.Sections.get_Item(i);
// 遍历节中的每一个段落
for (let j = 0; j < section.Body.Paragraphs.Count; j++) {
const paragraph = section.Body.Paragraphs.get_Item(j);
// 遍历段落中的每一个子对象
for (let k = 0; k < paragraph.ChildObjects.Count; k++) {
const obj = paragraph.ChildObjects.get_Item(k);
if (!(obj instanceof docModule.TextRange)) continue;
const format = obj.CharacterFormat;
const key = `${format.FontName}|${format.FontSize}|${format.TextColor.Name}`;
fontMap.set(key, {
name: format.FontName,
size: format.FontSize,
color: format.TextColor.Name
});
}
}
}
// 拼接输出内容
const lines = [];
for (const font of fontMap.values()) {
lines.push(`Font Name: ${font.name}, Size: ${font.size}, Color: ${font.color}`);
}
// 定义输出文件名并写入 VFS
const outputFileName = "GetListOfUsingFonts_out.txt";
window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join("\n"));
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
doc.Dispose();
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>获取Word文档中使用的字体列表</h1>
<button onClick={GetListOfUsingFonts}>开始</button>
</div>
);
}
export default App;
统计字体列表后生成的文本文件内容

嵌入私有字体
嵌入私有字体的核心流程同样分为三个阶段:首先通过 FetchFileToVFS 把待嵌入的字体文件连同文档一起载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,在文档中写入一段使用该字体的文字,并把 EmbedFontsInFile 设为 true、调用 AddPrivateFont 注册字体文件的路径与注册名;最后保存文档并从 VFS 读取,封装为 Blob 后生成下载链接。
EmbedFontsInFile 与 AddPrivateFont 都是保存阶段才生效的配置,必须写在 SaveToFile 之前。PrivateFontPath 的第一个参数是字体在文档中的注册名,需要与 CharacterFormat.FontName 设置的值完全一致,否则 Word 找不到匹配项。
function App() {
const EmbedPrivateFont = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将待嵌入的私有字体文件载入 VFS
await window.spire.FetchFileToVFS("PT Serif Caption.ttf", "/Library/Fonts/", `${process.env.PUBLIC_URL}static/font/`);
// 将目标 Word 文档载入 VFS
const inputFileName = "EmbedPrivateFont.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 在第一节末尾追加一个段落,并套用私有字体
const p = doc.Sections.get_Item(0).AddParagraph();
const range = p.AppendText("Quarterly Operations Review");
range.CharacterFormat.FontName = "PT Serif Caption";
range.CharacterFormat.FontSize = 20;
// 开启字体嵌入,并把私有字体文件注册到文档中
doc.EmbedFontsInFile = true;
doc.AddPrivateFont(new docModule.PrivateFontPath("PT Serif Caption", "PT Serif Caption.ttf"));
// 定义输出文件名
const outputFileName = "EmbedPrivateFont_out.docx";
// 将文档保存到 VFS
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>在Word文档中嵌入私有字体</h1>
<button onClick={EmbedPrivateFont}>开始</button>
</div>
);
}
export default App;
嵌入私有字体后生成的文档效果

常见问题
输出的字体列表里同一个字体重复出现很多次
原因:如果沿用「先用一个对象保存字体名与字号,再把这个对象当作 Map 的键」的写法,去重是不会生效的。JavaScript 中对象属于引用类型,Map 比较键时用的是引用相等而非值相等;每轮循环新建的对象都是全新引用,fontMap.has(font) 永远返回 false,于是文档中出现多少次 TextRange,列表里就会输出多少行,同一字体反复出现。
解决:改用原始类型作为键,把参与去重的字段拼成字符串(如 字体名|字号|颜色),保证内容相同的字体得到同一个键:
const format = obj.CharacterFormat;
// 用字符串作键:内容相同即视为同一项,去重才会生效
const key = `${format.FontName}|${format.FontSize}|${format.TextColor.Name}`;
fontMap.set(key, {
name: format.FontName,
size: format.FontSize,
color: format.TextColor.Name
});
如果只需要字体名称这一层去重,用 Set 收集 format.FontName 即可。
字体已经嵌入,换台电脑打开却仍是替代字体
原因:常见的有三种情形。其一是调用时机不对——AddPrivateFont 只是把字体文件登记到内存中的文档对象上,真正写入 docx 的字体表与字体部件发生在保存阶段,因此 EmbedFontsInFile = true 和 AddPrivateFont(...) 都必须写在 SaveToFile 之前,漏掉 EmbedFontsInFile 则字体文件根本不会被写入。其二是字体名不匹配——PrivateFontPath 的第一个参数是文档中的注册名,必须与 CharacterFormat.FontName 的值完全一致(含空格与大小写),差一个字符就会匹配失败。其三是字体文件自身的嵌入许可——部分商用字体的 OS/2 表中 fsType 位禁止嵌入,Word 会直接忽略这类字体,此时只能更换字体或改用可嵌入的授权版本。
解决:把字体嵌入的全部配置集中放在保存之前,并保证注册名与字体名逐字一致:
// 字体名与注册名保持完全一致
const FONT_NAME = "PT Serif Caption";
const range = p.AppendText("Quarterly Operations Review");
range.CharacterFormat.FontName = FONT_NAME;
range.CharacterFormat.FontSize = 20;
// 开启嵌入并登记字体文件,两者都必须在 SaveToFile 之前
doc.EmbedFontsInFile = true;
doc.AddPrivateFont(new docModule.PrivateFontPath(FONT_NAME, "PT Serif Caption.ttf"));
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







