Windows Terminal 字体特性与变体轴:用 font.features 与 font.axes 精确控制 ligatures、字重与字形风格
本篇基于 Windows Terminal 的设计规格《Font features and axes of variation》,讲解终端如何把用户配置的字体特性(font features)与变体轴(axes of variation)一路传递到 DirectWrite 渲染层:读完你将掌握 font 对象中 features 与 axes 两个字典型设置的写法、4 字符特性标签的语义、默认特性列表的合并规则,以及从 设置模型 到 Atlas 渲染引擎 的完整实现链路。
背景:问题与动机
在早期版本中,Windows Terminal 只能配置字体族、字号与字重。当字体带有连字(ligatures)时,用户没有任何途径将其关闭;同时 Cascadia Code 提供了多个 stylistic set(风格集,ss01–ss04),但设置模型没有暴露选择它们的入口。规格文档指出这两类需求本质上是同一问题:用户希望能对字体施加 OpenType 层面的精细控制。
规格将能力分为两块(见 [规格原文](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1790 - Font features and axes-spec.md)):
- 字体特性(Font features):二值开关为主,例如是否启用连字、使用哪组 stylistic set;
- 变体轴(Axes of variation):带数值的轴,常见的如
wght(字重)、slnt(倾斜),某些字体还包含更特殊的轴。
设置写法:font 对象下的 features 与 axes 两个字典
规格定义的最终 UI/UX 方案是:在配置文件(settings.json)的 font 对象(该对象结构由字体配置统一化改造引入)下新增两个字典。键是 4 字符的特性/轴标签,值是特性参数或轴数值:
"font": {
"face": "Cascadia Code",
"size": 12,
"features": {
"ss03": 1,
"liga": 0
},
"axes": {
"slnt": 20.5
}
}
语义上:{"ss03": 1} 启用第 3 组 stylistic set,{"liga": 0} 关闭连字;{"slnt": 20.5} 表示将倾斜轴设为 20.5。多数特性的参数值按二值解释——0 表示不应用该特性,非 0 表示应用。
仓库当前的设置模型完整实现了该方案:
- MTSMSettings.h 中的
MTSM_FONT_SETTINGS宏列出了font对象的全部设置项,其中FontAxes(JSON 键axes)与FontFeatures(JSON 键features)均为IFontAxesMap/IFontFeatureMap类型,即IMap<hstring, float>——标签到浮点值的映射,与规格完全一致; - FontConfig.cpp 负责解析与序列化。注意其中
CopyFontInfo的注释:两个字典不能直接赋值(那只是引用),必须逐项克隆为新的single_threaded_map,这保证了配置文件继承(profile inheritance)时各 profile 拥有独立的特性副本; - profiles.schema.json 中对该字段的官方描述是:"Sets the DWrite font features for the given font. For example, { "ss01": 1, "liga":0 } will enable ss01 and disable ligatures.",可作为字段合法性的权威参考。
字体特性的底层实现:DWRITE_FONT_FEATURE 与默认列表合并
规格的核心设计约束来自 DirectWrite 的 API 行为:向 DWrite 传入一个字体特性列表后,DWrite 只会应用你提供的列表,而不再应用它内置的标准特性集合(规格引用了该标准列表包含 11 项特性)。因此若用户只想新增或删除其中一项,就必须由应用侧维护一个"内部默认字典",再把用户的字典合并上去:新键值对追加、已有键值对更新,最终用合并后的字典构造 DWRITE_FONT_FEATURE 数组交给 DWrite。
仓库中这一合并逻辑落在渲染层。AtlasEngine.api.cpp 的 AtlasEngine::_updateFont 展示了当前实现:
- 当用户提供了非空的 features 字典时,先预置 3 个默认启用的特性:
liga(标准连字)、clig(上下文连字)、calt(上下文替换),参数均为 1。源码注释说明:这 3 项是 DirectWrite 默认启用的子集,完整罗列所有默认项是冗余的(对应上游 issue GH#10774 的结论); - 遍历用户字典,仅接受长度恰为 4 的键,用
DWRITE_MAKE_FONT_FEATURE_TAG(s[0], s[1], s[2], s[3])把 4 个字符构造为特性标签; - 若用户覆盖的是上述 3 个默认标签之一(例如
"liga": 0),直接改写预置项的parameter;否则作为新项emplace_back追加; - 合并结果存入
font->fontFeatures,在 AtlasEngine.cpp 中经DWriteFont::CreateFontFeature转成 COM 对象后随文本格式一并交给 DWrite 排版。
这里与规格原文有一处演进值得注意:规格写作时期(2021 年)描述的是"内置 11 项标准特性"的合并模型,而当前实现把默认项收敛为 liga/clig/calt 三项,其余默认行为交给 DWrite 自身处理——合并策略(用户字典覆盖默认项、追加新项)则保持不变。
变体轴的实现:wght、ital、slnt 的优先级
变体轴的表达方式与特性几乎相同:4 字符标签指定轴、数值指定轴值,例如 {'slnt', 20}。AtlasEngine.api.cpp 中,当用户提供了非空的 axes 字典时,引擎先预置 wght(字重)、ital(斜体)、slnt(倾斜)三条轴,数值为 -1.0f(表示沿用字体默认值),然后以同样的方式处理用户条目:命中预置轴则覆盖数值,否则追加。最终构造出 DWRITE_FONT_AXIS_VALUE 数组传给 DWrite。
规格中专门讨论了一个冲突点:如果用户同时定义了旧的 weight 设置和 wght 轴,应以 wght 轴为准。规格给出的理由有二:
wght是设置模型中更晚加入的字段,同时定义两者多半是用户忘了清理旧值;wght轴是精确的浮点值,而weight是枚举(最终才被映射为浮点值),表达更精确。
从源码结构看,这一优先级是成立的:ControlCore.cpp 的 _updateFont 一方面按 weight 设置构造期望字体(FontInfoDesired),另一方面把 FontAxes 映射原样传入渲染引擎的 UpdateFont;wght 轴值会以 DWRITE_FONT_AXIS_VALUE 的形式直接作用于 DirectWrite 的类型面创建过程,因此可以推断轴值在字体实例化阶段压过了枚举字重。
可靠性:非法标签的静默丢弃
规格在"Reliability"一节要求:用户字典里可能写出格式错误或不存在于该字体的标签,实现必须保证这类声明被忽略(最好伴随警告),只应用格式正确且确实存在的项。
当前实现采用最保守的白名单式校验:在 AtlasEngine.api.cpp 与 #L585 两处,只有键长度等于 4 的条目才会进入标签构造流程,其余条目被静默跳过(源码中未见到额外的警告输出);而"标签是否真实存在于当前字体"则交由 DirectWrite 判定,DWrite 对不认识的标签不会报错。参数值也被规范化:特性参数经 static_cast<UINT32>(std::max(0l, lrintf(p.second))) 取整并钳制为非负,符合"特性参数是 UINT32"的 API 约束。
性能代价:放弃"简单文本"快速路径
规格"Performance"一节指出了这项能力最主要的成本:Atlas 渲染引擎对"简单"文本 run 有一条快速取字形(shortcut)路径,可以跳过昂贵的 GetGlyphs 调用;但只要默认特性列表被改动(新增或修改任一特性),就无法预判字形会发生什么变化,于是每一段文本 run 都必须走完整的 GetGlyphs 流程。
规格同时给出了一条缓解视角:实际触发快速路径失败的字符并不少。以 Cascadia Code 为例,含 i、j、l、n、w、x 的文本 run 因这些字形存在本地化变体,本来就会被判定为"非简单",从而照常调用 GetGlyphs——也就是说,修改特性列表带来的额外开销在部分场景下是增量式的,而非全量切换。从源码结构看,BackendD3D.cpp 中存在针对 DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES 标签的检查,可以推断"是否仍处于默认连字状态"正是该快速路径判定的一部分。
兼容性与遗留问题
规格在"Compatibility"一节要求:旧版 Windows 可能缺少支持特性/轴设置的 DirectWrite 更新,必须在此类系统上回退到既有实现。规格未展开回退的具体机制,阅读当前代码时需注意这一前提:特性与轴的完整效果依赖于较新的 DirectWrite 版本,低版本系统上该两组设置可能不产生效果。
规格还预留了两个后续方向,均可对照现状理解:
- 按文本 run 粒度变化特性:DWrite 本身支持在不同 run 上应用不同特性;首个版本仅将特性应用于整个缓冲区。若未来放开按 run 配置,可复用终端既有的文本 run 拆分机制;
- 设置 UI 的呈现:由于用户需要手工输入任意 4 字符标签,这类设置比常规下拉/滑杆设置更复杂,UI 表示方式需要单独设计。
关键源码索引
| 环节 | 文件 | 说明 |
|---|---|---|
| 设置项定义 | MTSMSettings.h | axes / features 两个 IMap<hstring,float> 设置项 |
| 解析/序列化 | FontConfig.cpp | 字典的逐项克隆、变更记录 |
| 控制层下发 | ControlCore.cpp | _updateFont 将特征/轴映射交给渲染引擎 |
| 设置接口 | IControlSettings.idl | FontFeatures / FontAxes 属性声明 |
| 合并与校验 | AtlasEngine.api.cpp | 默认项预置、4 字符校验、用户项覆盖与追加 |
| DWrite 调用 | AtlasEngine.cpp | CreateFontFeature 构造 COM 对象 |
| 官方 Schema | profiles.schema.json | features 字段的字段级说明 |
综合来看,features 与 axes 把 DirectWrite 的 OpenType 控制能力以"字典覆盖默认值"的方式暴露给了终端用户:写法上是两个简单的字符串-数值映射,实现上则由设置模型逐层透传、渲染引擎完成与默认特性列表的合并、非法标签的过滤,并付出放弃部分文本快速路径的性能代价。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00