【问题标题】:Faking Subpixel Antialiasing on Text with Core Animation使用核心动画在文本上伪造亚像素抗锯齿
【发布时间】:2011-10-15 01:14:13
【问题描述】:

对于任何使用 Core Animation 图层支持视图的项目的人来说,很遗憾,对于未在预设不透明背景上渲染的文本,亚像素抗锯齿(文本平滑)被禁用是很明显的。现在,对于能够为其文本设置不透明背景的人(使用setBackgroundColor: 调用或Interface Builder 中的等效设置),此问题不会造成太大问题。但是对于其他人来说,他们完全没有办法解决它,这不是一个可以忽视的事情。

我已经在网上寻找了两天多的解决方案,但没有找到任何可用的解决方案。我要做的就是创建一个在工作表窗口上设置的文本标签(用户不可编辑)(工作表窗口背景是透明的,因此让工作表的内容视图将 wantsLayer 声明为 true 会禁用所有标签上的文本平滑窗户);很简单的事情。我见过很多有争议的解决方案(快速的谷歌搜索会在许多其他地方提出这个话题),但到目前为止,所有这些解决方案都依赖于人们能够并且愿意与不透明的背景妥协(你不能在一张纸上使用)。

到目前为止,我能想象到的最佳方向是将文本与文本平滑预渲染到图像上,然后像往常一样在图层支持的视图上显示图像。然而,这似乎是不可能的。我认为人们会尝试的“正常”方式是这样的:

NSAttributedString *string = [[self cell] attributedStringValue];
NSImage *textImage = [[NSImage alloc] initWithSize:[string size]];
[textImage lockFocus];
[string drawAtPoint:NSZeroPoint];
[textImage unlockFocus];

但这似乎不起作用(很可能是因为从drawRect: 调用时,设置的图形上下文已经禁用了文本平滑 - 亚像素抗锯齿已关闭,而是使用常规抗锯齿),但两者都没有在this question(您可以在其中创建自己的图形上下文)中找到更复杂的解决方案。

那么怎么可能做这样的事情呢?你能以某种方式“伪造”文本平滑的效果吗?是否有甚至可以设置某些东西的hack-ish解决方法?我真的不想因为这个愚蠢的问题而放弃 Core Animation;它有很多好处,可以节省大量时间和代码。


编辑:经过大量搜索,我找到了一些东西。虽然thread itself 只是重申了我的怀疑,但我想我可能已经找到了解决问题的方法:自定义CALayer 绘图。 Timothy Wood 在他的一个回复中附上了一个示例项目,该项目显示了使用多种技术进行字体平滑化,其中一些技术有效。我会考虑将其集成到我的项目中。

编辑#2:原来,上面的链接也是一个半身像。虽然链接的方法提供了亚像素抗锯齿,但它们在透明背景上也失败了。

【问题讨论】:

  • 不确定这是否是一个答案,所以发表评论。你检查过核心文本吗?这是一个随机的核心文本示例:pastie.org/2249243。它确实 drawRect 级别的文本绘图,具有抗锯齿和所有爵士乐。老实说,我不确定这如何与 CALayer 融合。
  • 我很确定 CoreText 与任何其他文本都存在同样的问题,不是吗?或者,至少,这是我一直相信的。
  • 我想当 Core Text 绘制到 CALayer 时,它最终会变成 rgba 像素。 IE。它是文本还是其他什么都没有关系,因为它是光栅化的。但现在我只是猜测。
  • 这就是问题所在。对于文本平滑,您需要绘制 ARAGAB 像素(即每种颜色都有自己的 alpha 值),这 CALayers 似乎无法处理...

标签: objective-c cocoa text core-animation antialiasing


【解决方案1】:

如果您使用的是透明纸,您事先不知道它下面的像素是多少。他们可能会改变。请记住,所有三种颜色都有一个 Alpha 通道:如果将其设为透明,则不会看到任何子像素效果,但如果将其设为不透明,则所有三个子元素都将与背景合成。如果您为在白色背景上合成的边缘提供了正确的颜色,那么如果背景更改为其他颜色,它将看起来不正确。

