字体选择

IDWriteFontSet4 接口公开了从字体集中选择字体的方法。 这些方法使可以转换为 版式字体系列模型 ,同时保持与现有应用程序、文档和字体的兼容性。

字体选择 (有时称为字体匹配或字体映射) 是选择最适合应用程序传入的输入参数的可用字体的过程。 输入参数有时称为 逻辑字体。 逻辑字体包括字体系列名称以及其他属性,指示家庭中的特定字体。 字体选择算法与逻辑字体 (“所需字体”) 可用 物理字体 (“你拥有的字体”) 匹配。

字体系列是共享常见设计的命名字体组,但在权重等属性中可能有所不同。 字体系列模型定义哪些属性可用于区分系列中的字体。 新版式字体系列模型在Windows上两种字体系列模型具有许多优势。 但是,更改字体系列模型会产生混淆和兼容性问题的机会。 IDWriteFontSet4 接口公开的方法实现了一种混合方法,该方法可提供版式字体系列模型的优势,同时缓解兼容性问题。

本主题将较旧的字体系列模型与版式字体系列模型进行比较:它解释了更改字体系列模型带来的兼容性挑战;最后,它说明如何使用 [IDWriteFontSet4] (/windows/win32/api/dwrite_3/nn-dwrite_3-idwritefontset4) 方法克服这些挑战。

RBIZ 字体系列模型

GDI 应用程序生态系统中使用的事实上的字体系列模型有时称为“四字体模型”或“RBIZ”模型。 此模型中的每个字体系列通常最多有四种字体。 “RBIZ”标签来自用于某些字体文件的命名约定,例如:

文件名 字形
verdana.ttf 常规
verdanab.ttf 加粗
verdanai.ttf 斜体
verdanaz.ttf 粗体斜体

使用 GDI 时,用于选择字体的输入参数由 LOGFONT 结构定义,其中包括系列名称 () lfFaceName 、权重 lfWeight () 和斜体 (lfItalic) 字段。 该 lfItalic 字段为 TRUE 或 FALSE。 GDI 允许 lfWeight 字段成为范围 FW_THIN (100) 的任何值 ,FW_BLACK ( 900) ,但由于历史原因,字体设计长期以来一直使同一 GDI 字体系列中不超过两个权重。

早期流行的应用程序用户界面包括斜体按钮 (以打开和关闭斜体) 和粗体按钮 (切换正常和粗体权重) 。 使用这两个按钮在系列中选择字体假定为 RBIZ 模型。 因此,即使 GDI 本身支持两个以上的权重,应用程序兼容性也导致字体开发人员以与 RBIZ 模型一致的方式设置 GDI 系列名称 (OpenType 名称 ID 1) 。

例如,假设你想要向 Arial 字体系列添加更重的“黑色”粗细。 从逻辑上讲,此字体是 Arial 系列的一部分,因此你可能希望通过设置为lfFaceName“Arial”和lfWeightFW_BLACK来选择它。 但是,应用程序用户无法使用双状态粗体按钮在三个权重之间进行选择。 解决方法是为新字体提供不同的系列名称,因此用户可以通过从字体系列列表中选择“Arial Black”来选择它。 同样,不能仅使用粗体和斜体按钮从同一字体系列的不同宽度中进行选择,因此 Arial 的窄版本在 RBIZ 模型中具有不同的系列名称。 因此,我们在 RBIZ 模型中有“Arial”、“Arial Black”和“Arial Narrow”字体,尽管这些字体都属于一个家庭。

从这些示例中,可以看到字体系列模型的限制如何影响字体分组到家庭的方式。 由于字体系列按名称标识,这意味着同一字体可以具有不同的系列名称,具体取决于所使用的字体系列模型。

DirectWrite不直接支持 RBIZ 字体系列模型,但它提供从 RBIZ 模型转换的方法,例如 IDWriteGdiInterop::CreateFontFromLOGFONTIDWriteGdiInterop::ConvertFontToLOGFONT。 还可以通过调用字体 IDWriteFont::GetInformationalStrings 方法并指定 DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES来获取字体的 RBIZ 系列名称。

粗细拉伸式字体系列模型

粗细拉伸式字体系列模型是引入版式字体系列模型之前DirectWrite使用的原始字体系列模型。 它也称为重量宽度斜率 (WWS) 。 在 WWS 模型中,同一系列中的字体可以有三个属性的不同:权重 (DWRITE_FONT_WEIGHT) 、拉伸 (DWRITE_FONT_STRETCH) ,以及样式 (DWRITE_FONT_STYLE) 。

WWS 模型比 RBIZ 模型灵活两种方式。 首先,同一系列中的字体可以通过拉伸 (或宽度) 以及粗细和样式 (常规、斜体或斜体) 来区分。 其次,同一个家庭中可以有两个以上的重量。 这种灵活性足以让 Arial 的所有变体都包含在同一 WWS 系列中。 下表比较了所选 Arial 字体的 RBIZ 和 WWS 字体属性:

