CocoaDev

Edit AllPages


I try to reduce the number of strings i render (in a flipped view) by storing them in an NSImage. However, the text is always drawn up-side-down whatever isFlipped or transforms i throw at it. I’m sure it is something trivial, Anyone?

// check if we already have an image for this label
NSImage *labelImage = [planet labelForKey:label];

// nope, make one
if (labelImage == nil) {
    // calculate size
    NSSize labelSize = [label sizeWithAttributes:normalStateAttribute];
    labelImage = [[[NSImage alloc] initWithSize:labelSize] autorelease];
    NSRect labelRect;
    labelRect.size = labelSize;
    labelRect.origin = NSZeroPoint;
    
    // setup the image
    [labelImage setCachedSeparately:YES]; // we go for speed vs mem
    [labelImage setScalesWhenResized: YES];
    [labelImage setDataRetained:YES];
    
    // the actual drawing      
    [labelImage lockFocus];
    [[NSColor whiteColor] set]; 
    [label drawInRect:labelRect withAttributes:normalStateAttribute];  
    [labelImage unlockFocus];        
    
    // and store it
    [planet setLabel:labelImage forKey:label];

} 

NSRect destRect;
destRect.origin = planetViewBounds.origin;
destRect.origin.y += planetViewBounds.size.height;
int labelWidth =  [labelImage size].width;
destRect.origin.x += (planetViewBounds.size.width - labelWidth) / 2;
destRect.size = [labelImage size];

NSRect sourceRect;
sourceRect.origin = NSZeroPoint;
sourceRect.size = [labelImage size];

// actual draw
[labelImage setFlipped:YES];
[labelImage drawInRect:destRect fromRect:sourceRect operation:NSCompositeSourceOver fraction:1.0];

Thanks

Chris


Notice that using regular NSImage’s for text-caching purposes will remove all anti-aliasing (clear type) from letters. I think that -(NSBitmapImageRep *)[NSView bitmapImageRepForCachingDisplayInRect:(NSRect)aRect] will help, but I haven’t test it yet.

What about flipping… try to draw your image using -(void)[NSImage compositeToPoint:(NSPoint)aPoint operation:(NSCompositingOperation)op] or dissolveSomething…: methods. They often helps me to solve same issues.

– DenisGryzlov


I’ll check out the bitmapImageRepForCachingDisplayInRect, but this not not in a view but in a helper dat draws part of the view. As for compositeToPoint is similar to drawInRect it just does not respect the image canvas size and is left for backwards compatibilty. I found a number of possible solutions with google, which seem to point to manually flipping the entire imagerep byte by byte. yuck

Chris


You can flip the text drawing rather than flipping the image. This should also be more efficient, since the cache will exactly match the drawing.

[labelImage lockFocus]; if ([self isFlipped]) { NSAffineTransform *flipTransform = [NSAffineTransform transform]; [flipTransform translateXBy:0.0 yBy:labelRect.size.height]; [flipTransform scaleXBy:1.0 yBy:-1.0]; [flipTransform concat]; } [[NSColor whiteColor] set]; [label drawInRect:labelRect withAttributes:normalStateAttribute];
[labelImage unlockFocus];

I can kind of explain what went wrong too, though it still seems like a Cocoa bug to me. First, setDataRetained: does nothing in this case. setDataRetained tells an image to keep its original image data around, rather than a cached image rep. When an image is generated by locking focus on the image and drawing, it only ever has a cached image rep, so there’s no original data to keep around. Further, a cached image rep is already supposed to have all the information about the context in which it will be used. That is, if you have a bitmap image, call setFlipped:YES and draw it, then the generated cached image rep is already flipped. It shouldn’t be flipped again when the image is drawn again, even if flipped == YES at draw time. What can happen is that changing the flippedness of the image can invalidate the cache, so the image has to go back to source data next time its drawn. Buuuut, again, there’s no source in your case, just a cache.

