31

For anyone working on a project with Core Animation layer-backed views, it's unfortunately obvious that subpixel antialiasing (text smoothing) is disabled for text not rendered on a pre-set opaque background. Now, for people who are able to set opaque backgrounds for their text (either with a setBackgroundColor: call or the equivalent setting in Interface Builder), this issue doesn't present too much of a problem. For others, though, who have absolutely no way to work around it, it's not something one can ignore.

I've been looking online for well over two days for a solution, and nothing usable has come up. All I want to do is create a text label (not user-editable) that is set on a sheet window (sheet window backgrounds are transparent, so having the sheet's content view declare wantsLayer as true disables text smoothing for all labels on the window); something very simple. I've seen a lot of contested solutions (a quick Google search will bring up this topic in many other places), but so far, all of those solutions rely on people being able and willing to compromise with an opaque background (which you cannot use on a sheet).

So far, the best direction I can imagine taking this in is pre-rendering the text with text smoothing onto an image, and then displaying the image as usual on a layer-backed view. However, this doesn't seem to be possible. The 'normal' way I would assume one would try is this:

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

But that doesn't seem to work (most likely because when called from drawRect:, the graphics context set up already has text smoothing disabled - subpixel antialiasing is turned off, and regular antialiasing is used instead), but neither does the more involved solution found in this question (where you create your own graphics context).

So how is doing something like this possible? Can you somehow 'fake' the effect of text smoothing? Are there even hack-ish workarounds that will get something set up? I really don't want to have to abandon Core Animation just because of this silly issue; there are a lot of benefits to it that save a lot of time and code.


Edit: After a lot of searching, I found something. Although the thread itself only reaffirms my suspicions, I think I may have found one solution to the problem: custom CALayer drawing. Timothy Wood, in one of his responses, attached a sample project that shows font smoothing using several techniques, several of which work. I'll look into integrating this into my project.

Edit #2: Turns out, the link above is a bust as well. Although the methods linked give subpixel antialiasing, they fail as well on transparent backgrounds.

Community
  • 1
  • 1
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Not sure if this is an answer, so posting as comment. Have you checked out Core Text? Here's a random Core Text example: http://pastie.org/2249243. It does drawRect level text drawing, with antialiasing and all that jazz. Honestly not sure how this blends with CALayer though. – August Lilleaas Jul 28 '11 at 11:41
  • I'm pretty sure CoreText suffers from the same problem as any other text, no? Or, at least, that's what I've been lead to believe. – Itai Ferber Jul 28 '11 at 11:50
  • I would imagine that when Core Text draws to a CALayer, it ends up as rgba pixels. I.e. it shouldn't matter if it is text or whatever, as it's rasterized. But now I'm just guessing. – August Lilleaas Jul 28 '11 at 11:54
  • 5
    That's the problem. For text smoothing, you need to draw ARAGAB pixels (i.e. each color has its own alpha value), which CALayers seemingly can't handle... – Itai Ferber Jul 28 '11 at 16:08

5 Answers5

9

If you're using a transparent sheet, you don't know in advance what the pixels below it will be. They may change. Remember that you have a single alpha channel for all three colors: if you make it transparent, you won't see any subpixel effect, but if you make it opaque, all three subelements are going to get composited with the background. If you give an edge the right color for compositing over a white background, it won't look right if the background changes to some other color.

For example, let's say you're drawing black text on a white background, and the subelement order is RGB. A right edge may be a faint blue: high B value (full brightness on the side away from the glyph), slightly lower but still high R and G values (lower brightness on the side closer to the glyph). If you now composite that pixel over a dark gray background, you're going to make it lighter than it would have been if you had rendered black text on dark gray background directly.

Basically, you are not facing an arbitrary limitation of CoreAnimation: it simply makes no sense to use subpixel rendering on a transparent layer that might be composited over an arbitrary background. You'd need a separate alpha per color channel, but since the pixel format of your buffer is RGBA (or ARGB or whatever it is), you can't have it.