全名 RBIZ 系列名称 lfWeight lfItalic WWS FamilyName 重量 拉伸 样式
Arial Arial 400 0 Arial 400 5 0
Arial Bold Arial 700 0 Arial 700 5 0
Arial Black Arial Black 900 0 Arial 900 5 0
Arial Narrow Arial Narrow 400 0 Arial 400 3 0
Arial Narrow Bold Arial Narrow 700 0 Arial 700 3 0

正如你所看到的,“Arial Narrow”与“Arial”具有相同 lfWeight 的值 lfItalic ,因此它具有不同的 RBIZ 系列名称以避免歧义。 “Arial Black”有不同的RBIZ家族名称,以避免在“Arial”家族中拥有两个以上的权重。 相比之下,所有这些字体都位于相同的粗细拉伸式系列中。

然而,重量拉伸式模型并不开放。 如果两个字体具有相同的粗细、拉伸和样式,但在某些其他方面有所不同 (,例如光学大小) ,则它们不能包含在同一 WWS 字体系列中。 这给我们带来了版式字体系列模型。

版式字体系列模型

与其前身不同,版式字体系列模型 开放式的。 它支持字体系列中任意数量的变体轴。

如果将字体选择参数视为设计空间中的坐标,则粗细拉伸样式模型将三维坐标系统定义为坐标轴、拉伸和样式。 WWS 系列中的每个字体都必须有一个唯一的位置,其坐标沿这三个轴定义。 若要选择字体,请指定 WWS 系列名称和粗细、拉伸和样式参数。

相比之下,版式字体系列模型具有 N 维设计空间。 字体设计器可以定义任意数量的设计轴,每个轴都由四个字符 的轴标记标识。 给定字体在 N 维设计空间中的位置由轴值的数组定义,其中每个 轴值包括轴标记和浮点值。 若要选择字体,请指定版式系列名称和轴值数组, (DWRITE_FONT_AXIS_VALUE结构)

虽然字体轴的数量是开放式的,但有一些具有标准含义的已注册轴,粗细、拉伸和样式值可以映射到已注册的轴值。 DWRITE_FONT_WEIGHT 可以映射到“wght” (DWRITE_FONT_AXIS_TAG_WEIGHT) 轴值。 DWRITE_FONT_STRETCH 可以映射到“wdth” (DWRITE_FONT_AXIS_TAG_WIDTH) 轴值。 DWRITE_FONT_STYLE 可以映射到“斜体”和“slnt” (DWRITE_FONT_AXIS_TAG_ITALICDWRITE_FONT_AXIS_TAG_SLANT) 轴值的组合。

另一个已注册的轴是“opsz” (DWRITE_FONT_AXIS_TAG_OPTICAL_SIZE) 。 像 Sitka 这样的光学字体系列包括沿“opsz”轴差异的字体,这意味着它们设计用于不同的点大小。 WWS 字体系列模型没有光学尺寸轴,因此 Sitka 字体系列必须拆分为多个 WWS 字体系列:“Sitka Small”、“Sitka Text”、“Sitka Subheading”等。 每个 WWS 字体系列对应于不同的光学大小,并且用户需为给定字号指定正确的 WWS 系列名称。 使用版式字体系列模型,用户只需选择“Sitka”,应用程序可以根据字号自动设置“opsz”轴值。

版式字体选择和可变字体

变体轴的概念通常与可变字体相关联,但它也适用于静态字体。 OpenType STAT (样式属性) 表声明字体具有哪些设计轴以及这些轴的值。 此表对于可变字体是必需的,但也与静态字体相关。

DirectWrite API 公开每个字体的“wght”、“wdth”、“ital”和“slnt”轴值,即使它们不存在于 STAT 表中,或者没有 STAT 表。 如果可能,这些值派生自 STAT 表。 否则,它们派生自字体粗细、字体拉伸和字体样式。

字体轴可以是变量或非变量。 静态字体仅具有非可变轴,而变量字体可能同时具有这两个轴。 若要使用变量字体,必须创建变量字体 实例 ,其中所有变量轴都绑定到特定值。 IDWriteFontFace 接口表示静态字体或变量字体的特定实例。 可以创建具有指定轴值的变量字体的 任意实例 。 此外,变量字体可以在 STAT 表中声明具有轴值的预定义组合的命名实例。 命名实例使变量字体的行为与静态字体的集合类似。 枚举 IDWriteFontFamilyIDWriteFontSet 的元素时,每个静态字体和每个命名变量字体实例都有一个元素。

版式字体匹配算法首先根据姓氏选择潜在的匹配候选项。 如果匹配候选项包括变量字体,则同一变量字体的所有匹配候选项都折叠为一个匹配候选项,其中每个变量轴都分配了一个特定值,尽可能接近该轴的请求值。 如果变量轴没有请求的值,则会为其分配该轴的默认值。 然后,匹配候选项的顺序通过将其轴值与请求的轴值进行比较来确定。

