Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to work around poor text rendering in text backed by a CALayer

I have some variable text in an NSTextField that renders on a CALayer background view. As a CALayer does not support sub-pixel aliasing for text rendering of any text on top of it, this text looks rubbish.

A bit of googling reveals the reasons why this is, and that text must be rendered onto an opaque background to have SPA enabled. Rendering onto an opaque background is something I'd like to avoid if at all possible in this case. Is there a better workaround?

I am completely amenable to rendering the text myself into an NSImage, if that will help, but I can't find any confirmed reports that it will.

It looks absolutely fine in Interface Builder so I know the secret is somewhere inside this computer just straining to get out.

like image 515
Nick Locking Avatar asked Nov 27 '10 19:11

Nick Locking


1 Answers

Workaround found. Nothing in Quartz can render text with Sub-Pixel Aliasing on top of a transparent background, seemingly. However, you CAN render text to an offscreen bitmap buffer, providing that offscreen bitmap buffer has been created in the correct fashion. The background of this buffer must be opaque.

My view previously had a PNG background that was slightly transparent. I could have simply made this background opaque and rendered to it without problem, but as this view needs to fade in and out, it needed to be CALayer-backed, so the text renders once properly, and then subsequently renders without Sub-Pixel Aliasing.

Here's my code. It seems incredibly verbose, I'd love it if anyone could help me whittle it down. It assumes you have an NSImageView called _title and an NSString called title.

// Create the attributed string
NSMutableAttributedString *attStr = [[[NSMutableAttributedString alloc] initWithString: title] autorelease];

NSRange strRange = NSMakeRange(0, [attStr length]);
NSFont *font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize: NSSmallControlSize]];

[attStr addAttribute: NSForegroundColorAttributeName value: [NSColor whiteColor] range: strRange];
[attStr addAttribute: NSFontAttributeName value: font range: strRange];

NSMutableParagraphStyle *paraStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[paraStyle setAlignment: NSCenterTextAlignment];
[paraStyle setLineBreakMode: NSLineBreakByTruncatingMiddle];
[attStr addAttribute: NSParagraphStyleAttributeName value: paraStyle range: strRange];

// Set up the image context
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * [_title frame].size.width;
NSUInteger bitsPerComponent = 8;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// These constants are necessary to enable sub-pixel aliasing.
CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;

// Create the memory buffer to be used as the context's workspace
unsigned char *contextBuffer = malloc([_title frame].size.height * [_title frame].size.width * 4);

CGContextRef context = CGBitmapContextCreate(contextBuffer, 
                                             [_title frame].size.width, 
                                             [_title frame].size.height, 
                                             bitsPerComponent, 
                                             bytesPerRow, 
                                             colorSpace, 
                                             bitmapInfo);


[NSGraphicsContext saveGraphicsState];

[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO]];


CGContextSetFillColorWithColor(context, CGColorGetConstantColor(kCGColorBlack));
CGRect rectangle = CGRectMake(0, 0, [_title frame].size.width,[_title frame].size.height);
CGContextAddRect(context, rectangle);
CGContextFillPath(context);

CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attStr);

CGContextSetTextPosition(context, 10.0, 10.0);
CTLineDraw(line, context);
CFRelease(line);

// Create a data provider from the context buffer in memory
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, contextBuffer, bytesPerRow * [_title frame].size.height, NULL);

// Create an image from the data provider
CGImageRef imageRef = CGImageCreate ([_title frame].size.width,
                                     [_title frame].size.height,
                                     bitsPerComponent,
                                     bytesPerPixel * 8,
                                     bytesPerRow,
                                     colorSpace,
                                     bitmapInfo,
                                     dataProvider,
                                     NULL,
                                     false,
                                     kCGRenderingIntentDefault
                                     );

// Turn it into an NSImage
NSImage *newImage = [[[NSImage alloc] initWithCGImage:imageRef size: NSZeroSize] autorelease];

CGDataProviderRelease(dataProvider);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CGImageRelease(imageRef);

free(contextBuffer);

[NSGraphicsContext restoreGraphicsState];

[_title setImage: newImage];
like image 61
Nick Locking Avatar answered Oct 13 '22 17:10

Nick Locking