如何在使用DirectWrite时平衡基于脚本的OpenType功能与其他OpenType功能?

10
全面披露:我正在开发我的libui GUI框架的文本API。这个API在Windows上使用DirectWrite,在OS X上使用Core Text,在其他Unix系统上使用Pango(它使用HarfBuzz进行OpenType shaping)。我想指定的文本格式属性之一是要使用的一组OpenType特性,这三个库都提供;DirectWrite的是IDWriteTypography。
现在,当你使用这些库绘制一些文本时,默认情况下会启用一些有用的OpenType特性,例如标准连字(如f+i连字)的liga。我以为这是特定于字体的,但事实证明这是特定于正在被塑造的文本的脚本。Microsoft为OpenType支持的所有脚本提供了指导方针(在“特定于脚本的开发”下),我可以看到在HarfBuzz中执行所有这些操作的相当复杂的逻辑。
在Core Text和Pango中,如果我启用其他属性,它们将添加到这些默认值之上。但是在DirectWrite中,特别是IDWriteTextLayout::SetTypography(),这样做会删除默认值:
生成此输出的程序可以在此处找到。
显然,我的第一个选择是询问如何在DirectWrite中获取默认特性。尽管已经有人在这个网站上这样做了,但答案似乎是否定的。
我猜DirectWrite允许我完全控制要应用于某些文本的特性列表。这很好,除了我不能在其他API中这样做,除非我以某种方式明确禁用默认功能!当然,我不知道这个列表是否会改变,所以硬编码可能不是最好的主意。
即使硬编码是一个选择,我也可以获取每个脚本的HarfBuzz列表,但是a)它相当复杂;b)对于一个脚本,有多个可能的shaper,取决于(我认为)版本兼容性(例如,缅甸)。
那么为什么不使用HarfBuzz的列表重新创建DirectWrite的默认特征列表呢?毕竟,它似乎想要对其他字形生成器精确,所以这应该可行,不是吗?好的,我需要做两件事情:确定要使用哪个脚本,并确定在其中的哪些字符上使用哪些属性,以使字符在单词中的位置很重要。
DirectWrite提供了一个接口IDWriteTextAnalyzer,提供执行字形处理的功能。我可以使用它,但是它似乎返回脚本数据在DWRITE_SCRIPT_ANALYSIS结构中,而脚本ID的描述说“写作系统脚本的从零开始的索引表示”。这并没有帮助,所以我编写了一个程序来仅转储我输入的文本的脚本编号。在输入字符串上运行它。
لللللللللللللاااااااااالا abcd محمد ابن بطوطة‎‎ Отложения датского яруса

产生输出
0 - 26 script 3 shapes 0
26 - 5 script 49 shapes 0
31 - 14 script 3 shapes 0
45 - 2 script 1 shapes 1
47 - 25 script 22 shapes 0

我无法将这些脚本编号与任何Windows头文件中的内容匹配:如果在任何API中定义了阿拉伯语、拉丁语或西里尔语的编号,它们都不匹配这些编号。即使我得到了脚本和脚本编号之间的映射,这仍然不能为我提供应用单词内部特征所需的数据。
那么Uniscribe呢?其等效的SCRIPT_ANALYSIS类型的文档表明,它的脚本ID是一个“[opaque]值”,其“该成员的值是未定义的,应用程序不应依赖其值从一个释放版本到下一个版本的值相同”。虽然我可以通过语言代码来确定脚本,但除了“西方”(拉丁文?)脚本的LANG_ENGLISH外,还没有定义的值。DirectWrite的值是否与Uniscribe的值相同?而且看起来我至少可以通过查看fLinkBefore和fLinkAfter字段来确定单词的初始和最终状态,但这是否足以正确地应用每个脚本的属性?
HarfBuzz确实有一个实验性的DirectWrite后端,并且不打算被真正的程序使用;我还不确定它是否具有我上面指定的相同特性破坏。如果我找出来了,我会在这里更新这部分。
最后,如果我在类似kaxaml的东西中输入以下等效测试用例:
<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>  
  <FlowDocumentPageViewer>
  <FlowDocument FontFamily="Constantia" FontSize="48">
  <Paragraph>
  afford afire aflight 1/4<LineBreak/>
  <Run Typography.Fraction="1">afford afire aflight 1/4</Run>
  </Paragraph>
  </FlowDocument>
  </FlowDocumentPageViewer>
  </Grid>
</Page>

我看到连字符被正确应用,即使在后一种情况下:
(结尾的分数只是为了证明那个属性被应用了。)如果我假设XAML使用DirectWrite,则证明我的第一个选项(简单地将自定义属性覆盖在默认属性上)应该是可行的...(我基于这样的想法做出这个假设:XAML提供了一个非常相似的API来绘制2D图形,与我必须手动编写大量粘合代码来使用vanilla Direct2D做相同的事情相比,在XAML中有很多填补的空缺,因此我认为在XAML中可能的任何事情都可能在Direct2D中实现,并且通过扩展DirectWrite也可以实现,因为它们在技术上是同时引入的...)
此时我完全迷失了。我希望至少在各平台上能够预测,但我不确定程序甚至是否应直接使用OpenType功能。我对文本布局API有错误的期望吗?如果我想要这个,我是否必须放弃IDWriteTextLayout并自己完成所有文本成形和布局?
还是说我必须放弃普通的Windows 7支持并升级到平台更新的DirectWrite功能集?甚至放弃Windows 7?

