---
title: "字体子集与复用（05 / issue #3）"
description: "静态 TrueType 子集、字体内容身份复用、缺字检查、嵌入许可与全量保留策略。"
image: "https://whynpc9.github.io/ofdrw.net/ofdrw.net/social.svg"
---

> Documentation Index
> Fetch the complete documentation index at: https://whynpc9.github.io/ofdrw.net/llms.txt
> Use this file to discover all available pages before exploring further.

# 字体子集与复用（05 / issue #3）

新建包默认 `SubsetWhenSafe`：`OfdFontResource.Data` 是字节来源，`Id` / `OfdTextElement.FontResourceId` 绑定字体，`Bold` / `Italic` 保留样式。05 不增加另一套公开字体描述，04 的 `OfdFont` / `OfdGraphics` 可以继续绑定这些 Core 资源。不同名称或样式但相同字节的资源保留各自 ID，共用全包用字集合和一份 `FontFile`。同名不同内容始终分开。

```csharp
package.Fonts.Add(new OfdFontResource {
    Id = "10", FontName = "My licensed font", Data = File.ReadAllBytes("font.ttf")
});
page.Elements.Add(new OfdTextElement {
    FontResourceId = "10", FontName = "My licensed font", Text = "中文 Alpha"
});
var result = await new OfdPackageWriter().WriteWithResultAsync(package, output);
foreach (var face in result.FontEmbedding)
    Console.WriteLine($"{face.SourceBytes} -> {face.PayloadBytes}, subset={face.IsSubset}");
```

## 生产后端与支持边界