例如,请考虑Windows中的 Sitka 版式系列。 Sitka 是一个光学字体系列,这意味着它有一个“opsz”轴。 在Windows 11中,Sitka 以以下轴值作为两种变量字体实现。 请注意, opsz 轴是 wght 变量,而其他轴是非变量。

文件名 “opsz” “wght” “wdth” “ital” “slnt”
SitkaVF.ttf 6-27.5 400-700 100 0 0
SitkaVF-Italic.ttf 6-27.5 400-700 100 1 -12

假设请求的轴值为 opsz:12 wght:475 wdth:100 ital:0 slnt:0。 对于每个变量字体,我们将创建对变量字体实例的引用,在该 实例 中为每个变量轴分配一个特定值。 即,opsz轴分别设置为12和轴475wght。 这会产生以下匹配字体,非斜体字体排在第一位,因为它是更好的 ital 匹配和 slnt 轴:

SitkaVF.ttf opsz:12 wght:475 wdth:100 ital:0 slnt0
SitkaVF-Italic.ttf opsz:12 wght:475 wdth:100 ital:1 slnt:-12

在上面的示例中,匹配的字体是任意变量字体实例。 没有重量为 475 的 Sitka 命名实例。 相比之下,权重拉伸样式匹配算法仅返回命名实例。

字体匹配顺序

重载 的 GetMatchingFonts 方法 (IDWriteFontFamily::GetMatchingFonts) 和版式字体系列模型 (IDWriteFontCollection2::GetMatchingFonts) 。 在这两种情况下,输出都是根据每个候选字体与输入属性匹配程度降序排列的匹配字体的列表。 本部分介绍如何确定优先级。

在粗细拉伸样式模型中,输入参数是字体粗细 (DWRITE_FONT_WEIGHT) 、字体拉伸 (DWRITE_FONT_STRETCH ) ,以及字体样式 (DWRITE_FONT_STYLE) 。 2006年,米哈伊尔·莱昂诺夫和大卫·布朗在名为“WPF 字体选择模型”的白皮书中记录了查找最佳匹配的算法。 请参阅“匹配候选人脸列表中的人脸”部分。本文Windows Presentation Foundation (WPF) ,但后来DirectWrite使用相同的方法。

该算法使用 字体属性向量的概念,该概念针对给定的粗细、拉伸和样式组合的计算方式如下:

FontAttributeVector.X = (stretch - 5) * 1100;
FontAttributeVector.Y = style * 700;
FontAttributeVector.Z = (weight - 400) * 5;

请注意,每个向量坐标通过减去相应属性的“normal”值并乘以常量来规范化。 乘数补偿权重、拉伸和样式的输入值范围非常不同。 否则,体重 (100..999) 将主导风格 (0..2) 。

对于每个匹配候选项,矢量距离和点积在匹配候选项的字体属性向量和输入字体属性向量之间计算。 比较两个匹配候选项时,具有较小向量距离的候选项是更好的匹配项。 如果距离相同,则具有较小点积的候选项是更好的匹配项。 如果点积也相同,则沿 X、Y 和 Z 轴的距离按该顺序进行比较。

比较距离足够直观,但将点积用作辅助度量值可能需要一些解释。 假设输入权重是半曲 (600) ,两个候选权重是黑色 (900) ,半光 (300) 。 每个候选权重与输入权重的距离相同,但黑色权重从原点 (方向相同,即 400 或正常) ,因此它将具有较小的点积。

版式匹配算法是权重拉伸样式的通用化算法。 每个轴值都被视为 N 维字体属性矢量中的坐标。 对于每个匹配候选项,矢量距离和点积在匹配候选项的字体属性向量和输入字体属性向量之间计算。 具有较小矢量距离的候选项是更好的匹配项。 如果距离相同,则具有较小点积的候选项是更好的匹配项。 如果点积也相同,则可以将指定权重拉伸式系列中的存在用作断线器。

若要计算矢量距离和点积,匹配候选项的字体属性矢量和输入字体属性向量必须具有相同的轴。 因此,任一向量中的任何缺失轴值都通过替换该轴的标准值来填充。 矢量坐标通过减去相应轴的标准 (或“normal”) 值,并将结果乘以特定于轴的乘数来规范化。 下面是每个轴的乘数和标准值:

“乘数” 标准值
“wght” 5 400
“wdth” 55 100
“ital” 1400 0
“slnt” 35 0
“opsz” 1 12
其他 1 0

乘数与权重拉伸样式算法使用的乘数一致,但根据需要进行缩放。 例如,普通宽度为 100,相当于拉伸 5。 这会产生 55 与 1100 的乘数。 旧样式属性 (0..2) 纠缠斜体和斜体,在版式模型中,该属性分解为 (0..1) 的“斜体”轴和“斜体”轴 (-90。90) 。 如果假定倾斜字体的默认 20 度倾斜,则这两个轴选择的乘数为旧算法的等效结果。

版式字体选择和光学大小