如果您只需要用于UI文本,可以说最好使用系统默认设置,而不是调整功能。我不记得是否已经在DirectWrite上测试过这一点,但是完全控制功能列表似乎是没有用的,因为您必须了解每个脚本的功能集,并且禁用强制功能是没有用的。 - bunglehead
1
@bunglehead 谢谢。我需要通过进一步的讨论和发现来修改这篇文章,但事实证明你无法禁用强制功能,所以问题实际上更多地涉及如何获取(或重新获取)所有可选功能。脚本部分很遗憾;GetScriptProperties()是在IDWriteTextAnalyzer1中新增的,我可以使用它来猜测哪些脚本得到了什么,但我可以使用Uniscribe的ScriptItemizeOpenType()并获取OpenType脚本标记而不是ISO脚本代码,这可能会使它变得更容易...我将在明天更新的问题中写更多内容。 - andlabs
2
我强烈建议您在http://typedrawers.com上提出这个问题,而不是在这里。当然,这可能是一个“编程”问题,但是typedrawers是所有字体设计师、渲染引擎工程师和印刷排版大咖聚集的地方。在那里得到比你需要的更详细的答案的几率几乎是无限的。(显然要保持这里的开放性,如果您在任何一个网站上得到了答案,请将该答案链接到另一个网站,以便两个社区都能从易于找到的答案中受益) - Mike 'Pomax' Kamermans
在Windows上注意:Windows 7已经停止主流支持将近两年半了。EOL始于2015年1月13日,扩展企业支持将在2020年结束。维护win7兼容性没有任何价值。只需在最终准备发布时将应用程序与必要的MSVC++运行库分发捆绑在一起,以获取您的应用程序所需的DirectWrite API即可。 - Mike 'Pomax' Kamermans
谢谢您的建议。不过我已经自己知道了答案;将来如果有关于OpenType的问题,我会记住那个网站。DirectWrite是一个系统依赖项,而不是MSVC运行时依赖项,所以操作系统版本在这里是个问题。 - andlabs
显示剩余3条评论
2个回答

4

在与Peter Sikking和Ebrahim Byagowi讨论后,我调试了一个更通用的程序来测试事情,并找出了内部发生了什么。

然而,首先要说的是这同样适用于Uniscribe和DirectWrite

事实证明,无论我使用什么特性集,DirectWrite始终提供一组默认的OpenType特性!情况是,提供的默认特性列表取决于我是否加载自己的特性以及取决于整形引擎。对于水平书写模式和英语中的latn脚本,这是通过“通用引擎”完成的。

如果我不提供任何特性,通用引擎将加载特定于脚本的特性。对于水平的latn,此列表为

locl
ccmp
rlig
rclt
calt
liga
clig

如果我提供功能,通用引擎将为所有脚本使用相同的默认列表:
locl
ccmp
rclt
rlig
mark
mkmk
dist

所以我不知道该怎么办。我可能只需要在libui代码中自己提供liga和其他一些东西(当然标记为HACK),但这仍然很奇怪。我也不确定动机是什么。无论如何,这解释了我看到的行为。


1
如果您的问题总体上是关于编程或至少涉及编程,我将尝试回答您一些疑问句。这取决于情况。如果IDWriteTextLayout接口在所有方面都适合您的项目任务,除了直接写入默认排版特征的易变性之外,请了解有关排版的内容并创建适合您需求的IDWriteTypography实例。为程序开发自定义文本布局可能需要大量时间和精力,特别是如果该程序应呈现双向文本、复杂脚本、内联对象等。
有时候,你的项目任务可能需要开发一个文本布局引擎,不仅仅是为了控制呈现文本中使用的排版特性。例如,你的经理/客户可能要求实现定制的断行机会或字形推进对齐算法。在这种情况下,您将实现IDWriteTextAnalizer::GetGlyphs方法。该方法具有参数DWRITE_TYPOGRAPHIC_FEATURES ** features,const UINT32 * featureRangeLengths,UINT32 featureRanges,这些参数使您能够替代一组“默认”的排版特性以呈现要呈现的文本范围(请参阅我对另一个问题的答案What are the default typography settings used by IDWriteTextLayout?)。只有受影响的特性才会被更改;其他特性具有其“默认”值。此外,如果您在下一个文本范围的GetGlyphs调用中省略这些参数(例如,使用NULL、NULL、0的值),则上一个GetGlyphs调用中更改的特性不会被该下一个范围的调用更改。
这个等效SCRIPT_ANALYSIS类型的文档说明其脚本ID是一个“[不透明]值”,其“该成员的值未定义,应用程序不应依赖其值从一个版本到另一个版本相同”。虽然我可以获取语言代码来识别脚本,但除了LANG_ENGLISH外,“西方”(拉丁?)脚本没有定义的值。
严格来说,这不是一个疑问陈述,但我想你对Unicode脚本ID的定义以及如何使用API对如此模糊定义的结构和常量感到不满意。
也许有点跑题,但我冒险假设“Unicode脚本ID”值的起源。截至2010-07-17,Unicode公司发布了Unicode 6.0版本。该标准包含了文档http://www.unicode.org/Public/6.0.0/ucd/PropertyValueAliases.txt,其中包含一个脚本列表。列表如下:
   # Script (sc)

   sc ; Arab      ; Arabic
   sc ; Armi      ; Imperial_Aramaic
   etc.

阿拉伯文脚本排名第1,西里尔文脚本排名第20,拉丁文脚本排名第47。此外,在其他地方我看到这个列表以通用和继承的脚本开始。它将阿拉伯文脚本排在第3位,西里尔文脚本排在第22位,拉丁文脚本排在第49位。这些序数对你来说很熟悉,是吗?
幸运的是,我们不需要依赖“Unicode脚本ID”值;我们需要的是脚本属性,而不是脚本ID或缩写。API在这方面是自洽的,当我们向GetScriptProperties方法传递从AnalyzeScript调用中得出的数字时,它会为文本范围提供实际的脚本属性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接