字体选择

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 重量 拉伸 Style
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 可以映射到“ital”和“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 作为两个具有以下轴值的可变字体实现。 请注意, opszwght 轴是可变的,而其他轴是非变量的。

文件名 “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。 对于每个可变字体,我们将创建一个对变量字体 实例 的引用,其中每个变量轴都分配有一个特定值。 也就是说, opszwght 轴分别设置为 12475。 这会生成以下匹配字体,其中非斜体字体排在第一位,因为它与 和 slnt 轴更匹配ital

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 的命名实例。 相比之下,权重拉伸样式匹配算法仅返回命名实例。

字体匹配顺序

对于 idWriteFontFamily::GetMatchingFonts () 和 IDWriteFontCollection2::GetMatchingFonts) 的粗细拉伸字体系列 (模型,有不同的重载 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) 纠缠斜线和斜度,在版式模型中,斜线和斜度被分解为“ital”轴 (0..1) 和“slnt”轴 (-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 系列名称。 例如,文档可能会 (WWS 姓氏) 指定“Sitka Subheading”,而不是 (版式姓氏) 指定“Sitka”。

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

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

全名 WWS 系列 重量 拉伸 Style 拼写错误系列 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 Condensed) 。 返回与全名匹配的字体。 忽略请求的轴值。 允许按完整字体名称进行匹配,因为 GDI 支持它。

上一部分描述了一个名为“Legacy”的不明确版式系列。 混合算法通过将“Legacy”或“Legacy Soft”指定为系列名称来避免歧义。 如果指定了“旧式软”,则不存在歧义,因为匹配仅在 WWS 系列中进行。 如果指定了“旧版”,则版式系列中的所有字体都被视为匹配候选项,但通过使用“旧版”WWS 系列中的成员身份作为平局来避免歧义。

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

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

混合字体选择 API

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

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

  • IDWriteFontSet4::GetMatchingFonts 方法返回给定系列名称和轴值数组的匹配字体的优先列表。 如上所述,family name 参数可以是版式姓氏、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::GetMatchingFonts 之前调用 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;
}

若要对示例进行舍入,以下是命令行分析函数和main函数:

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());
    }
}