使用版式字体系列模型的应用程序可以通过将轴值指定 opsz 为字体选择参数来实现光学大小调整。 例如,字处理应用程序可以指定等于 opsz 磅字号的轴值。 在这种情况下,用户可以选择“Sitka”作为字体系列,应用程序会自动选择具有正确 opsz 轴值的 Sitka 实例。 在 WWS 模型中,每个光学大小都公开为不同的系列名称,由用户选择正确的系列名称。

从理论上讲,可以通过在字体选择将轴值替代opsz为单独的步骤,在权重拉伸样式模型下实现自动光学大小调整。 但是,仅当第一个匹配字体是具有变量轴的变量 opsz 字体时,这才有效。 指定 opsz 为字体选择参数同样适用于静态字体。 例如,Sitka 字体系列在Windows 11中实现为可变字体,但作为Windows 10中的静态字体集合。 静态字体具有不同的非重叠 opsz 轴范围, (这些字体被声明为字体选择范围,但它们不是变量轴) 。 指定 opsz 为字体选择参数可使选择光学大小的正确静态字体。

版式字体选择优势和兼容性问题

版式字体选择模型在早期模型中具有多种优势,但其纯形式存在一些潜在的兼容性问题。 本部分介绍优点和兼容性问题。 下一部分介绍一个混合字体选择模型,该模型在缓解兼容性问题的同时保留了优势。

版式字体系列模型的优点是:

  • 字体可以按设计器预期分组为系列,而不是由于字体系列模型的限制而拆分为子家庭。

  • 应用程序可以根据字号自动选择正确的 opsz 轴值,而不是向用户公开不同的光学大小作为不同的字体系列。

  • 可以选择任意变量字体实例。 例如,如果可变字体支持连续范围 100-900 中的权重,则版式模型可以选择此范围 中的任何 粗细。 较旧的字体系列模型只能从字体定义的命名实例中选择最接近的粗细。

版式字体选择模型的兼容性问题包括:

  • 某些较旧的字体不能明确选择,只使用版式系列名称和轴值。

  • 现有文档可能按 WWS 系列名称或 RBIZ 系列名称引用字体。 用户可能还希望使用 WWS 和 RBIZ 系列名称。 例如,文档可以指定“Sitka Subheading” (WWS 系列名称) 而不是“Sitka” (版式系列名称) 。

  • 库或框架可能采用版式字体系列模型来利用自动光学大小调整,但不提供用于指定任意轴值的 API。 即使提供了新的 API,框架也需要使用仅指定权重、拉伸和样式参数的现有应用程序。

与较旧字体的兼容性问题之所以出现,是因为版式系列名称的概念占了字体轴值的概念,该概念与 OpenType 1.8 中的变量字体一起引入。 在 OpenType 1.8 之前,版式系列名称只是表示设计器的意图,即一组字体是相关的,但不能保证这些字体可以根据其属性以编程方式区分。 作为假设示例,假设以下所有字体都具有版式系列名称“Legacy”:

全名 WWS 系列 重量 拉伸 样式 拼写错误系列 wght wdth ital slnt
旧的 旧的 400 5 0 旧的 400 100 0 0
旧版粗体 旧的 700 5 0 旧的 700 100 0 0
旧版黑色 旧的 900 5 0 旧的 900 100 0 0
旧版软 旧版软 400 5 0 旧的 400 100 0 0
旧版软粗体 旧版软 700 5 0 旧的 700 100 0 0
旧版软黑色 旧版软 900 5 0 旧的 900 100 0 0

“传统”版式系列有三个权重,每个权重具有常规和“软”变体。 如果这些字体是新字体,则可以将其实现为声明 SOFT 设计轴。 但是,这些字体预先表示 OpenType 1.8,因此其唯一的设计轴是从粗细、拉伸和样式派生的。 对于每个粗细,此字体系列有两个具有相同轴值的字体,因此不能单独使用轴值明确选择此系列中的字体。

混合字体选择算法

下一部分中介绍的字体选择 API 使用混合字体选择算法,该算法可保留版式字体选择的优点,同时缓解其兼容性问题。

混合字体选择通过启用字体粗细、字体拉伸和字体样式值映射到相应的字体轴值,为较旧的字体系列模型提供了桥梁。 这有助于解决文档和应用程序兼容性问题。

此外,混合字体选择算法允许指定的姓氏是版式系列名称、粗细拉伸式系列名称、GDI/RBIZ 系列名称或全字体名称。 按以下方式之一进行匹配,按优先级降序排列:

  1. 该名称与版式系列 ((例如 Sitka) )匹配。 匹配发生在版式系列中,并且使用所有请求的轴值。 如果名称与 WWS 子家庭 (匹配,则一个小于版式系列) ,则 WWS 子家庭的成员身份用作断线器。

  2. 名称与 WWS 系列 ((例如 Sitka Text) )匹配。 匹配发生在 WWS 系列中,并且将忽略除“wght”、“wdth”、“ital”和“slnt”以外的轴值。

  3. 该名称与 GDI 系列 ((例如 Bahnschrift Condensed) )匹配。 匹配发生在 RBIZ 系列中,并且请求的轴值除“wght”、“ital”和“slnt”之外,将被忽略。

  4. 该名称与全名匹配 (,例如,Bahnschrift Bold 凝结) 。 返回与全名匹配的字体。 请求的轴值将被忽略。 允许按全字体名称进行匹配,因为 GDI 支持它。

