编程

Qt 6.7 未来的文本改进

1277 2023-12-16 21:37:00

Qt 6.7 的特性在今天冻结了,现在开始进一步稳定代码的过程。离实际发布还有几个月的时间,本文将展望一下 Qt 和 QtQuick 中字体处理的改进。更具体地说,讨论了三件事:可变字体支持、全新的“大字体”文本渲染器和字形特性支持。

可变字体

过去,字体最多有四种变体(或所谓的子族/子集):常规(Regular)、粗体(Bold)斜体(Italic)斜体加粗(Italic Bold)。如果你想在此基础上进行任何更改,你必须为这些字体创建一个全新的系列名称,因为除了提供一个字体系列+这四种样式中的一种之外,没有其他方法可以消除字体选择的歧义。

如今,有了更多的灵活性:字体可以有一个任意的“样式名”(或“子族名”),许多字体都有大量的变体可供选择。变化可以是像以前一样的粗细或斜体,也可以是其他属性,如字符宽度-这样,就可以创建非常特定的字体子集,例如 “Bahnchrift Semibold Semicontended” 。(历史说明:与旧字体系统的兼容性实际上在 Windows 上仍然可见,在 Windows 中,子集名称通常被命名为 “legacy” 家族名以实现向后兼容性,并且您有一组次要的 “typographic” 名称,这些名称为您提供了字体的真正字体集和子集。)

从逻辑上讲,子族都属于一个通用字体族,但从文件系统的角度来看,它们是作为独立的字体文件实现的。(在某些情况下,你可以有一个所谓的 “TrueType collection” 文件,其中包含一组多个字体,但这实际上只是一个更容易分发的包装,其中包含每个单独字体文件的完整副本。)

近年来,出现了一种新的子族方法:可变字体具有称为“轴”的浮点参数,可以改变字体的字形显示方式。字体设计者为每个轴提供了一组“主控形状”,用于识别每个字形在最小值和最大值(可能还有其他值)下的外观。然后,它们可以为沿轴的不同值的特定组合定义“命名实例”,字体系统可以在主控形状之间进行插值,以提供所需的确切外观。

例如,字体可以支持标准的 “wdth” 和 “wght” 轴,用于使用四个母版修改字形的宽度和重量:wdth=50、wght=100、wdth=200 和wght=900。一旦实现了这一点,字体设计师就可以定义一个名为 “Semibold Semicondensed” 的实例,其中 “wdth” 设置为 87,“wght” 设置为 600,插值将完成其余操作-无需为字体宽度和字体粗细的特定组合手动设计每个字形。

在以前的 Qt 版本中,根本不支持将这些字体加载为应用字体,如果尝试,结果会非常糟糕。举个例子,这张 Qt6.6 应用中加载的变量 Anyone 字体的屏幕截图。字体中的可变轴固定在其最小值,在明确要求的情况下,光栅化器会合成粗体/斜体。幸运的是,大多数可变字体仍然与一组向后兼容的 “static” 字体文件一起分发,这是迄今为止在 Qt 中加载它们的唯一方法。

在 Qt 6.7 中,我们增加了对在所有平台上加载可变应用字体的支持。QFontDatabase 将查找存储在字体中的命名实例,并将其添加为可选择的子族。它们可以使用样式名称特性或按 weight/style 进行选择,与存储为单独文件的传统子族相同。

当使用传统属性(如 weight 和 width)选择字体时,将首选预定义的命名实例,Qt 将尝试找到最匹配的实例。然而,在一些高级用例中,您可能希望更直接地访问变量轴,以获得所需的确切外观。

虽然许多字体只支持标准轴,如 “wght”、“wdth” 和 “ital”,但字体设计者并不局限于这些。四个  latin-1 字符的任何标签都可以用于指定轴,并且可以以戏剧性的方式改变字形的外观,只要轮廓和控制点的数量在所有主图形中保持不变。(按照惯例,自定义轴将是大写的,以确保与未来引入的官方轴不冲突。)

以支持自定义 “TIME” 轴的 Anicons 字体为例。更改 “TIME” 将导致字形在字体中定义为“主帧”的关键帧之间设置动画。

注意:如前所述,此功能在所有平台上都可用,但在 Windows 上,它要求您使用 DirectWrite 后台或跨平台 Freetype 后台运行应用。这可以通过将例如 -platform-windows:fontengine=directwrite 作为命令行参数传递给应用程序来实现。或者,它也可以在应用程序的 qt.conf 中自定义。DirectWrite 后台计划在未来取代 Windows上 的默认功能,但目前它是可选的。这是因为它目前不支持传统的字体名称或位图字体,因此它将返回比默认 GDI 后端稍小的字体集。

文本的曲线渲染器

在 Qt Quick 中渲染文本的默认方式(自2.0版本以来)是被称为 Qt 渲染的方式。这主要是因为我们不想将自己锁定到默认的特定实现,所以我们选择了一个更通用的名称。在底层,它使用了一种目前公认的方法:存储在纹理中的二维距离场来表示字形的轮廓,以及一个片段着色器,该着色器可以使用相同的源纹理在不同大小和比例的范围内有效地渲染这些图形。