But this leads us to the solution. If you know that the background will remain the same (eg, the sheet displays over a window whose contents you control), then you can simply make your layer opaque, fill it with a copy of the covered region of the background window, and render subpixel-antialiased text on it. Basically, you'd be precompositing your layer with the background. If the background stays the same, this will look identical to what normal alpha compositing would do, except that you can now do subpixel text rendering; if the background changes, then you'd have to give up on doing subpixel text rendering anyway (although I guess you could keep copying the background and redrawing your opaque overlay whenever it changes).

LaC
  • 12,624
  • 5
  • 39
  • 38
  • This seems to be the answer. After days of searching, I guess it really isn't possible to do what I'm trying to achieve. What I'll have to do is make my sheet smaller than my parent window so the background stays constant. Thanks for your answer; I'll give it another day or so just to see that nobody else has anything to say, and I'll mark this as correct and give you the bounty. – Itai Ferber Aug 03 '11 at 04:29
  • +1 for the theory, but I don't think that drawing on opaque backrounds and then copying them into the semi-transparent sheet is a good idea. This kind of hack will very likely lead to lots of problems in the future, because basic assumptions that the window manager makes are violated (eg. the contents of two windows are independent). As I see it, CALayers are a trade-off: You give up subpixel-rendering for higher performance. I don't know all the background of what you want to do, but I suggest you think about what's more important, rather than trying dirty hacks to try having both. – Jakob Egger Aug 05 '11 at 09:56
  • Thanks for the accept, Itai. The bounty is about to expire, btw. – LaC Aug 06 '11 at 13:36
2

As a simple hack, if text anti-aliasing is working (but not subpixel), you can fake it by rendering to a view that is 3x as wide, then scaling down. This is nonportable as I know of no way to query the element order on your display, but it should work.

E.g.,

RGB RGB RGB -> RGB
|    |    |    |||  
|    |    +----++^
|    +---------+^
+--------------^
George WS
  • 3,903
  • 5
  • 27
  • 39
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • Sounds promising. Tips on how to best implement this? (FYI, I'm not upvoting this just yet; it is a little hacky, and I'd like to wait and see if there's any sort of system-supported solution first; you do deserve it, though.) – Itai Ferber Jul 30 '11 at 21:26
  • I'm not sure how I would implement it, but if you can render Core Animation into an arbitrary OpenGL context I would do that, make it write into a pixel buffer, then use that as input to a shader. – Dietrich Epp Jul 30 '11 at 21:39
  • Well, I implemented it very quickly, and while it draws text technically correctly, it doesn't look right to the common eye. (For reference: http://pastie.org/2296218 - to a certain point, it looks better as the scale goes up, but there's a big performance hit.) – Itai Ferber Jul 30 '11 at 21:50
  • Make sure you only scale up in the X direction. Then, when you scale down, you can't use `-drawInRect` because it won't do the subpixel trick. You have to do it yourself. – Dietrich Epp Jul 30 '11 at 22:11
  • Strange, that doesn't seem to work. Instead of `[transform scaleBy:SCALE]`, `[transform scaleXBy:SCALE yBy:1.0]` doesn't draw the text. – Itai Ferber Jul 30 '11 at 22:19
  • Hm, bizarre. It's been years since I last used Core Animation, sorry I can't offer any better suggestions. – Dietrich Epp Jul 30 '11 at 23:09
  • Why are you expecting scaling to work that way? With normal pixel downsampling, you won't get the R channel from the left pixel, the G channel from the center, and the B channel from the right, but instead, each channel in the output pixel will be the average of the corresponding channel across the three input pixels. – LaC Aug 02 '11 at 23:01
  • @LaC: I'm not expecting scaling to work that way by default. You have to write more code. – Dietrich Epp Aug 02 '11 at 23:05
  • Ah yes, I was referring to Itai's use of `drawInRect`, but I had not seen that you had already commented on that. – LaC Aug 02 '11 at 23:18
  • Of course, if you're using a normal RGBA buffer, you can't rely on ordinary compositing either, which means your algorithm needs to know the value of the background pixels you're going to composite with; but in that case, my solution is simpler. – LaC Aug 02 '11 at 23:24