上一部分描述了一个名为“Legacy”的模糊版式系列。 混合算法允许通过将“Legacy”或“Legacy Soft”指定为家族名称来避免歧义。 如果指定了“旧软”,则不存在歧义,因为匹配仅在 WWS 系列内发生。 如果指定了“旧版”,则版式系列中的所有字体都被视为匹配候选项,但使用“旧版”WWS 系列的成员身份作为断线符,避免了歧义。

假设文档指定系列名称和粗细、拉伸和样式参数,但没有轴值。 应用程序首先可以通过调用 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 将粗细、拉伸、样式和字号转换为轴值。 然后,应用程序可以将系列名称和轴值传递给 IDWriteFontSet4::GetMatchingFontsGetMatchingFonts 按优先级顺序返回匹配字体的列表,并且结果适合指定家族名称是版式系列名称、粗细延伸式系列名称、RBIZ 系列名称还是全名。 如果指定的系列具有“opsz”轴,则根据字号自动选择适当的光学大小。

假设文档指定粗细、拉伸和样式, 指定轴值。 在这种情况下,显式轴值也可以传递到 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues,该方法返回的派生轴值将仅包含未显式指定的字体轴。 因此,文档 (或应用程序) 显式指定的轴值优先于从粗细、拉伸、样式和字号派生的轴值。

混合字体选择 API

混合字体选择模型由以下 IDWriteFontSet4 方法实现:

  • IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 方法将字号、粗细、拉伸和样式参数转换为相应的轴值。 客户端传入的任何显式轴值均从派生轴值中排除。

  • IDWriteFontSet4::GetMatchingFonts 方法返回给定家族名称和轴值的数组的匹配字体的优先级列表。 如上所述,系列名称参数可以是版式系列名称、WWS 系列名称、RBIZ 系列名称或全名。 (全名标识特定字体样式,例如“Arial Bold Italic”。 GetMatchingFonts 支持通过全名进行匹配,以便使用 GDI 实现更大的自满性,这也允许它进行匹配。)

以下其他DirectWrite API 还使用混合字体选择算法:

正在使用的字体选择 API 的代码示例

本部分演示了一个完整的控制台应用程序,该应用程序演示 IDWriteFontSet4::GetMatchingFontsIDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 方法。 首先,让我们包括一些标头:

#include <dwrite_core.h>
#include <wil/com.h>
#include <iostream>
#include <string>
#include <vector>

IDWriteFontSet4::GetMatchingFonts 方法按优先级顺序返回与指定系列名称和轴值匹配的字体列表。 以下 MatchAxisValues 函数将参数输出到 IDWriteFontSet4::GetMatchingFonts 和返回的字体集中匹配字体的列表。

// Forward declarations of overloaded output operators used by MatchAxisValues.
std::wostream& operator<<(std::wostream& out, DWRITE_FONT_AXIS_VALUE const& axisValue);
std::wostream& operator<<(std::wostream& out, IDWriteFontFile* fileReference);
std::wostream& operator<<(std::wostream& out, IDWriteFontFaceReference1* faceReference);
//
// MatchAxisValues calls IDWriteFontSet4::GetMatchingFonts with the
// specified parameters and outputs the matching fonts.
//
void MatchAxisValues(
    IDWriteFontSet4* fontSet,
    _In_z_ WCHAR const* familyName,
    std::vector<DWRITE_FONT_AXIS_VALUE> const& axisValues,
    DWRITE_FONT_SIMULATIONS allowedSimulations
    )
{
    // Write the input parameters.
    std::wcout << L"GetMatchingFonts(\"" << familyName << L"\", {";
    for (DWRITE_FONT_AXIS_VALUE const& axisValue : axisValues)
    {
        std::wcout << L' ' << axisValue;
    }
    std::wcout << L" }, " << allowedSimulations << L"):\n";
    // Get the matching fonts for the specified family name and axis values.
    wil::com_ptr<IDWriteFontSet4> matchingFonts;
    THROW_IF_FAILED(fontSet->GetMatchingFonts(
        familyName,
        axisValues.data(),
        static_cast<UINT32>(axisValues.size()),
        allowedSimulations,
        &matchingFonts
    ));
    // Output the matching font face references.
    UINT32 const fontCount = matchingFonts->GetFontCount();
    for (UINT32 fontIndex = 0; fontIndex < fontCount; fontIndex++)
    {
        wil::com_ptr<IDWriteFontFaceReference1> faceReference;
        THROW_IF_FAILED(matchingFonts->GetFontFaceReference(fontIndex, &faceReference));
        std::wcout << L"    " << faceReference.get() << L'\n';
    }
    std::wcout << std::endl;
}