现有字体子集路径使用纯 .NET managed sfnt/glyf 后端，无 Python/fonttools、HarfBuzz CLI 或本地动态库依赖。Core 的 `OpenTypeFace` 是字体表读取/重建的共同来源，DOCX 集合选面、PDF 名称隔离、SVG、coverage 与子集生成共用。Native 的测量和完整嵌入源仍来自同一个 PDF 注册源；写出时才生成独立子集，不改变调用方字节、字体资源或已计算坐标。新增的可选 [HarfBuzz 预布局适配器](https://whynpc9.github.io/ofdrw.net/reference/text-prelayout/)是另一条显式新入口，不改变此子集路径。

| 范围 | 行为 |
| --- | --- |
| 静态 TTF / glyf OpenType | cmap 4/12、非 BMP、组合字形递归闭包、GSUB 1–8 保守不动点闭包；保持 GID、hmtx/vmtx、hinting、GSUB/GPOS/GDEF 原字节 |
| cmap 14 / UVS | 保留完整 14 子表及所有非默认变体 GID；输入变体序列必须存在；默认映射仍依赖普通 cmap |
| 规范化 | 加入 NFC/NFD 码点闭包，避免浏览器规范化时丢失字形 |
| TTC | `CollectionFaceIndex` 零基显式选面，默认 0；`MaximumCollectionBytes` 限制容器，`MaximumFontBytes` 限制所选 face，提取前先检查投影输出尺寸；按实际 face 内容复用；写出独立 sfnt，PDF/SVG 使用相同选面 |
| CFF/CFF2 OTF、变量字体、AAT/颜色/未知依赖表、JSTF | 全量保留，返回 `FONT_FULL_PRESERVED` 与原因；不宣称这些格式已子集化 |
| RTL/bidi 字符 | 按 Unicode 17.0.0 Bidi_Class（含规范默认值）识别 RTL/控制字符；BOM/ZWNBSP 和 LTR 脚本不误判。缺少通用 Unicode mirror closure 的 RTL 保留全量并诊断；不宣称复杂文字子集支持完整 |
| 大 BMP 字符集 | 压缩格式 4 连续 delta 段并保留格式 12；格式 4 无法容纳时全量保留，避免现有 PDFsharp 拒绝 format12-only 字体 |
| SourceXml/CGTransform/Raw、模板/注释、保留包条目 | 整包保守保留全量字体，避免裁掉未知 GID/扩展引用；已有子集保持原字节；诊断会说明 coverage 未验证的未解析字体 |

保持 GID 的代价是 `maxp` 数量和 `loca`/宽度表仍覆盖原编号；被移除的 glyph 是空 loca 区间，真实轮廓减少。共享样式解析按 `OS/2.fsSelection` 优先、缺省时 `head.macStyle` 回退，与 DOCX catalog 一致，PDF/SVG 不重复合成已存在的粗斜体。字体名称改成内容身份名称，版权、许可等 name 记录保留，以遵守 RFN 字体改名条件。OFD 仍保存 Unicode `TextCode`；不会以图片或轮廓替换文本实现减小。

历史 CFF PDF 的 Poppler 探针报告字体类型不匹配；当前 R08 对实际选中的 CFF 字体在写出前明确拒绝，不把旧产物的可保存/可抽取视为规范兼容。受限 OpenType/CFF1 的 SVG 文字与原 GID 轮廓分别见[R08 四出口能力表](https://github.com/whynpc9/ofdrw.net/blob/2489966c47833cb137fe33cdc4c3d2df60daec3a/docs/validation/r08-four-outlet-ledger.md)；它们不启用 CFF PDF。支持表只描述实际后端能力，不承诺任意字体/Word 文档保真。

## 缺字、预算及旧行为

已绑定的嵌入字体逐码点验证。Unicode 17 Default_Ignorable_Code_Point 格式控制符不要求 cmap 轮廓（UVS 序列仍验证）；RTL 控制仍触发 full 保护。普通缺字在写出 ZIP 前抛出 `InvalidDataException`，包含 `U+...`。PDF/SVG 对嵌入字体也用同一 coverage 验证。调用方应将文字绑定到包含字形的配置回退资源；库不会把缺字静默绘成方框。新增未知 XML 只阻止裁剪，不能绕过已建模文字的缺字检查。没有嵌入 Data 的 name-only 字体仍由阅读器/宿主解析，无法在写包时验证系统字体。

`OfdDocumentOptions.FontEmbedding` 和 `DocxConversionOptions.FontEmbedding` 控制字节、Unicode 用字和 GSUB/复合闭包操作预算；异步写包传递取消。重叠表在复制前拒绝。`OfdPackageWriteResult.FontEmbedding` 逐内容身份记录原/新体积、别名数、保留/原 glyph 数及原因；`Diagnostics` 可观察安全保留策略。

`Mode = OfdFontEmbeddingMode.Full` 是调用方显式选择的旧全量行为，用于对照或需要完整字形编辑的文档；不验证 coverage。对子集包新增文字，应重新绑定原字体 Data，并清除不再需要的旧 Runs/SourceXml，或生成新包；已有子集不能恢复已删除字形。读写旧未知资源不主动裁剪/重编号，也不删除可能被扩展引用的旧载荷。

## 许可与固定样例

子集化不授予字体嵌入权，不改变原字体许可证。自动子集模式尊重 `OS/2.fsType`：restricted/bitmap-only 禁止新嵌入，no-subsetting 保留全量；fsType 为 0 也不能代替调用方取得授权。Full 和未知内容保留也检查可解析字体的这些标记；无法解析的旧不透明载荷仅可保留并诊断。调用方仍须确保有权分发原载荷。

样例使用固定、可再分发的 LXGW WenKai（OFL 1.1，RFN 子集内部改名）和 Noto Sans（OFL 1.1）。来源、版本、SHA-256 和原许可见 `e2e/Ofdrw.Net.FontSubset.E2E/testdata/fonts`，不包含专有系统字体。现有子集后端未引入第三方 subset 代码；新的可选预布局适配器单独锁定 HarfBuzzSharp 与 native 资产，不承接子集输出。

执行 `e2e/Ofdrw.Net.FontSubset.E2E` 可生成同文档的 full/subset OFD、各页 SVG、经本次 OFD 导出的 PDF、固定字体的 generated-layout native/default DOCX，以及字节/身份对照 JSON。11 包实际消费脚本也运行此样例。功能、自动渲染、PNG 和 macOS Preview 的证据分列在[验收目录](https://github.com/whynpc9/ofdrw.net/blob/2489966c47833cb137fe33cdc4c3d2df60daec3a/docs/evidence/font-subset/README.md)。

补充平面（非 BMP）字形在 OFD 中仍保留原始 Unicode、cmap 与 GID。R08 的 PDF 出口仅接纳显式嵌入、授权允许嵌入与子集化、具有 Unicode 覆盖的静态单色 TrueType `glyf` face：每个文字对象最多 1024 个可见 scalar、face 最多 64 MiB，且用字不能共享有歧义的 GID。导出时只在实际裁剪过轮廓的 PDF 私有字体中将补充平面字符暂映射至空闲 BMP 私用码位，保持原轮廓 GID，再为 Type0 字体写入指向原 UTF-16 代理对的 ToUnicode；跨文字对象或签名外观若同一 PDF 字体身份对应不同 Unicode 映射，则拒绝导出。OFD 字节与文字语义不变。PDFsharp 仍逐 UTF-16 code unit 绘制，因此这是受限的适配路径，不是任意非 BMP 字体支持。未绑定的 name-only、Type1/CFF、变量/彩色字体和超出预算的补充平面 PDF 仍明确拒绝。Windows symbol cmap 仅在文字本身全部使用直接映射、无歧义的 BMP 私用区码点，字体为允许嵌入的静态单色 TrueType 时，才为 PDF/SVG 导出制作 Unicode cmap 副本；PDF 最终 FontFile2 恢复完整字体以保留嵌套复合组件并遵守禁止子集化位。低字节回退命中的普通字母仍拒绝，OFD 原字体与 TextCode 不变；SVG 与 PDF 的视觉验收分别记录。含 CGTransform 的旧包保持原字体和 XML，未建模的显式 GID 不用普通 Unicode coverage 错误拒绝；PDF/SVG 导出也明确拒绝未建模的 CGTransform，避免忽略其真实字形。

字体子集与绘图分别维护自己的行为约定。集成时继续遵守同源字体字节及既有坐标不重算的契约。

`SourceXml` 必须是一个合法 XML 元素。无法解析的 XML 在 Full/subset 两种模式都于 ZIP 写出前明确拒绝，保留调用方原字节；“未知内容保护”不承诺把非法 XML 写成合法 OFD。

## PDF 字体与控制语义

所有实际非空 PDF 文本，包括 name-only 常规字体和无 FontResource 绑定的文本，都会惰性取得物理字节、验证当前全部文本覆盖并注册内容身份。未使用普通名称资源不探测，实际空文本在 XML/语义预检后跳过 XFont。普通宿主探测失败可按配置 default、Arial 顺序尝试覆盖候选；空结果、I/O、格式或注册预算失败允许尝试下一候选，全部失败明确报错。库不未经检查地绘制原 family 或 Arial。解析结果可以缓存，当前文本覆盖必须逐次验证。物理快照按内容复用，增加粗体/斜体时登记同一字节的有效样式 alias。

宿主 TTC 的主名称探测按返回 face 名唯一匹配 name IDs4/6（完整名/PostScript 名），未命中时 IDs1/16（family）也必须唯一；不凭 index0、模糊名称或请求粗斜体猜选。多面默认/Arial 集合同样必须唯一匹配，只有配置默认的单面集合可采用唯一 face。不明或歧义按探测失败处理。名称目录在完整 face 展开前有界扫描：输入256MiB、累计名称解码1MiB、所选 face64MiB；严格拒绝畸形编码/边界。公开 Data.CollectionFaceIndex 的显式选面路径不变。字段定义见 [OpenType name 规范](https://learn.microsoft.com/en-us/typography/opentype/spec/name)。

Windows symbol-only cmap 的一般字符语义仍未建模。OFD 写包/读取编辑保全量并标识 coverage 未验证。仅当选中文字全是具有直接、唯一且有效 GID 的 BMP 私用区码点，且字体满足静态单色 TrueType 嵌入约束时，R08 才按[直接 PUA profile](https://github.com/whynpc9/ofdrw.net/blob/2489966c47833cb137fe33cdc4c3d2df60daec3a/docs/validation/r08-symbol-cmap-profile.md)为 PDF/SVG 创建 Unicode cmap 副本；偶然命中低字节回退的普通字母（如 A）和其它未建模编码仍明确拒绝。名称探测的拒绝标记在可选 catch 之外保留，symbol 回退候选跳过，不能冒充 Unicode 覆盖。未使用 symbol 资源不阻断处理。

PDF 明确拒绝语义无法保留的 default-ignorable，包括 ZWJ/ZWNJ、LRM/RLM/ALM、方向格式控制、variation selectors、WORD JOINER、不可见数学运算符、软连字符、零宽断词、CGJ、蒙古文分隔、旧方向控制和 FEFF。弃用不等于没有语义，OFD 内容里的 FEFF 首字符也不能自动认作编码流 BOM。原始 Unicode 仍保留在 OFD/SVG；不把删除后的像素一致当作语义保真。字符用途依据 [Unicode 17 第23章](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-23/) 与 [BOM FAQ](https://www.unicode.org/faq/utf_bom.html)。

当前唯一 default-ignorable 绘制例外是四个 Hangul filler（115F/1160/3164/FFA0）：已映射时保留字形/真实 advance；字体无字形时按 Unicode default-ignorable 缺字规则省略。该有界例外只验证现有文字/游程/Delta 样例，不能推广为完整 Hangul shaping 或其它 default-ignorable 可删除。原 OFD 文本与定位槽位不重排。实际 Preview 是独立验收门。

PDF 导入的透明 CID/私用区语义文本若没有可验证的原字体，不能保证语义再导出；缺字会明确失败。仅验证栅格视觉往返的 caller 可显式选择 PdfTextLayerMode.None，SDK 默认 Invisible 不变；这不代表无损字体恢复或 ActualText 功能已实现。

字体用字预遍历同时包含正文、所有模板元素和注释外观；每个元素先检查取消，按所绑定 face 内容身份汇总。模板/注释的全量保留原因不绕过其中已建模文字的 Unicode coverage，缺字必须在 ZIP 前报错；未建模 Raw/CGTransform 继续保守保留并诊断。

取消检查贯穿保留策略的每页/元素、coverage 与用字标量遍历。规范化按原文→NFC→NFD 延迟执行并在 BCL Normalize 前后检查；单次 Normalize 内部不可中断，不保证硬实时取消。

Source: https://whynpc9.github.io/ofdrw.net/reference/font-subset-and-reuse/index.mdx