2

I'm not really sure I have grasped the question, so this is a real punt.

But I noticed LaC's answer depends on the pixels being written on top of each other in the normal way. In the manner you would in Photoshop call the "Normal Blend Mode".

You can merge two layers in lots of other ways including "Multiply":

enter image description here

This individually multiplies the R, G, and B of each pixel. So if you had the text on a white background, you could get a further-back layer to show through by setting the top layer to "Multiply" which causes both layers to burn into each other like so.

This should work for your use case too:

enter image description here

Both text layers in this shot have an opaque white background, but the one on the right has the blend mode set to "Multiply".

With "Multiply":

  • white pixels in the top layer multiply lower layers by 1, leaving them unchanged
  • black pixels in the top layer multiply lower layers by 0, reducing them to black
  • shades in between darken the lower layers proportionally

In other words, the same result that you would have got just laying the text directly onto the background.

I haven't subpixel antialiased the text in this screenshot but it would work equally well with differing R, G, and B values.


I'm not remotely familiar with Mac's API, but according to this link, Core Animation does have this capability, which you get by writing:

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

or

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

(I have no idea what "Blend Mode" vs "Compositing" relates to in Mac parlance so you'll have to try both!)


EDIT: I'm not sure you ever specified your text was Black. If it's White, you can use a white-on-black layer set to a Blend Mode of "Screen".

Chris Burt-Brown
  • 2,717
  • 1
  • 16
  • 16
  • While this should work great in theory, in practice, setting the compositing filter of an NSTextField to either of those values simply prevents it from drawing. It's rather strange... – Itai Ferber Aug 05 '11 at 22:44
0

If you are simply trying to get sharp looking text to display on an opaque background and hitting your head against the wall with CATextLayer - give up and use NSTextField with the inset disabled and linefragmentpadding set to 0. Sample code is swift but you should be able to translate easily...

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)

for me displays text the equivalent to:

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)

This class obviously comes with more overhead and possibly unnecessary things like text selecting/copying etc, but hey the text displays well.

James Alvarez
  • 7,159
  • 6
  • 31
  • 46
-3

I don't know if this solution will work for you but can you draw the text into an image with transparent background using UIGraphicsBeginImageContext(), I am pretty sure you can set up aliasing for this, if not you draw your text at twice or 4 times the size and then scale down when drawing the image in you view.

Nathan Day
  • 5,981
  • 2
  • 24
  • 40
  • Sorry to disappoint, but this code is going into a Mac app. Hence the `cocoa` tag, and not `cocoa-touch`. Thanks for the suggestion, though. I've tried that idea, and it doesn't work either. – Itai Ferber Jul 30 '11 at 20:32
  • The can use -[NSLock lockFocus]; and -[NSLock unlockFocus] or is that what you tried. – Nathan Day Jul 30 '11 at 21:08
  • Check out the block of code I included above; that's what I've tried. – Itai Ferber Jul 30 '11 at 21:17
  • I can't tell from your example but did, creating the NSImage a 2x or 4x the size and then draw the text at 2x or 4x, then draw the image at 1/2 or 1/4 size, maybe create a shrinking version of theImage to speed up draw in your drawRect method. – Nathan Day Jul 30 '11 at 21:36
  • That's what @Dietrich suggested. Check out comments on his answer. – Itai Ferber Jul 30 '11 at 21:47
  • @Nathan: The question is about SUBPIXEL-antialiasing, not normal anti-aliasing. Drawing on transparent background or scaling down does not achieve subpixel antialiasing. Also, `NSLock` has nothing to do with drawing. – Jakob Egger Aug 05 '11 at 09:50