应用程序可能有粗细、拉伸和样式参数,而不是 (,或者除了) 轴值之外。 例如,应用程序可能需要处理使用 RBIZ 或粗细拉伸样式参数引用字体的文档。 即使应用程序增加了对指定任意轴值的支持,它也需要支持较旧的参数。 为此,应用程序可以在调用 IDWriteFontSet4::GetMatchingFontFonts 之前调用 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues

除了轴值外,以下 MatchFont 函数还采用粗细、拉伸、样式和字号参数。 它将这些参数转发到 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 方法,以计算派生轴值,这些值追加到输入轴值。 它将组合轴值传递给上述 MatchAxisValues 函数。

struct FontStyleParams
{
    FontStyleParams() {}
    FontStyleParams(DWRITE_FONT_WEIGHT fontWeight) : fontWeight{ fontWeight } {}
    FontStyleParams(float fontSize) : fontSize{ fontSize } {}
    DWRITE_FONT_WEIGHT fontWeight = DWRITE_FONT_WEIGHT_NORMAL;
    DWRITE_FONT_STRETCH fontStretch = DWRITE_FONT_STRETCH_NORMAL;
    DWRITE_FONT_STYLE fontStyle = DWRITE_FONT_STYLE_NORMAL;
    float fontSize = 0.0f;
};
//
// MatchFont calls IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues to convert
// the input parameters to axis values and then calls MatchAxisValues.
//
void MatchFont(
    IDWriteFactory7* factory,
    _In_z_ WCHAR const* familyName,
    FontStyleParams styleParams = {},
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues = {},
    DWRITE_FONT_SIMULATIONS allowedSimulations = DWRITE_FONT_SIMULATIONS_BOLD | DWRITE_FONT_SIMULATIONS_OBLIQUE
    )
{
    wil::com_ptr<IDWriteFontSet2> fontSet2;
    THROW_IF_FAILED(factory->GetSystemFontSet(/*includeDownloadableFonts*/ false, &fontSet2));
    wil::com_ptr<IDWriteFontSet4> fontSet;
    THROW_IF_FAILED(fontSet2->QueryInterface(&fontSet));
    // Ensure the total number of axis values can't overflow a UINT32.
    size_t const inputAxisCount = axisValues.size();
    if (inputAxisCount > UINT32_MAX - DWRITE_STANDARD_FONT_AXIS_COUNT)
    {
        THROW_HR(E_INVALIDARG);
    }
    // Reserve space at the end of axisValues vector for the derived axis values.
    // The maximum number of derived axis values is DWRITE_STANDARD_FONT_AXIS_COUNT.
    axisValues.resize(inputAxisCount + DWRITE_STANDARD_FONT_AXIS_COUNT);
    // Convert the weight, stretch, style, and font size to derived axis values.
    UINT32 derivedAxisCount = fontSet->ConvertWeightStretchStyleToFontAxisValues(
        /*inputAxisValues*/ axisValues.data(),
        static_cast<UINT32>(inputAxisCount),
        styleParams.fontWeight,
        styleParams.fontStretch,
        styleParams.fontStyle,
        styleParams.fontSize,
        /*out*/ axisValues.data() + inputAxisCount
        );
    // Resize the vector based on the actual number of derived axis values returned.
    axisValues.resize(inputAxisCount + derivedAxisCount);
    // Pass the combined axis values (explicit and derived) to MatchAxisValues.
    MatchAxisValues(fontSet.get(), familyName, axisValues, allowedSimulations);
}

以下函数演示了使用一些示例输入调用上述 MatchFont 函数的结果:

void TestFontSelection(IDWriteFactory7* factory)
{
    // Request "Cambria" with bold weight, with and without allowing simulations.
    //  - Matches a typographic/WWS family.
    //  - Result includes all fonts in the family ranked by priority.
    //  - Useful because Cambria Bold might have fewer glyphs than Cambria Regular.
    //  - Simulations may be applied if allowed.
    MatchFont(factory, L"Cambria", DWRITE_FONT_WEIGHT_BOLD);
    MatchFont(factory, L"Cambria", DWRITE_FONT_WEIGHT_BOLD, {}, DWRITE_FONT_SIMULATIONS_NONE);
    // Request "Cambria Bold".
    //  - Matches the full name of one static font.
    MatchFont(factory, L"Cambria Bold");
    // Request "Bahnschrift" with bold weight.
    //  - Matches a typographic/WWS family.
    //  - Returns an arbitrary instance of the variable font.
    MatchFont(factory, L"Bahnschrift", DWRITE_FONT_WEIGHT_BOLD);
    // Request "Segoe UI Variable" with two different font sizes.
    //  - Matches a typographic family name only (not a WWS family name).
    //  - Font size maps to "opsz" axis value (Note conversion from DIPs to points.)
    //  - Returns an arbitrary instance of the variable font.
    MatchFont(factory, L"Segoe UI Variable", 16.0f);
    MatchFont(factory, L"Segoe UI Variable", 32.0f);
    // Same as above with an explicit opsz axis value.
    // The explicit value is used instead of an "opsz" value derived from the font size.
    MatchFont(factory, L"Segoe UI Variable", 16.0f, { { DWRITE_FONT_AXIS_TAG_OPTICAL_SIZE, 32.0f } });
    // Request "Segoe UI Variable Display".
    //  - Matches a WWS family (NOT a typographic family).
    //  - The input "opsz" value is ignored; the optical size of the family is used.
    MatchFont(factory, L"Segoe UI Variable Display", 16.0f);
    // Request "Segoe UI Variable Display Bold".
    //  - Matches the full name of a variable font instance.
    //  - All input axes are ignored; the axis values of the matching font are used.
    MatchFont(factory, L"Segoe UI Variable Display Bold", 16.0f);
}

