docx 自定义字体完全指南:使用 JS/TS 在 Word 文档中嵌入 TTF/OTF 字体

原创2026-09-16 17:00:4784 阅读
文章标签:文档

导读

本篇指南围绕 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.odttffont2.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.odttffonts/font2.odttffonts/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 属性)中

通过 Paragraphrun 属性设置,让段落内所有 run 默认使用该字体:

new Paragraph({
    run: {
        font: "MyCustomFont",
    },
    children: [new TextRun("All text in this paragraph uses the custom font")],
});

在文档默认样式中

Documentstyles.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 的关系;createFontTablefont-table.ts 中按数组顺序为每个字体生成 w:font 节点,因此数组顺序即 zip 包内 font1.odttffont2.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

将一次自定义字体嵌入串起完整调用链,便于理解端到端行为:

  1. Document 构造器把 fonts 选项交给 FontWrappersrc/file/file.ts);
  2. FontWrapper 为每个字体生成唯一 fontKey(GUID)、创建 w:fonts 字体表并登记关系(font-wrapper.ts);
  3. createFontTable 生成 w:fonts 根元素,每个字体对应一个 w:font 节点(font-table.ts);
  4. createRegularFont / createFont 填充 w:font 的子元素:w:charsetw:familyw:pitchw:embedRegular(含 w:fontKeyr:id),以及一组默认字体签名 w:sigcreate-regular-font.ts);
  5. 打包阶段 next-compiler 读取 FontWrapper 的字体数据,按 font${i+1}.odttf 写入 zip 的 word/fonts/ 目录(next-compiler.ts)。

关于生成的 XML 形态,font.spec.ts 给出了精确断言,例如一个常规字体对应 w:font 节点含 w:namew:charsetw:familyw:pitchw:embedRegularw:fontKey 属性(键被花括号包裹,如 {00000000-0000-0000-0000-000000000000})。这也印证了自定义字体的 name 只存在于 w:font 显示名,而与包内文件路径完全解耦。

完整示例与仓库资源

仓库提供了两个可直接运行的自定义字体 demo:

相关参考文档还包括 styling-with-xml.md(基于 XML 的样式定制)与 styling-with-js.md(基于 JS 的样式定制),可将字体设置与更完整的样式体系结合使用。

小结

  • Documentfonts 数组中注册 { 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 打开时自动还原。
docx