I think the code above should be fine for your case, but an alternative is to draw the text into an NSBitmapImageRep, set the image up to use the bitmap image rep as source. I expect this to be as fast as the above, but behaves a bit more as expected wrt isFlipped. Here’s the code for that:

    NSBitmapImageRep *stringBitmap = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
                                                                              pixelsWide:labelSize.width 
                                                                              pixelsHigh:labelSize.height 
                                                                           bitsPerSample:8 
                                                                         samplesPerPixel:4 
                                                                                hasAlpha:YES 
                                                                                isPlanar:NO 
                                                                          colorSpaceName:NSCalibratedRGBColorSpace 
                                                                            bitmapFormat:0 
                                                                             bytesPerRow:(4 * labelSize.width) 
                                                                            bitsPerPixel:32] autorelease];
    
    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:[NSGraphicsContext 
                    graphicsContextWithBitmapImageRep:stringBitmap]];
    NSRect labelBounds = NSMakeRect(0,0,labelSize.width,labelSize.height);
    [[NSColor clearColor] set];
    NSRectFill(labelBounds);
    [label drawInRect:labelBounds withAttributes:normalStateAttribute];  
    [NSGraphicsContext restoreGraphicsState];
    
    labelImage = [[NSImage alloc] initWithSize:labelSize];
    [labelImage addRepresentation:stringBitmap];

An image created in this way will respect setFlipped:.


A third option is to flip the receiving context via transforms rather than relying on -[NSImage setFlipped:].


Also, as Denis mentions, subpixel rendering cannot be used when caching text into a bitmap if the image has a transparent background. The color of the background contributes to the subpixel calculations, so it’s necessarily not going to work.


I’ve made a test-project just to find a way how to draw a text into a flipped view (NSTableView + NSCell subclass). Here it is: http://www.shockwidgets.com/fliptest.zip

It seems that the solutions is to draw text into image using drawInRect:…, and drawing cached image to a flipped view using compositeToPoint:…. Such combo will give proper results, but again… text will lose anti-aliasing and will look very bad. However, NSTableView class uses private NSImageCacheView class to draw column-row dragging and live resize stuff. And as you can see, all anti-aliasing is preserved in it, so solution for this issue can be done using this class.

P.S. The best way to speed-up string-drawing is to use NSLayoutManager/NSTextStorage/NSTextContainer combination in your subclass. For more information, follow here: FastTextRenderingInNSView

– DenisGryzlov


As Chris, you really don’t want to get into using the composite methods. They ignore the context. They’re old, confusing, and are only in for compatibility.

Text cached this way doesn’t lose antialiasing (grey edges), by the way, it loses subpixel rendering. http://en.wikipedia.org/wiki/Subpixel_rendering

As far as solutions.. I gave three above.


All these methods compositeToPoint, dissolveToPoint, drawInRect, drawAtPoint were introduced in Mac OS X 10.0 and none of them have deprecated status in Tiger. What did you mean when saying “old, confusing and only for compatibility”? They are only unsuitable if you need rotate/scale your view. By the way, does it mean that when using resolution independance UI scaling compositeToPoint method will result in wrong drawing?

As for anti-aliasing, that’s my mistake. The proper name for this technology in Apple’s world is font smoothing (as in CGContextSetShouldSmoothFonts).

– DenisGryzlov


Hi,

thanks for the example App, it not only shows what works, but also how it works. It will help me greatly! As for compositeToPoint i simply quoted Cocoa Programming by Scott Anguish “These methods exist primairly for backward compatibilty with olde code. Versions of Cocoa prior to Max OS X had the limitation that they couldn’t composite images in a way that would respect the current coordinate system. The compositing destination point would respect the coordinate system, but the image would not. To keep old code from needing to be completely rewritten, these methods have been retained” - To our advantage too, i might humbly add.

Thanks again

Chris


Hi,

It seems to me like this method compositeToPoint: is a bit dodgy for NSImage itself : http://uselessthingies.wordpress.com/2008/05/12/flipped-composited-images/ regarding the current coordinate system… If my NSView is flipped (isFlipped returns YES), compositing an NSImage at a point P and drawing an NSRect at the same point will result in the rectangle being drawn right and the image not. But then what method should be used to composite an NSImage on an NSView ? I am missing something here ?

– CyrilChapellier


Just use drawAtPoint: or drawInRect:. You can flip the image (using -[NSImage setFlipped:]) if it’s not oriented correctly.