下面是上述 TestFontSelection 函数的输出:

GetMatchingFonts("Cambria", { wght:700 wdth:100 ital:0 slnt:0 }, 3):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
    cambria.ttc wght:400 wdth:100 ital:0 slnt:0 BOLDSIM
    cambriaz.ttf wght:700 wdth:100 ital:1 slnt:-12.4
    cambriai.ttf wght:400 wdth:100 ital:1 slnt:-12.4 BOLDSIM
GetMatchingFonts("Cambria", { wght:700 wdth:100 ital:0 slnt:0 }, 0):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
    cambriaz.ttf wght:700 wdth:100 ital:1 slnt:-12.4
    cambria.ttc wght:400 wdth:100 ital:0 slnt:0
    cambriai.ttf wght:400 wdth:100 ital:1 slnt:-12.4
GetMatchingFonts("Cambria Bold", { wght:400 wdth:100 ital:0 slnt:0 }, 3):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
GetMatchingFonts("Bahnschrift", { wght:700 wdth:100 ital:0 slnt:0 }, 3):
    bahnschrift.ttf wght:700 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:12 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { wght:400 wdth:100 ital:0 slnt:0 opsz:24 }, 3):
    SegUIVar.ttf opsz:24 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { opsz:32 wght:400 wdth:100 ital:0 slnt:0 }, 3):
    SegUIVar.ttf opsz:32 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable Display", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:36 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable Display Bold", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:36 wght:700 wdth:100 ital:0 slnt:0

下面是上面声明的重载运算符的实现。 MatchAxisValues 使用这些值来写入输入轴值和生成的字体人脸引用:

// Output a font axis value.
std::wostream& operator<<(std::wostream& out, DWRITE_FONT_AXIS_VALUE const& axisValue)
{
    UINT32 tagValue = axisValue.axisTag;
    WCHAR tagChars[5] =
    {
        static_cast<WCHAR>(tagValue & 0xFF),
        static_cast<WCHAR>((tagValue >> 8) & 0xFF),
        static_cast<WCHAR>((tagValue >> 16) & 0xFF),
        static_cast<WCHAR>((tagValue >> 24) & 0xFF),
        '\0'
    };
    return out << tagChars << L':' << axisValue.value;
}
// Output a file name given a font file reference.
std::wostream& operator<<(std::wostream& out, IDWriteFontFile* fileReference)
{
    wil::com_ptr<IDWriteFontFileLoader> loader;
    THROW_IF_FAILED(fileReference->GetLoader(&loader));
    wil::com_ptr<IDWriteLocalFontFileLoader> localLoader;
    if (SUCCEEDED(loader->QueryInterface(&localLoader)))
    {
        const void* fileKey;
        UINT32 fileKeySize;
        THROW_IF_FAILED(fileReference->GetReferenceKey(&fileKey, &fileKeySize));
        WCHAR filePath[MAX_PATH];
        THROW_IF_FAILED(localLoader->GetFilePathFromKey(fileKey, fileKeySize, filePath, MAX_PATH));
        WCHAR* fileName = wcsrchr(filePath, L'\\');
        fileName = (fileName != nullptr) ? fileName + 1 : filePath;
        out << fileName;
    }
    return out;
}
// Output a font face reference.
std::wostream& operator<<(std::wostream& out, IDWriteFontFaceReference1* faceReference)
{
    // Output the font file name.
    wil::com_ptr<IDWriteFontFile> fileReference;
    THROW_IF_FAILED(faceReference->GetFontFile(&fileReference));
    std::wcout << fileReference.get();
    // Output the face index if nonzero.
    UINT32 const faceIndex = faceReference->GetFontFaceIndex();
    if (faceIndex != 0)
    {
        out << L'#' << faceIndex;
    }
    // Output the axis values.
    UINT32 const axisCount = faceReference->GetFontAxisValueCount();
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues(axisCount);
    THROW_IF_FAILED(faceReference->GetFontAxisValues(axisValues.data(), axisCount));
    for (DWRITE_FONT_AXIS_VALUE const& axisValue : axisValues)
    {
        out << L' ' << axisValue;
    }
    // Output the simulations.
    DWRITE_FONT_SIMULATIONS simulations = faceReference->GetSimulations();
    if (simulations & DWRITE_FONT_SIMULATIONS_BOLD)
    {
        out << L" BOLDSIM";
    }
    if (simulations & DWRITE_FONT_SIMULATIONS_OBLIQUE)
    {
        out << L" OBLIQUESIM";
    }
    return out;
}

