渲染文本,有多困难?事实证明,这非常难!据我所知,几乎没有任何系统能“完美地”呈现文本。这都是最好的努力,尽管有些努力比其他努力更重要。
我假设您希望支持用户提供的任意文本,这些文本具有自定义字体、颜色和带有换行和支持文本选择的样式。基本上是正确显示简单的富文本文档、终端、网页或其他任何内容所需的最低要求。
这里最重要的主题是:没有一致的正确答案,每件事都比你想象的重要得多,每件事都会影响其他一切。
我在这里关注的主题没有特别的韵味或理由,它们只是在Firefox中渲染几年后出现在我脑海中的主题。例如,我不会花太多时间讨论文本分割或管理不同平台特定文本库的挑战,因为我不会看太多。
文本很复杂,英语不善于表达这些细微差别。就本文件而言,我将尽量遵守以下条款。请注意,这些词不是“正确的”,我只是觉得它们对向没有语言学背景的英语母语人士传达关键概念很有用。
标量:Unicode标量,Unicode描述的“最小单位”(又名代码点)。
字符:一个Unicode扩展字集集群(EGC),Unicode描述的“最大单元”(可能由多个标量组成)。
字形:由字体产生的一种原子呈现单位。通常,字体中会有一个唯一的ID。
连字:由几个标量,甚至可能是几个字符组成的字形(母语人士可能认为连字是多个“字符”,也可能不认为连字是多个“字符”,但对于字体来说,它只是一个“字符”)。
脚本:组成某种语言的一组字形(字体倾向于实现特定的脚本)。
颜色:字体的RGB和Alpha值(有些用例不需要Alpha,但很有趣)。
样式:字体的粗体和斜体修饰符(暗示、别名和其他设置在实际实现中也会被塞满)。
为了让您了解典型的文本渲染管道是如何工作的,下面是一个简单的示意图:
大多数字体实际上并没有提供所有现存的字形。字形太多,所以字体通常设计为只实现特定的脚本。最终用户通常不知道或不关心这一点,因此当字符不可用时,一个健壮的系统必须级联到其他字体中。
例如,尽管以下文本的标记并不表示存在多种字体,但在所有系统上正确绘制它绝对需要:hello😺 मनीष بسم 好. 这与步骤1(造型)非常接近,这取决于步骤3(造型)的结果!
(或者,你可以采用Noto方法,使用一种包含所有字符的Uber字体。尽管这意味着用户无法配置字体,也无法在所有平台上为用户提供“原生”文本体验。但假设你想要更强大的解决方案。)
同样,布局要求您知道文本的每个部分占用了多少空间,但这只有在您塑造文本之后才能知道!第二步取决于第三步的结果?
造型完全取决于你对自己的布局和造型的了解,所以我们似乎被卡住了。我们该怎么办?
首先,造型会作弊。虽然我们真正想要的字体是完整的字形,但样式设计只需要询问标量。如果字体不能正确支持脚本,它就不应该声称知道组成脚本的标量。因此,我们可以很容易地找到“最佳”字体,如下所示:
对于文本中的每个字符(EGC),不断询问级联中的每个字体是否知道组成该字符的所有标量,如果知道,就使用它。如果我们在没有供应商的情况下走到最后,那么我们就会生产豆腐(, 缺少图示符指示器)。
在表情符号的例子中,你可能已经见过这个过程的失败模式了!因为一些表情符号实际上是几个简单表情符号的连字,所以字体可能会成功地报告对该字符的支持,同时只生成组件。所以🤦🏿♀️ 可能看起来像🤦 🏿 ♀ 如果字体“太旧”,无法了解新的连字。如果unicode实现“太旧”,无法了解字符,导致样式系统接受字体中的部分匹配,也会发生这种情况。
因此,现在我们不需要查看布局或形状,就可以确切地知道我们将使用什么字体(尽管形状可能会改变我们的颜色,后面的部分将对此进行详细介绍)。我们能同时解开布局和形状吗?不!像段落分隔符这样的东西会让你在行上有一个很好的硬中断,但是进行包装的唯一方法是迭代地进行造型!
你必须假设你的文本只适合一行,并塑造它,直到你用完空间。在这一点上,您可以执行布局操作,并确定在何处打断文本并开始下一行。重复这个过程,直到所有的东西都成型并布置好。
来自英语,你可能会认为连字只是花哨的绒毛。我的意思是,谁真的在乎“æ”是否写为“ae”?事实证明,有些语言基本上完全是连字。例如“ड्ड بسم“具有”的个性ड् ड بسم”。如果您在一个有能力的文本呈现系统(任何主要浏览器)中查看此内容,那么这两个字符串看起来应该非常不同。
不:这不是unicode标量和扩展的grapheme集群之间的区别。如果你向一个unicode健壮的系统(比如Swift)询问该字符串的扩展字符集,它会吐出这5个字符!
一个字符的形状取决于它的邻域:你不能一个字符一个字符地正确绘制文本。
也就是说,你必须使用一个成形库。这方面的行业标准是HarfBuzz,要实现自己的标准非常困难。使用哈夫布兹。
草书的字形经常相交以避免接缝,这可能会给您带来问题。
让我们看看“मनीष “又来了。看起来不错吧?让我们把它炸了吧:
如果你在狩猎或边缘,这可能仍然看起来不错!如果你使用的是Firefox或Chrome,它看起来很糟糕,如下所示:
问题是Chrome和Firefox正在试图作弊。他们吃蔬菜,并正确地塑造文字,但一旦他们有了字形,他们仍然试图单独绘制。除了透明和重叠之外,这基本上可以正常工作。然后在重叠处变暗。
“正确”的实现会将文本绘制到没有透明度的临时曲面上,然后将该曲面合成到具有透明度的场景中。Firefox和Chrome不这么做,因为它很昂贵,而且对于主要的西方语言来说通常是不必要的。有趣的是,他们确实理解这个问题,因为他们实际上会竭尽全力专门处理emoji(但我们将在后面讨论)。
好的,这一个主要是一个好奇,因为我不知道有任何超合理的情况下会发生这种情况,但它自然会下降的加价。以下是两段内容相同但颜色样式不同的文本:
पन्ह पन्ह त्र र्च कृक ृ ड ्ड न ्ह ृ े إل ا ب س م ا ل ل ه
पन्ह पन्ह त्र र्च कृकृ ड्ड न्हृे إلا بسم الله
以下是它们在Chrome中的外观(如果使用新的布局实现):
我想每个人都应该像Firefox那样做,对吧?但如果我们放大,我们可以看到它正在做一些非常刺耳的事情:
问题是,这里真的没有合理的答案。我们已经用不同的样式分解了一个连字,由于连字在某种意义上是一个呈现“单元”,所以拒绝支持它是合理的(就像大多数人那样)。
不管出于什么原因,Firefox上的工作人员非常热衷于尝试更优雅地处理它。一般的方法是用最佳猜测面具和不同的颜色多次画连字,效果出人意料地好!
尝试支持这些“部分连字”有一些好处:只有整形才能知道是否会发生连字,而且它可以取决于系统特定的字体,所以连字可能会出现在没有人预料到的地方!这里的经典英文示例是用户安装的字体跨越超链接边界的æ连字。
还有一点很糟糕,英语可以改变中间词的风格,但草书却不能?
如果按照本机系统的方式绘制表情符号,则需要不尊重文本的颜色设置(透明度除外):
你好❤️ 😺 🎉 ™️ 🥶 😡 😈 🤟 🤟🏻 🤟🏿 那里(黑色)
你好❤️ 😺 🎉 ™️ 🥶 😡 😈 🤟 🤟🏻 🤟🏿 那里(红色)
你好❤️ 😺 🎉 ™️ 🥶 😡 😈 🤟 🤟🏻 🤟🏿 那里(透明)
你好❤️ 😺 🎉 ™️ 🥶 😡 😈 🤟 🤟🏻 🤟🏿 那里(粗体)
你好❤️ 😺 🎉 ™️ 🥶 😡 😈 🤟 🤟🏻 🤟🏿 有(斜体)
表情符号通常有自己的本色,这种颜色甚至可以有语义意义,就像肤色修饰语一样。更麻烦的是:它们有多种颜色!
据我所知,这在表情符号出现之前并不是一件真正的事情,所以不同的平台以不同的方式处理这件事。一些人将表情符号作为一个直接的图像提供(苹果),另一些人将表情符号作为一系列单色图层提供(微软)。
后一种方法有点不错,因为它通过“仅仅”将一个glyph去糖化为一系列单色glyph,与现有的文本渲染管道很好地集成,每个人都习惯于使用这些glyph。
然而,这意味着您的样式可以在绘制“单个”字形时反复更改。这也意味着一个“单一”字形可能会重叠,从而导致前面一节中讨论的透明度问题。然而,如上所示,浏览器确实正确地合成了表情符号的透明度!
您已经需要检测颜色图示符并对其进行特殊处理,因此很容易为它们采用特殊的合成路径
草书有点难看,透明度不好,但表情符号很可怕/胡言乱语,所以额外的工作是合理的
哦,还有,将表情符号斜体化或加粗是什么意思?你应该忽略这些风格吗?你应该合成它们吗?谁知道呢。🤷♀️
是的,不管出于什么原因,很多系统都会秘密地增加表情符号的字体大小,让它们看起来更好。
文本非常小,非常详细,而且非常重要的是它很容易阅读。听起来像是反走样(AA)的工作!哦,480p真的是低分辨率,嗯。更多AA!!!
灰度AA是消除混叠的“自然”方法。其基本思想是为部分覆盖的像素提供部分透明度。在构图过程中,这将导致像素稍微着色,就像它被稍微覆盖一样,从而创建更清晰的细节。
它是灰度的,因为这是用于一维颜色的术语,就像我们的一维透明度(否则字形往往是单一的纯色)。同样,在白色背景上的黑色文本的常见情况下,抗锯齿会在边缘周围显示为灰色。
亚像素AA是一种滥用桌面显示器上像素布局的常见方式的把戏。它比这更复杂,所以如果你真的感兴趣,你应该查一下,但这里有一个TL;高层概念的DR:
你的显示器的像素实际上是三小列红、绿、蓝。如果你把一个像素变成红色,你也会把它变成“黑白”。同样地,如果你把它变成蓝色,你就变成了“黑白”。换句话说,通过混淆颜色,你可以将水平分辨率提高三倍,获得更多细节!
你可能会认为这看起来非常混乱,像彩虹一样,但实际上效果非常好(有些人不同意)。人类的大脑喜欢看到模式并使事情变得平滑。这就是说,如果你拍摄一张亚像素AA文本的屏幕截图,如果你调整图像大小,或者甚至在不同亚像素布局的显示器上观看,你绝对能够看到颜色。这就是为什么文本截图看起来非常奇怪和糟糕的原因。
(总的来说,这也意味着图标的颜色可能会意外地改变其感知的大小和位置,这真的很烦人。)
所以亚像素AA是一个非常好的黑客,可以显著提高文本的易读性,太棒了!但是,可悲的是,这也是一个巨大的颈部疼痛!
请注意,无论使用哪种AA系统,也可以使用亚像素轮廓偏移。尽管您总是希望光栅化的图示符捕捉到完整像素,但光栅化本身适用于特定的子像素偏移(0到1之间的值)。
如果它的亚像素偏移为0.5,那么它的光栅化将是两个50%的灰色像素
栅格化图示符的成本惊人地高,所以您真的希望将其缓存在atlas中。但在使用亚像素偏移时,如何缓存轮廓光栅化?每个偏移量都有自己独特的光栅化,所以你不太可能得到这样的缓存命中率!
质量和性能必须平衡,这可以通过捕捉亚像素偏移来实现。对于英文文本,一个合理的平衡是在将水平子像素偏移捕捉为四分之一整数时没有垂直子像素精度。这只剩下4个亚像素位置,这仍然是质量上的一大改进,同时允许合理的缓存量。
greyscale AA的一个优点是,你可以玩得快一点,放松一点,它会优雅地退化。例如,如果在纹理上变换文本(缩放、旋转或平移),它可能看起来有点模糊,但基本上看起来很好。
如果你用亚像素AA做同样的事情,它看起来会很糟糕。亚像素AA背后的整个想法是,你在滥用显示器中像素的布局方式。如果显示的像素与纹理的像素不一致,则红色和蓝色边缘将清晰可见!
有人可能会认为,解决这个问题的“办法”是在新位置重新标记符号。事实上,如果转换是静态的,这是可行的。但如果变换是一个动画,这实际上看起来会更糟。这实际上是一个非常常见的浏览器错误:如果我们无法检测到某个文本正在发生动画,当每个字形在不同的子像素捕捉和每帧提示之间来回跳跃时,角色将抖动。
因此,浏览器包含了几种试探法来检测可能是动画的东西,以便它们可以强制禁用页面该部分的亚像素AA(理想情况下甚至亚像素定位)。这可能很难做到可靠,因为任意复杂的JS可以驱动动画,而不给浏览器任何清晰的“提示”。
此外,如果涉及部分透明度,亚像素AA也有问题。基本上,我们正在调整R、G和B通道,以编码3个透明度值(每个子像素一个),但文本本身也有一种颜色,并且文本上的内容会改变,因此信息很容易丢失。
当使用greyscale AA时,我们有一个专用的alpha通道,因此不会丢失任何东西。因此,当涉及透明度时,浏览器倾向于使用灰度AA。
除了Firefox。再一次,这是一个奇怪的地方,有人在Firefox上工作非常热情,做了一些复杂的事情:组件Alpha。事实证明,你实际上可以正确地合成亚像素AA文本,但它实际上需要有3个额外的通道,专门用于R、G和B通道的透明度。不出所料,这会使以这种方式合成的文本的内存占用增加一倍。
在较新版本的macos上,默认情况下在操作系统级别禁用文本的子像素aa
Chrome似乎更积极地禁用了亚像素aa(不确定具体的策略是什么)
Firefox的新图形后端(webrender)为了简单起见放弃了Alpha组件
这部分只是一些不需要太多讨论的小东西。
天呐,这太棒了。这些字体主要由Adobe提供,因为它们不久前就真正进入了SVG。有时候你可以忽略SVG部分(我相信源代码Pro字体在技术上包含一些SVG字形,但实际上它们并没有被网站使用),但通常你需要实现SVG支持来绘制所有字体。
你听说过SVG动画字体吗?不好的我认为他们现在到处都失败了。(由于一些热心的开发者,Firefox随机支持了它一段时间。)
如果你天真地尊重用户对超大字体(或超大缩放级别)的要求,你会在字形图谱的大小上遇到极端的内存管理问题,因为每个字符都可能比整个屏幕大。有几种方法可以解决这个问题:
在构图过程中以较小的尺寸和较高的比例栅格化轮廓(简单,产生模糊的边缘)
人们通常知道文本的主要方向可以是从左到右(英语)、从右到左(阿拉伯语)或从上到下(日语)。
在桌面上,如果将鼠标拖动到该文本上以选择它,您可能会注意到该选择在中间变得不连续和跳跃。这是因为我们在同一行中混合了从左到右和从右到左的文本,这绝对是经常发生的。
起初,向右拖动会增加选择,但随后会减少选择,直到它突然开始再次增加。这实际上是完全正确和可取的:选择只是在实际的底层字符串中保持连续。通过这种方式,您可以正确复制跨越转换的文本片段。
因此,您需要在选择代码的命中检测中处理这一点。在布局过程中,你还需要在换行算法中处理这个问题。
什么?哦,不你好 1234你好
当字体中缺少字符时,能够向用户传达这一情况是很好的。这是“豆腐”字形。现在,你可以只画一个空白的豆腐(一个矩形)并保留它,但是如果你想有所帮助,你可以写出缺少的字符的值,这样就可以更容易地调试它。
但是,等等,我们用文字来解释我们不能画文字?隐马尔可夫模型。
你可以假设系统必须有一个基本的字体,可以画0-9和a-F,但是对于那些希望用工具真正破坏工具的人来说,你可以做Firefox做的事情:microfont!
Firefox内部有一个小的硬编码数组,描述了一个小字体图集的一位像素艺术,正好对应这16个字符。所以在画豆腐的时候,它可以快速地画出那些字形,而不用担心字体。
对于高质量的字体,像斜体和粗体这样的样式是本机提供的,因为没有简单的算法可以很好地实现这些效果。
除了一些字体不提供这些样式,所以你需要一个简单的算法来实现这些效果。
准确地说,你是如何检测和处理这一切的,这是非常具体的系统,非常复杂,超出了我的专业领域,所以我不能很好地解释它。我只是在浏览Webrender的字体代码。
不管怎样,不管你做什么,你都需要一个综合的退路。谢天谢地,这些实现实际上非常简单:
老实说,这些方法做得相当不错!但用户可能会注意到事情似乎“不对”,如果你投入工作,你可以做得更好。
特定于平台的bug、优化和怪癖已经发展了足够长的时间,成为了美学。所以,即使你坚信某些东西是理想的或重要的,也总会有一大群用户有不同的偏好。健壮的文本呈现系统支持这些不同的首选项(同时选择合理的默认值)。
您应该支持系统配置、字体特定配置、应用程序特定配置和文本运行特定配置。你也应该试着
......