Skip to content

字体子集与复用(05 / issue #3)

静态 TrueType 子集、字体内容身份复用、缺字检查、嵌入许可与全量保留策略。

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

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 预布局适配器是另一条显式新入口,不改变此子集路径。

范围 行为
静态 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 四出口能力表;它们不启用 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 的证据分列在验收目录。

补充平面(非 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 规范。

Windows symbol-only cmap 的一般字符语义仍未建模。OFD 写包/读取编辑保全量并标识 coverage 未验证。仅当选中文字全是具有直接、唯一且有效 GID 的 BMP 私用区码点,且字体满足静态单色 TrueType 嵌入约束时,R08 才按直接 PUA profile为 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章 与 BOM FAQ。

当前唯一 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 内部不可中断,不保证硬实时取消。

Type to search…

↑↓ navigate↵ selectEsc close