gdi+无法直接从ttc中提取单个字体,需手动解析ttc头获取子字体偏移,再用addmemoryfont加载指定ttf子流;wpf可通过fontsource+uri索引(如file:///x.ttc#2)原生支持。

怎么用 Gdiplus 读取 TTC 文件里的单个字体
Windows GDI+ 原生不支持直接从 TTC 中提取指定字体实例,GdipCreateFontFromCollection 这类 API 根本不存在。你调用 PrivateFontCollection.AddFontFile 加载一个 .ttc 文件时,它会把整个集合里所有字体都注册进去——但你无法控制“只加载第 2 个字体”或“跳过 Bold 变体”。这是最常被误解的点:不是“能读但要写额外逻辑”,而是“根本没提供按索引/名称选取子字体的公开接口”。
实操建议:
- 如果只是想让 WPF 或 WinForms 控件能用 TTC 里的字体,直接调用
PrivateFontCollection.AddFontFile("xxx.ttc")就够了;系统内部会解析并注册全部子字体,之后你可用new Font("Arial Unicode MS", 12)这种名字引用(前提是名字匹配) - 若需确认 TTC 包含哪些字体名,必须自己解析 TTC 结构——靠
PrivateFontCollection是拿不到列表的 - 别在部署时假设用户已安装 TTC 中的字体;
AddFontFile的注册仅对当前进程有效,且不能跨 session 持久化
手动解析 TTC 文件头获取字体数量和偏移
TTC 文件本质是多个 TTF 的拼接,开头有固定结构:ttcf 签名、版本、字体数量,后跟一串偏移数组。你不需要完整实现 OpenType 解析,只需读前 12 字节就能拿到子字体个数和每个 TTF 的起始位置。
常见错误现象:用 FileStream 直接读 byte[4] 判断签名,却忽略字节序(TTC 头部用大端,而 x86 是小端);结果把 0x74746366 当成 "tfct" 判定失败。
实操建议:
- 用
BinaryReader配合BitConverter.IsLittleEndian ? BitConverter.GetBytes(x).Reverse().ToArray() : ...处理四字节整数,或直接用IPAddress.HostToNetworkOrder转换 - TTC 版本目前只有 0x00010000 和 0x00020000 两种,遇到其他值说明不是合法 TTC
- 偏移数组起始位置 = 12 + 4 * fontCount,每个偏移是 uint32,指向对应 TTF 的第一个字节(即从该位置开始可当独立 TTF 流处理)
用 Stream 拆出单个 TTF 子流再喂给 PrivateFontCollection
拿到某个子字体的偏移和下一个偏移(或文件末尾),就能构造一个只包含该 TTF 的 MemoryStream 或 SubArrayStream。这时候再调用 AddFontFile 就等价于加载单个 TTF——绕过了 TTC 的“全量注册”限制。
使用场景:你有一个 TTC 含 Light / Regular / Bold 三版,但 UI 只允许用户选 Regular;或者你想预检某字体是否支持中文(查 cmap 表),但不想把所有变体都注册进进程。
实操建议:
- 别用
File.OpenRead后反复Seek—— 在某些网络路径或只读介质上会失败;优先用File.ReadAllBytes加载全量再切片 -
PrivateFontCollection.AddFontFile接收的是路径,不是流;所以必须把子 TTF 写到临时文件,或改用AddMemoryFont(注意:后者要求传入完整 TTF 字节数组,且不能释放内存) - 调用
AddMemoryFont后,务必保留该字节数组引用,否则 GC 回收会导致后续Font创建失败,错误信息是"Object is not a valid Font"
WPF 中用 FontSource 加载 TTC 子字体更可控
WinForms 和 GDI+ 被动接受 TTC 全量注册,WPF 却提供了细粒度控制:FontSource 构造函数支持传入 Uri + int index,直接定位 TTC 内第 N 个字体。这是目前唯一无需手动解析、也无需临时文件的原生方案。
性能影响:WPF 内部仍会读取整个 TTC 文件,但只解析指定索引的字体表(如 name、cmap、glyf),比全量注册轻量得多;且不会污染进程级字体缓存。
实操建议:
- URI 格式必须为
pack://application:,,,/Fonts/#FontName或file:///C:/xxx.ttc#2(末尾#2表示第 3 个字体,索引从 0 开始) - 如果 TTC 中字体 name 表缺失或重复,
#0可能加载失败;此时 fallback 到手动解析 +AddMemoryFont - WPF 的
TextBlock.FontFamily支持这种 URI,但 WinForms 的Label.Font不支持——别混用上下文
真正麻烦的从来不是“怎么读 TTC”,而是“怎么确定你要的那个字体到底在第几个位置、它的 name 字符串是否带空格或版本号、以及下游控件是否认这个 name”。这些细节不打日志根本看不出问题。










