字体选择
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 name ID 1)。
例如,假设你想要向 Arial 字体系列添加更重的“Black”粗细。 从逻辑上讲,此字体是 Arial 系列的一部分,因此你可能希望通过将 lfFaceName
设置为“Arial”并选择它,并将 lfWeight
设置为 FW_BLACK。 但是,应用程序用户无法使用双状态加粗按钮在三个权重之间进行选择。 解决方法是为新字体提供不同的系列名称,因此用户可以通过从字体系列列表中选择“Arial Black”来选择它。 同样,无法仅使用粗体和斜体按钮从同一字体系列的不同宽度中进行选择,因此 Arial 的窄版本在 RBIZ 模型中具有不同的系列名称。 因此,我们在 RBIZ 模型中拥有“Arial”、“Arial Black”和“Arial Narrow”字体,尽管这些字体都属于一个家庭。
从这些示例中,可以看到字体系列模型的限制如何影响字体分组到家庭中的方式。 由于字体系列按名称标识,这意味着同一字体可以具有不同的系列名称,具体取决于所使用的字体系列模型。
DirectWrite 不直接支持 RBIZ 字体系列模型,但它提供转换到 RBIZ 模型的方法,例如 IDWriteGdiInterop::CreateFontFromLOGFONT 和 IDWriteGdiInterop::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 窄粗体 | Arial Narrow | 700 | 0 | Arial | 700 | 3 | 0 |
如你所看到的,“Arial Narrow”的 lfWeight
和 lfItalic
值与“Arial”相同,因此它具有不同的 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_ITALIC 和 DWRITE_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 表中声明 命名实例。 命名实例使变量字体的行为与静态字体的集合类似。 枚举 IDWriteFontFamily 或 IDWriteFontSet的元素时,每个静态字体和每个命名变量字体实例都有一个元素。
版式字体匹配算法首先根据姓氏选择潜在的匹配候选项。 如果匹配候选项包括变量字体,则同一变量字体的所有匹配候选项将折叠为一个匹配候选项,其中每个变量轴都分配了一个尽可能接近该轴请求的值的特定值。 如果变量轴没有请求的值,则会为其分配该轴的默认值。 然后,匹配候选项的顺序通过将其轴值与请求的轴值进行比较来确定。
例如,考虑 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
轴和 wght
轴分别设置为 12
和 475
。 这会生成以下匹配字体,非斜体字体排在第一位,因为它与 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 命名实例。 相比之下,权重拉伸样式匹配算法仅返回命名实例。
字体匹配顺序
权重拉伸式字体系列模型(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) 纠缠斜体和倾斜性,在版式模型中,它分解为“斜体”轴(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 系列名称。 例如,文档可以指定“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 系列名称或完整字体名称。 匹配以下列方式之一发生,按优先级降序排列:
该名称与版式系列(例如 Sitka)匹配。 匹配发生在版式系列中,并且使用所有请求的轴值。 如果名称还与 WWS 子家庭(即比版式系列小一个)匹配,则 WWS 子家庭中的成员身份将用作断层。
该名称与 WWS 系列匹配(例如 Sitka Text)。 匹配发生在 WWS 系列中,并忽略除“wght”、“wdth”、“ital”和“slnt”以外的请求轴值。
该名称与 GDI 系列匹配(例如,Getschrift Condensed)。 匹配发生在 RBIZ 系列中,并忽略除“wght”、“ital”和“slnt”以外的请求轴值。
该名称与全名匹配(例如,Schrift Bold Condensed)。 返回与全名匹配的字体。 请求的轴值将被忽略。 允许按全字体名称进行匹配,因为 GDI 支持它。
上一部分介绍了一个名为“Legacy”的模糊版式系列。 混合算法通过将“Legacy”或“Legacy Soft”指定为系列名称来避免歧义。 如果指定了“旧软”,则不存在歧义,因为匹配仅在 WWS 系列内发生。 如果指定了“旧版”,则版式系列中的所有字体都被视为匹配候选项,但使用“旧版”WWS 家族的成员身份作为断层符来避免歧义。
假设文档指定系列名称和粗细、拉伸和样式参数,但没有轴值。 应用程序首先可以通过调用 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues将粗细、拉伸、样式和字号转换为轴值。 然后,应用程序可以将系列名称和轴值传递给 idWriteFontSet4::GetMatchingFonts。 GetMatchingFonts 按优先级返回匹配字体的列表,并且结果适合指定家族名称是版式系列名称、粗细拉伸式系列名称、RBIZ 系列名称还是全名。 如果指定的系列具有“opsz”轴,则根据字号自动选择适当的光学大小。
假设文档指定粗细、拉伸和样式,也 指定轴值。 在这种情况下,还可以将显式轴值传递到 idWriteFontSet4::ConvertWeightStretchStyleToFontAxisValuesIDWriteFontFontSet4::ConvertWeightStretchStyleToFontAxisValues,并且该方法返回的派生轴值将仅包含未显式指定的字体轴。 因此,文档(或应用程序)显式指定的轴值优先于从粗细、拉伸、样式和字号派生的轴值。
混合字体选择 API
混合字体选择模型由以下 IDWriteFontSet4 方法实现:
IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 方法将字体大小、粗细、拉伸和样式参数转换为相应的轴值。 客户端传入的任何显式轴值均从派生轴值中排除。
IDWriteFontSet4::GetMatchingFonts 方法返回给定系列名称和轴值数组的匹配字体的优先列表。 如上所述,系列名称参数可以是版式系列名称、WWS 系列名称、RBIZ 系列名称或全名。 (全名标识特定字体样式,如“Arial Bold Italic”。GetMatchingFonts 支持通过全名匹配来匹配 GDI,这也允许它。
以下其他 DirectWrite API 还使用混合字体选择算法:
如果通过 调用 IDWriteTextLayout4::SetFontAxisValues 或 IDWriteTextLayout4::SetAutomaticFontAxntAxes 启用版式字体系列模型,IDWriteTextLayout4::SetAutomaticFontAxes
正在使用的字体选择 API 的代码示例
本部分演示完整的控制台应用程序,用于演示 IDWriteFontSet4::GetMatchingFonts 和 IDWriteFontSet4::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;
}
若要舍入示例,下面是命令行分析函数和主 函数:
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());
}
}