若要舍入示例,下面是命令行分析函数和 函数:

char const g_usage[] =
"ParseCmdLine <args>\n"
"\n"
" -test             Test sample font selection parameters.\n"
" <familyname>      Sets the font family name.\n"
" -size <value>     Sets the font size in DIPs.\n"
" -bold             Sets weight to bold (700).\n"
" -weight <value>   Sets a weight in the range 100-900.\n"
" -italic           Sets style to DWRITE_FONT_STYLE_ITALIC.\n"
" -oblique          Sets style to DWRITE_FONT_STYLE_OBLIQUE.\n"
" -stretch <value>  Sets a stretch in the range 1-9.\n"
" -<axis>:<value>   Sets an axis value (for example, -opsz:24).\n"
" -nosim            Disallow font simulations.\n"
"\n";
struct CmdArgs
{
    std::wstring familyName;
    FontStyleParams styleParams;
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues;
    DWRITE_FONT_SIMULATIONS allowedSimulations = DWRITE_FONT_SIMULATIONS_BOLD | DWRITE_FONT_SIMULATIONS_OBLIQUE;
    bool test = false;
};
template<typename T>
_Success_(return)
bool ParseEnum(_In_z_ WCHAR const* arg, long minValue, long maxValue, _Out_ T* result)
{
    WCHAR* endPtr;
    long value = wcstol(arg, &endPtr, 10);
    *result = static_cast<T>(value);
    return value >= minValue && value <= maxValue && *endPtr == L'\0';
}
_Success_(return)
bool ParseFloat(_In_z_ WCHAR const* arg, _Out_ float* value)
{
    WCHAR* endPtr;
    *value = wcstof(arg, &endPtr);
    return *arg != L'\0' && *endPtr == L'\0';
}
bool ParseCommandLine(int argc, WCHAR** argv, CmdArgs& cmd)
{
    for (int argIndex = 1; argIndex < argc; argIndex++)
    {
        WCHAR const* arg = argv[argIndex];
        if (*arg != L'-')
        {
            if (!cmd.familyName.empty())
                return false;
            cmd.familyName = argv[argIndex];
        }
        else if (!wcscmp(arg, L"-test"))
        {
            cmd.test = true;
        }
        else if (!wcscmp(arg, L"-size"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseFloat(argv[argIndex], &cmd.styleParams.fontSize))
                return false;
        }
        else if (!wcscmp(arg, L"-bold"))
        {
            cmd.styleParams.fontWeight = DWRITE_FONT_WEIGHT_BOLD;
        }
        else if (!wcscmp(arg, L"-weight"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseEnum(argv[argIndex], 100, 900, &cmd.styleParams.fontWeight))
                return false;
        }
        else if (!wcscmp(arg, L"-italic"))
        {
            cmd.styleParams.fontStyle = DWRITE_FONT_STYLE_ITALIC;
        }
        else if (!wcscmp(arg, L"-oblique"))
        {
            cmd.styleParams.fontStyle = DWRITE_FONT_STYLE_OBLIQUE;
        }
        else if (!wcscmp(arg, L"-stretch"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseEnum(argv[argIndex], 1, 9, &cmd.styleParams.fontStretch))
                return false;
        }
        else if (wcslen(arg) > 5 && arg[5] == L':')
        {
            // Example: -opsz:12
            DWRITE_FONT_AXIS_VALUE axisValue;
            axisValue.axisTag = DWRITE_MAKE_FONT_AXIS_TAG(arg[1], arg[2], arg[3], arg[4]);
            if (!ParseFloat(arg + 6, &axisValue.value))
                return false;
            cmd.axisValues.push_back(axisValue);
        }
        else if (!wcscmp(arg, L"-nosim"))
        {
            cmd.allowedSimulations = DWRITE_FONT_SIMULATIONS_NONE;
        }
        else
        {
            return false;
        }
    }
    return true;
}
int __cdecl wmain(int argc, WCHAR** argv)
{
    CmdArgs cmd;
    if (!ParseCommandLine(argc, argv, cmd))
    {
        std::cerr << "Invalid command. Type TestFontSelection with no arguments for usage.\n";
        return 1;
    }
    if (cmd.familyName.empty() && !cmd.test)
    {
        std::cout << g_usage;
        return 0;
    }
    wil::com_ptr<IDWriteFactory7> factory;
    THROW_IF_FAILED(DWriteCoreCreateFactory(
        DWRITE_FACTORY_TYPE_SHARED,
        __uuidof(IDWriteFactory7),
        (IUnknown**)&factory
    ));
    if (!cmd.familyName.empty())
    {
        MatchFont(
            factory.get(),
            cmd.familyName.c_str(),
            cmd.styleParams,
            std::move(cmd.axisValues),
            cmd.allowedSimulations
        );
    }
    if (cmd.test)
    {
        TestFontSelection(factory.get());
    }
}