docx 自定义字体完全指南:使用 JS/TS 在 Word 文档中嵌入 TTF/OTF 字体
导读
本篇指南围绕 docx(一个用 JS/TS 以声明式 API 生成和修改 .docx 文件的库,支持 Node.js 与浏览器)的字体嵌入能力展开,讲解如何将自定义字体直接打包进文档,使文档在任意系统上打开时都保持一致的排版外观。读完本文后,你将掌握在 Document 构造器中注册字体、通过 TextRun / 段落 / 默认样式三种层级应用字体、嵌入多字体与混用系统字体,以及在浏览器环境中加载字体并导出文档的完整方案,并能理解 characterSet 参数对非拉丁文字渲染的关键作用。
为什么需要嵌入自定义字体
系统字体依赖查看者本机安装环境:如果对方电脑上没有你使用的字体,Word 会自动替换为其他字体,导致版式、字距甚至换行都发生变化。将字体文件直接嵌入 .docx 包内,可确保"所见即所得"。docx 库通过 Document 构造器的 fonts 选项接收一组字体描述对象,在打包阶段把字体数据写入 word/fonts/ 目录,并在 fontTable.xml(即 OOXML 的 w:fonts 元素)中登记字体定义,最终生成一个自包含的文档。
嵌入字体的基本用法
Node.js 环境下,用 fs.readFileSync 读取字体文件得到 Buffer,再传给 Document:
import * as fs from "fs";
import { CharacterSet, Document, Paragraph, TextRun } from "docx";
const fontData = fs.readFileSync("./fonts/MyCustomFont.ttf");
const doc = new Document({
fonts: [
{
name: "MyCustomFont",
data: fontData,
characterSet: CharacterSet.ANSI,
},
],
sections: [
{
children: [
new Paragraph({
children: [
new TextRun({
text: "This text uses a custom font",
font: "MyCustomFont",
}),
],
}),
],
},
],
});
随后通过 Packer.toBuffer(doc)(Node)或 Packer.toBlob(doc)(浏览器)导出即可。库内对 fonts 选项的处理位于 src/file/file.ts,其中用 new FontWrapper(options.fonts ?? <a href="https://link.gitcode.com/i/3b41fb4f7d322070c2c5a2ffa4ff8ed9" target="_blank">]) 构造字体包装器;FontWrapper 负责建立字体表与关系(Relationships),参见 [src/file/fonts/font-wrapper.ts。
字体名称(Font Names)
name 属性是文档中引用该字体的家族名,它可以包含空格和非 ASCII 字符,例如 "EB Garamond"、"Noto Sans JP"。
需要特别注意的是:docx 内部在 zip 包中为嵌入字体数据使用顺序文件名(font1.odttf、font2.odttf……),而不是你传入的字体名。这样设计是有原因的——FontWrapper 源码注释指出,Word 把嵌入字体路径当作字面文件名处理,若路径里出现空格或非 ASCII 字符会被直接拒绝(对应上游 issue #3019)。因此你选择的 name 只影响 w:font name="..." 中的显示名,不会影响文件兼容性;底层 zip 条目路径由 word/fonts/font<N>.odttf 统一生成。
该行为有测试用例背书:font-wrapper.spec.ts 验证了即使字体名为 "EB Garamond"、"Source Serif 4"、"Crimson Pro",生成的关系目标也必然是 fonts/font1.odttf、fonts/font2.odttf、fonts/font3.odttf,且不会出现带空格的字面文件名。打包阶段 next-compiler.ts 同样按 font${i + 1}.odttf 的顺序写入 zip 条目,与 FontWrapper 中的关系目标严格对应。
字体选项(Font Options)
fonts 数组中每个元素的类型定义位于 src/file/fonts/font-table.ts:
| Property | Type | Required | 说明 |
|---|---|---|---|
| name | string |
必填 | 文档中引用的字体名 |
| data | Buffer |
必填 | 字体文件数据(TTF、OTF 等) |
| characterSet | CharacterSet |
可选 | 字体的字符集标识,控制 w:charset |
三个字段的语义:name 用于在 TextRun/样式中引用;data 是字体文件的原始字节;characterSet 告诉 Word 如何解释字体的字符编码。
字符集(Character Sets)与非拉丁文字渲染
characterSet 会写入 fontTable.xml 中的 w:charset 元素(对应源码 createFont 生成的 w:charset 节点)。虽然该字段可选,但对非拉丁文字至关重要——不指定时 Word 可能回退到默认编码,导致字符渲染错误。
CharacterSet 常量定义在 src/file/fonts/font.ts,映射为 OOXML 约定的十六进制标识符,可用值如下:
| 值 | 说明 |
|---|---|
CharacterSet.ANSI |
标准 ANSI 字符 |
CharacterSet.DEFAULT |
默认字符集 |
CharacterSet.SYMBOL |
符号字符 |
CharacterSet.MAC |
Macintosh 字符 |
CharacterSet.SHIFTJIS |
日文 Shift-JIS |
CharacterSet.HANGUL |
韩文 Hangul |
CharacterSet.JOHAB |
韩文 Johab |
CharacterSet.GB2312 |
简体中文 |
CharacterSet.CHINESEBIG5 |
繁体中文 |
CharacterSet.GREEK |
希腊字符 |
CharacterSet.TURKISH |
土耳其字符 |
CharacterSet.VIETNAMESE |
越南字符 |
CharacterSet.HEBREW |
希伯来字符 |
CharacterSet.ARABIC |
阿拉伯字符 |
CharacterSet.BALTIC |
波罗的海字符 |
CharacterSet.RUSSIAN |
西里尔字符 |
CharacterSet.THAI |
泰文字符 |
CharacterSet.EASTEUROPE |
东欧字符 |
从源码可见每个值本质是十六进制字符串(如 ANSI 为 "00"、MAC 为 "4D"、GB2312 为 "86"、THAI 为 "DE"),这意味着你既可以引用枚举常量,也可以直接传字符串字面量(92-declarative-custom-fonts.ts 中即传入了 characterSet: "00")。embedding 中文文档时建议使用 CharacterSet.GB2312,日文使用 CharacterSet.SHIFTJIS。
使用自定义字体的三种层级
在 TextRun 中
最细粒度,仅作用于单个文本片段:
new TextRun({
text: "Custom font text",
font: "MyCustomFont",
});
在段落样式(run 属性)中
通过 Paragraph 的 run 属性设置,让段落内所有 run 默认使用该字体:
new Paragraph({
run: {
font: "MyCustomFont",
},
children: [new TextRun("All text in this paragraph uses the custom font")],
});
在文档默认样式中
在 Document 的 styles.default.document.run 中设置,全文档生效:
const doc = new Document({
fonts: [{ name: "CustomFont", data: fontData, characterSet: CharacterSet.ANSI }],
styles: {
default: {
document: {
run: {
font: "CustomFont",
},
},
},
},
sections: [
/* ... */
],
});
这一层级的完整可运行示例见 demo/92-declarative-custom-fonts.ts:它把默认 run 字体设为 Pacifico,正文中所有 TextRun(包括加粗、调整字号者)都会自动继承,无需逐个指定。
嵌入多个字体
可在 fonts 数组中一次注册多个字体,并按需在不同位置引用:
const doc = new Document({
fonts: [
{
name: "HeadingFont",
data: fs.readFileSync("./fonts/Heading.ttf"),
characterSet: CharacterSet.ANSI,
},
{
name: "BodyFont",
data: fs.readFileSync("./fonts/Body.ttf"),
characterSet: CharacterSet.ANSI,
},
],
sections: [
{
children: [
new Paragraph({
children: [
new TextRun({
text: "Heading",
font: "HeadingFont",
size: 48,
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: "Body text with a different font",
font: "BodyFont",
}),
],
}),
],
},
],
});
FontWrapper 会为每个字体分配一个唯一的混淆键(GUID,经 uniqueUuid 生成),并为每个字体建立一条类型为 .../relationships/font 的关系;createFontTable 在 font-table.ts 中按数组顺序为每个字体生成 w:font 节点,因此数组顺序即 zip 包内 font1.odttf、font2.odttf……的对应顺序。
混用系统字体与嵌入字体
嵌入字体与系统字体可以共存,只需在 font 属性中混用不同名称:
new Paragraph({
children: [
new TextRun({
text: "System font (Arial), ",
font: "Arial",
}),
new TextRun({
text: "Custom embedded font",
font: "MyCustomFont",
}),
],
});
这通常用于"正文用系统字体保持轻量,标题/品牌字体用嵌入字体保证一致"的混合排版策略。
浏览器环境下的用法
浏览器没有 fs,需先把字体文件转成 ArrayBuffer(或 base64 字符串),再包装为 Buffer:
import { CharacterSet, Document, Packer, Paragraph, TextRun } from "docx";
import { saveAs } from "file-saver";
// Fetch the font file and convert to ArrayBuffer
const response = await fetch("./fonts/MyFont.ttf");
const fontData = await response.arrayBuffer();
const doc = new Document({
fonts: [
{
name: "MyFont",
data: Buffer.from(fontData),
characterSet: CharacterSet.ANSI,
},
],
sections: [
{
children: [
new Paragraph({
children: [
new TextRun({
text: "Text with embedded font",
font: "MyFont",
}),
],
}),
],
},
],
});
// Export the document
Packer.toBlob(doc).then((blob) => {
saveAs(blob, "document.docx");
});
支持的字体格式与兼容性建议
- TTF(TrueType Font)——最常见、兼容性最广,优先推荐;
- OTF(OpenType Font)——同样受支持。
为获得最佳兼容性,尽量使用 TTF 字体。
需要注意:docx 遵循 OOXML 规范(ECMA-376)对嵌入字体做**混淆(obfuscation)**处理。实现位于 obfuscate-ttf-to-odttf.ts:取字体的 GUID 键(32 位十六进制,去掉连字符)反转字节序列,与字体文件前 32 字节做 XOR 运算后写入包内。这是规范要求(防止简单提取嵌入字体),因此包内的字体文件以 .odttf 扩展名存放,打开时 Word 会自动解混淆还原,无需用户干预。
底层原理:从 fonts 选项到 OOXML
将一次自定义字体嵌入串起完整调用链,便于理解端到端行为:
Document构造器把fonts选项交给FontWrapper(src/file/file.ts);FontWrapper为每个字体生成唯一fontKey(GUID)、创建w:fonts字体表并登记关系(font-wrapper.ts);createFontTable生成w:fonts根元素,每个字体对应一个w:font节点(font-table.ts);createRegularFont/createFont填充w:font的子元素:w:charset、w:family、w:pitch、w:embedRegular(含w:fontKey与r:id),以及一组默认字体签名w:sig(create-regular-font.ts);- 打包阶段
next-compiler读取FontWrapper的字体数据,按font${i+1}.odttf写入 zip 的word/fonts/目录(next-compiler.ts)。
关于生成的 XML 形态,font.spec.ts 给出了精确断言,例如一个常规字体对应 w:font 节点含 w:name、w:charset、w:family、w:pitch、w:embedRegular 及 w:fontKey 属性(键被花括号包裹,如 {00000000-0000-0000-0000-000000000000})。这也印证了自定义字体的 name 只存在于 w:font 显示名,而与包内文件路径完全解耦。
完整示例与仓库资源
仓库提供了两个可直接运行的自定义字体 demo:
- demo/91-custom-fonts.ts:声明式嵌入
Pacifico字体(字体文件位于 demo/assets/Pacifico.ttf),在单个段落内以不同字号、加粗、Tab 组合展示,并使用Packer.toBuffer输出My Document.docx; - demo/92-declarative-custom-fonts.ts:通过
styles.default.document.run.font声明式地让整个文档默认使用自定义字体,正文TextRun无需逐个指定字体。
相关参考文档还包括 styling-with-xml.md(基于 XML 的样式定制)与 styling-with-js.md(基于 JS 的样式定制),可将字体设置与更完整的样式体系结合使用。
小结
- 在
Document的fonts数组中注册{ name, data, characterSet },即可把 TTF/OTF 字体嵌入 .docx; name可含空格与非 ASCII,包内路径固定为font1.odttf等顺序文件名,两者解耦,天然规避 Word 对含空格路径的拒绝问题;characterSet决定w:charset,对中文、日文、阿拉伯文等非拉丁文字的正确渲染至关重要;- 应用字体有三种粒度:
TextRun、段落run属性、文档默认样式,可单独使用也可组合; - 可同时嵌入多字体、与系统字体混用;浏览器环境用
fetch+ArrayBuffer获取字体数据; - 底层按 ECMA-376 规范对字体做 GUID 混淆(XOR 前 32 字节)后以
.odttf存入包内,Word 打开时自动还原。