事实证明,这是性能和质量之间的一个很好的折衷方案,默认方法在这一点上不太可能改变。然而,在某些情况下,算法的局限性对用户是可见的。

问题主要出现在字体对于源纹理来说太大时。该算法不再能够忠实地以目标大小重新创建字形,您将看到伪影,尤其是在尖角或 thin 特性上。

从这个屏幕截图中可以明显看出,在像素大小 200 处存在可见的伪影,根据您的用例,这可能是可接受的,也可能是不可接受的。在像素大小为 500 的情况下,artifacts 会扭曲文本,对于大多数用例,如果您需要使用该大小的字体,您可以解决方案。

幸运的是,我们已经有了解决方案:在 Qt 6.7 之前,主要的缓解方法是提高 renderTypeQuality(这将导致相关字体的内存消耗增加)或使用Text。NativeRendering 渲染类型(这需要以目标大小对字形进行预光栅化,因此这既需要 CPU 时间成本,也需要内存成本。)

在 Qt 6.6 中,我们为 Qt Quick Shapes 引入了一种新的渲染器,我们称之为曲线渲染器,在 Qt 6.7 中,它现在也作为可选的文本渲染器包括在内,它应该非常适合渲染大文本。

它的 GPU 时间成本确实比 QtRendering 高,所以它不会很快取代默认值,但对于更大的文本,即使在非常大的范围内,它也会给你带来漂亮的结果,而不会占用太多的显卡内存。

若要尝试此操作,请将 renderType 属性设置为 Text.CurveRenderer 并在 Qt 6.7 上运行。对于大多数应用文本,差异应该不是很明显,但对于大型 banner 或标题,它可能会带来实质性的改进。

字体塑造功能

最后,为高级用户提供了一个新的 API:对于上下文,“文本整形(text shaping)”是我们所说的获取 Unicode 字符和字体序列,然后将其转换为一组字形及其相对位置的过程。这涵盖了语法上必要的处理,例如在阿拉伯语或印度语中将某些字符序列组合成连字,还涵盖了更多的修饰特征,如 kerning

虽然 Qt 中字体整形器的默认行为通常是您想要和期望的,但在某些情况下,能够直接影响这个过程是很方便的。为了为用户启用这一功能,我们现在通过 Qt 的字体 API:C++ 中的 QFont::setFeature() 和 QML 中的 font.features 来公开此类字体整形功能。

字体功能类似于可变轴,因为它们将值与四个字符的 “tag” 相关联。然而,对于功能,该值是一个整数,在实践中通常是 0 或 1- off 或 on。启用或禁用给定的功能决定了在决定选择哪个字形及其相对位置时是否包括该功能。

OpenType® 了已注册功能的列表。与可变轴一样,字体可以包含具有任何有效标记的功能,并且按照惯例,大写标记用于自定义功能。

某些功能将在 Qt 中默认启用,具体取决于设置的其他字体属性。API 授予您重写 Qt 默认为的任何内容的权限,并强制执行您需要的行为。您在 QFont 中未设置的任何功能都将保持其默认值。

例如,除非设置了自定义字母间距,否则默认情况下 Qt 将启用可选的连字。对于某些字体,这意味着某些典型的字符组合将显示为单个合并的字形,这可能比相邻的单个字母在视觉上更令人愉悦。一个常见的例子是 “fi” 和 “ffi”,它们在许多字体中都有相应的连字。例如,以下是Windows 中的 Calibri 字体:

当使用此字体并启用连字功能时(默认情况下),“f” 和 “i” 的任何立即序列都将被一个单独的组合字形所取代,该字形将文本视觉地绑定在一起。

然而,在某些情况下,字母在逻辑上是分开的,不应该以这种方式组合。一个例子可能出现在键盘快捷键的文档中:

将这些字符合并会使其不太清楚这些是单独的关键笔划。为了确保这种情况不会意外发生,我们可以将 “liga” 特性设置为 0,从而在成型过程中禁用该功能。

priceconsole-noliga

通过启用/禁用字体功能,可以进行更多这些较小的调整。此外,某些字体包含不同风格的字形替代品,可以使用例如“salt”特性进行选择。

结论

正如开头所提到的,这些都将在 Qt 的下一个版本中出现。字体功能已经通过技术预览版 API 在 Qt 6.6 中提供,但请注意,C++ API 将根据反馈在 Qt 6.7 中更改,因此任何使用它的代码都必须为新的 API 更新。

对于我们当中的冒险者来说,这里讨论的一切都可以通过在 code.Qt.io 中获取 Qt 的 dev 分支并手动构建来进行测试。但与往常一样,在使用任何开源项目的开发分支时,要做好可能发生错误的准备,因为这个分支每天都在不断更新(如果你确实发现了错误,我们当然很乐意听到它们,这样我们就可以确保在发布前修复它们。)

最后一点:Qt 6.7 仍在开发中,因此,由于我们的多阶段审查过程,新的 API 和功能仍有可能发生变化。因此,如果升级到新快照后遇到问题,请务必查看快照文档。

 

C++