例如,假设您在白色背景上绘制黑色文本,并且子元素顺序为 RGB。右边缘可能是淡蓝色:高 B 值(远离字形一侧的全亮度),稍低但仍然高的 R 和 G 值(靠近字形的一侧亮度较低)。如果您现在将该像素合成在深灰色背景上,您将使它比直接在深灰色背景上渲染黑色文本时更亮。

基本上,您不会面临 CoreAnimation 的任意限制:在可能合成在任意背景上的透明层上使用子像素渲染根本没有意义。每个颜色通道都需要一个单独的 alpha,但由于缓冲区的像素格式是 RGBA(或 ARGB 或其他任何格式),所以您不能拥有它。

但这会导致我们找到解决方案。如果您知道背景将保持不变(例如,工作表显示在您控制其内容的窗口上),那么您可以简单地使您的图层不透明,用背景窗口覆盖区域的副本填充它,然后渲染亚像素抗锯齿文本。基本上,您会将图层与背景预先合成。如果背景保持不变,这将看起来与普通 Alpha 合成所做的相同,只是您现在可以进行亚像素文本渲染;如果背景发生变化,那么您无论如何都必须放弃进行亚像素文本渲染(尽管我猜您可以继续复制背景并在它发生变化时重新绘制不透明的叠加层)。

【讨论】:

  • 这似乎是答案。经过几天的搜索,我想真的不可能做到我想要实现的目标。我要做的是让我的工作表比我的父窗口小,这样背景就保持不变。感谢您的回答;我会再等一天左右,看看其他人有什么可说的,我会标记为正确并给你赏金。
  • 理论+1,但我不认为在不透明的背景上绘制然后将它们复制到半透明的纸张中是一个好主意。这种 hack 很可能在未来导致很多问题,因为违反了窗口管理器所做的基本假设(例如,两个窗口的内容是独立的)。在我看来,CALayers 是一种权衡:你放弃亚像素渲染以获得更高的性能。我不知道你想做什么的所有背景,但我建议你考虑什么更重要,而不是尝试肮脏的黑客来尝试两者兼得。
  • 感谢接受,Itai。顺便说一句,赏金即将到期。
【解决方案2】:

我不确定我是否掌握了这个问题,所以这是一个真正的平局。

但我注意到 LaC 的答案取决于以正常方式将像素写在彼此之上。就像在 Photoshop 中调用“正常混合模式”一样。

您可以通过许多其他方式合并两个图层,包括“相乘”:

这会分别将每个像素的 R、G 和 B 相乘。因此,如果您将文本放在白色背景上,则可以通过将顶层设置为“相乘”来显示更靠后的层,这会导致两个层像这样相互燃烧。

这也适用于您的用例:

此镜头中的两个文本图层都有不透明的白色背景,但右侧的图层将混合模式设置为“正片叠底”。

使用“乘法”:

  • 上层的白色像素将下层乘以 1,保持不变
  • 上层的黑色像素将下层乘以 0,将它们减少为黑色
  • 中间的阴影按比例使下层变暗

换句话说,与将文本直接放在背景上的结果相同。

我没有对这个屏幕截图中的文本进行亚像素抗锯齿处理,但它同样适用于不同的 R、G 和 B 值。


我对 Mac 的 API 不太熟悉,但根据 this link,Core Animation 确实有这个功能,你可以通过写来获得:

myLayer.compositingFilter = [CIFilter filterWithName:@"CIMultiplyBlendMode"];

myLayer.compositingFilter = [CIFilter filterWithName:@"CIMultiplyCompositing"];

(我不知道 Mac 用语中“混合模式”与“合成”的关系,所以你必须同时尝试!)


编辑:我不确定您是否曾指定您的文字是黑色的。如果是白色,您可以使用设置为“屏幕”混合模式的黑白图层。

【讨论】:

  • 虽然这在理论上应该很有效,但在实践中,将 NSTextField 的合成过滤器设置为其中任何一个值只会阻止它绘制。有点奇怪……
【解决方案3】:

作为一个简单的技巧,如果文本抗锯齿正在工作(但不是亚像素),您可以通过渲染到 3 倍宽的视图来伪造它,然后缩小。这是不可移植的,因为我知道无法查询显示器上的元素顺序,但它应该可以工作。

例如,

RGB RGB RGB -> RGB
|    |    |    |||  
|    |    +----++^
|    +---------+^
+--------------^

【讨论】:

  • 听起来很有希望。关于如何最好地实现这一点的提示? (仅供参考,我还没有对此表示支持;它有点笨拙,我想先看看是否有任何类型的系统支持的解决方案;不过,你确实应得的。)
  • 我不确定我将如何实现它,但如果你可以将核心动画渲染到任意 OpenGL 上下文中,我会这样做,将其写入像素缓冲区,然后将其用作输入一个着色器。
  • 好吧,我实现它的速度非常快,虽然它在技术上正确地绘制了文本,但在普通人眼中看起来并不正确。 (供参考:pastie.org/2296218 - 在某种程度上,随着规模的增加,它看起来更好,但性能受到很大影响。)
  • 确保只在 X 方向上放大。然后,当你缩小时,你不能使用-drawInRect,因为它不会做亚像素的把戏。你必须自己做。
  • 奇怪,这似乎不起作用。代替[transform scaleBy:SCALE][transform scaleXBy:SCALE yBy:1.0] 不绘制文本。
【解决方案4】:

如果你只是想在不透明的背景上显示清晰的文本并用 CATextLayer 将你的头撞到墙上 - 放弃并使用 NSTextField 并禁用 inset 并将 linefragmentpadding 设置为 0。示例代码很快但你应该可以轻松翻译...

var testV = NSTextView()
testV.backgroundColor = NSColor(calibratedRed: 0.73, green: 0.84, blue: 0.89, alpha: 1)
testV.frame = CGRectMake(120.0, 100.0, 200.0, 30.0)
testV.string = "Hello World!"
testV.textContainerInset = NSZeroSize
testV.textContainer!.lineFragmentPadding = 0
self.addSubview(testV)

对我来说显示文本相当于:

var testL = CATextLayer()
testL.backgroundColor = NSColor(calibratedRed: 0.73, green: 0.84, blue: 0.89, alpha: 1).CGColor
testL.bounds = CGRectMake(0.0, 0.0, 200.0, 30.0)
testL.position = CGPointMake(100.0, 100.0)
testL.string = "Hello World!"
testL.fontSize = 14
testL.foregroundColor = NSColor.blackColor().CGColor
self.layer!.addSublayer(testL)

这个类显然会带来更多的开销和可能不必要的东西,比如文本选择/复制等,但是文本显示得很好。

【讨论】:

    【解决方案5】:

    我不知道这个解决方案是否适合你,但你可以使用 UIGraphicsBeginImageContext() 将文本绘制到具有透明背景的图像中吗,我很确定你可以为此设置别名,如果不是你绘制你的文本以两倍或 4 倍的大小绘制,然后在视图中绘制图像时按比例缩小。

    【讨论】:

    • 很抱歉让您失望,但此代码将进入 Mac 应用程序。因此是cocoa 标签,而不是cocoa-touch。还是)感谢你的建议。我已经尝试过这个想法,但它也不起作用。
    • 可以使用-[NSLock lockFocus];和 -[NSLock unlockFocus] 或者是你尝试过的。
    • 查看我上面包含的代码块;这就是我尝试过的。
    • 我无法从您的示例中看出,但确实做到了,将 NSImage 创建为 2x 或 4x 大小,然后以 2x 或 4x 绘制文本,然后以 1/2 或 1/4 绘制图像大小,也许可以创建一个缩小版的 theImage 以加快 drawRect 方法中的绘制速度。
    • 这就是@Dietrich 的建议。查看 cmets 的答案。
    猜你喜欢
    • 1970-01-01
    • 2011-05-31
    • 2011-07-13
    • 2015-11-23
    • 1970-01-01
    • 1970-01-01
    • 2012-12-03
    • 1970-01-01
    • 2015-05-21
    相关资源
    最近更新 更多