D2 字体嵌入机制全解析:SVG 内嵌字体、自定义字体接入与跨平台回退
【免费下载链接】d2D2 is a modern diagram scripting language that turns text to diagrams.项目地址: https://gitcode.com/GitHub_Trending/d2/d2
D2(d2lang/d2)是一款"把文本变成图表"的现代图表脚本语言,其 SVG 渲染器会把字体文件直接以 base64 数据的形式内嵌进 SVG,从而保证输出结果确定、且无需任何网络请求即可在浏览器中完整呈现。本篇指南以d2renderers/d2fonts/README.md为核心骨架,结合仓库源码,系统讲解 D2 的内嵌字体机制、如何为 D2 接入自己的字体(如 Helvetica)、内置字体家族的资源组织方式,以及主字体缺字时的回退策略,读完你就能自行定制 D2 输出中的字体并理解其底层原理。
为什么要把字体直接嵌进 SVG
d2renderers/d2fonts/README.md开篇就给出了这一设计的两个核心动机:
- 确定性输出(deterministic outputs):字体随 SVG 一起打包,不依赖查看端机器上是否安装了对应字体,同一份
.d2源码在任何环境渲染出的字形完全一致; - 无网络调用(load without a network call):SVG 打开即完整渲染,不依赖外链的字体 CDN,也不受离线场景限制。
因此,D2 的 SVG 输出中会携带形如data:application/font-woff;base64,AAAA...的字体 data URI(见 d2fonts_common.go),这些 base64 字符串正是嵌入字体的载体。
内置字体家族与资源目录布局
d2fonts包默认内置三个字体家族(定义于 d2fonts_common.go):
| 字体家族 | 类型 | 用途 |
|---|---|---|
SourceSansPro | 无衬线体 | 默认字体(对应--theme下的普通文本) |
SourceCodePro | 等宽字体 | 代码块 / 等宽场景(对应mono) |
HandDrawn(FuzzyBubbles) | 手绘风格 | 草图/手绘主题 |
每个家族按四种字型(style)组织,常量定义在 d2fonts_common.go:regular、bold、semibold、italic。字号从 XS 到 XXXL 共 8 档,取值 13、14、16、20、24、28、32(见 d2fonts_common.go)。
资源文件按构建目标分两种形态存放于 d2renderers/d2fonts/encoded/:
- 非 WASM(原生)构建:嵌入
encoded/*.txt(即 base64 编码的 WOFF 文本)与ttf/*.ttf原文件,见 d2fonts_embed.go 同目录的 d2fonts_embed.go 中//go:embed encoded/SourceSansPro-Regular.txt等指令; - WASM 构建:嵌入
encoded/*.txt.br(Brotli 压缩后的编码文本),运行时解压,见 d2fonts_embed_wasm.go 与 d2fonts_embed_wasm.go 中的compression.DecompressBrotli解压逻辑。两种形态的存在是为了压缩 JS/WASM 包的体积,源码注释(d2fonts_common.go)对此有明确说明。
每个内嵌字体的字型映射在init()中注册到FontEncodings(编码字符串)与FontFaces(TTF 字节)两个全局注册表(d2fonts_embed.go)。其中HandDrawn家族没有独立的 italic 与 semibold 字型,会分别复用 regular 和 bold(d2fonts_embed.go)。
此外,D2 还把内置字体(含 Noto Color Emoji)的 SHA-256 摘要固定注册,供光栅化管线做身份校验,见 bundled_faces.go 中的bundledFaceSpecs与fontface.RegisterBundledFace调用。
如何接入自定义字体(以 Helvetica 为例)
README 以Helvetica为例给出了添加自定义字体的完整要求。第一步:提供 Truetype 字形文件(.ttf),放入d2renderers/d2fonts/ttf/目录:
./ttf/Helvetica-Bold.ttf ./ttf/Helvetica-Italic.ttf ./ttf/Helvetica-Regular.ttf第二步:提供这些字体的编码版本,mimetype 必须为application/font-woff,放入d2renderers/d2fonts/encoded/目录,与 TTF 一一对应:
./ttf/Helvetica-Bold.txt ./ttf/Helvetica-Italic.txt ./ttf/Helvetica-Regular.txt注意两点:其一,编码文件内容本质上就是data:application/font-woff;base64,<base64>形式的 WOFF 数据文本(README 中的./ttf/Helvetica-*.txt为相对encoded/目录的示意路径,实际目录见 d2renderers/d2fonts/encoded/);其二,若想贡献字体给 D2 仓库,该字体必须具有开源许可证(README 明确要求:"If you include a font to contribute, it must have an open license.")。
仓库源码为"先转 WOFF 再 base64"这一流程提供了直接实现:AddFontStyle会调用fontlib.Sfnt2Woff(ttf)把 TTF 转成 WOFF,再用base64.StdEncoding.EncodeToString编码并拼接data:application/font-woff;base64,前缀(d2fonts_common.go)。
缺失字型的回退行为
AddFontFamily是运行时注册自定义字族的入口(d2fonts_common.go),签名如下:
func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []byte) (*FontFamily, error)- 传入的四个字型 TTF 均可为空(
nil); - 某个字型缺失时,会自动回退到 SourceSansPro 的对应字型(如无 italic 则用 SourceSansPro-Italic),保证图表的粗体、斜体仍然成立(见 d2fonts_common.go 等处的 fallback 分支);
- 注册成功后,该自定义家族会被追加进全局
FontFamilies列表,从而参与后续渲染。
运行时的字形子集化
D2 并非把整份字体嵌入每个 SVG,而是按文本语料做子集化:Font.GetEncodedSubset(corpus string)会先去重提取语料中的唯一字符(uniqueChars),调用font.UTF8CutFont(fontBuf, uniqueChars)裁剪出仅含所需字形的最小 TTF,再经fontlib.Sfnt2Woff转 WOFF 并 base64 编码(d2fonts_common.go)。若子集化失败(如字体结构特殊),则回退到完整字体的编码(FontEncodings.Get(f)),保证功能不中断。这解释了为什么最终 SVG 既自带字体、又不会过分臃肿。
CLI 层面对字体的配置
自定义字体不仅能在仓库内硬编码,还可在命令行直接指定 TTF 路径。d2cli提供了 8 个字体相关参数(main.go),均有环境变量与 flag 两种写法:
| 环境变量 | Flag | 说明 | 缺省值 |
|---|---|---|---|
D2_FONT_REGULAR | --font-regular | 常规字体 TTF 路径 | Source Sans Pro Regular |
D2_FONT_ITALIC | --font-italic | 斜体字体 TTF 路径 | Source Sans Pro Italic |
D2_FONT_BOLD | --font-bold | 粗体字体 TTF 路径 | Source Sans Pro Bold |
D2_FONT_SEMIBOLD | --font-semibold | 半粗体字体 TTF 路径 | Source Sans Pro Semibold |
D2_FONT_MONO | --font-mono | 等宽常规字体 TTF 路径 | Source Code Pro Regular |
D2_FONT_MONO_BOLD | --font-mono-bold | 等宽粗体 TTF 路径 | Source Code Pro Bold |
D2_FONT_MONO_ITALIC | --font-mono-italic | 等宽斜体 TTF 路径 | Source Code Pro Italic |
D2_FONT_MONO_SEMIBOLD | --font-mono-semibold | 等宽半粗体 TTF 路径 | Source Code Pro Semibold |
例如:
d2 --font-regular=/path/to/Helvetica-Regular.ttf \ --font-bold=/path/to/Helvetica-Bold.ttf \ --font-italic=/path/to/Helvetica-Italic.ttf \ --font-semibold=/path/to/Helvetica-Semibold.ttf \ input.d2 output.svgloadFonts实现(main.go)会读取各路径的 TTF 字节,并调用d2fonts.AddFontFamily("custom", ...)与d2fonts.AddFontFamily("customMono", ...)分别注册主字体族与等宽字体族。也就是说,CLI 方式无需预先准备 WOFF 编码文本——TTF 会在加载时由Sfnt2Woff现场转换,而 README 所述的*.txt编码文件是为"内置/贡献字体"这种静态嵌入场景准备的。
主字体缺字时的回退解析
当文本中的字符超出主字体覆盖范围(如中文、阿拉伯文、表情符号)时,d2fonts提供两级回退:
- 内置 Noto Color Emoji 兜底:
BundledFallbackResolver优先用仓库固定的 Noto Color Emoji(NotoColorEmoji-COLRv1-v2.051.ttf.br,见 encoded/)解析支持的符号与 emoji,保证跨机器字形确定(bundled_fallback.go)。在 js/wasm 构建下该内置字形缺失,请求直接透传给下游; - 系统字体回退:
SystemFallbackResolver按操作系统约定的字体根目录扫描候选字体(macOS 的/System/Library/Fonts、/Library/Fonts,Linux/BSD 的/usr/share/fonts、/usr/local/share/fonts,Windows 的%WINDIR%\Fonts,见 fallback.go),仅当内置字体无法覆盖时才动用。它通过 cmap 探针轻量检查覆盖范围(fallback.go),并按字形权重、斜体、等宽等属性为候选排序(fallback.go),同时用SystemFallbackLimits(目录条目数、文件数、扫描字节、覆盖检查次数等)约束整个检索过程(fallback.go),避免无界开销。
这一两级回退机制在 fallback_test.go 等测试中都有覆盖,包括"回退字节必须独立拷贝、不得与原文件共享底层数组"等细节。
小结
- D2 通过
data:application/font-woff;base64,...将字体内嵌进 SVG,兼顾确定性输出与离线可用; - 内置家族为 SourceSansPro、SourceCodePro、HandDrawn(FuzzyBubbles),每个家族含 regular/bold/semibold/italic 四种字型;原生构建嵌入
encoded/*.txt,WASM 构建嵌入 Brotli 压缩的encoded/*.txt.br; - 自定义字体需提供 TTF 及其 WOFF base64 编码文本(mimetype 为
application/font-woff),贡献给仓库的字体必须有开源许可证; - 运行时按语料做字形子集化后再编码,避免 SVG 冗余;CLI 提供
--font-regular等 8 个参数直接加载外部 TTF; - 主字体缺字时,先走内置 Noto Color Emoji 兜底,再按严格限额扫描系统字体目录,保证任意脚本文本都能确定性渲染。
相关源码入口:d2fonts_common.go、d2fonts_embed.go、d2fonts_embed_wasm.go、fallback.go、bundled_fallback.go、CLI 字体参数。
【免费下载链接】d2D2 is a modern diagram scripting language that turns text to diagrams.项目地址: https://gitcode.com/GitHub_Trending/d2/d2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考