views:

780

answers:

2

I am using an NSView to host several Core Animation CALayer objects. What I want to be able to do is grab a snapshot of the view's current state as a bitmap image.

This is relatively simple with a normal NSView using something like this:

void ClearBitmapImageRep(NSBitmapImageRep* bitmap) {
    unsigned char* bitmapData = [bitmap bitmapData];
    if (bitmapData != NULL)
        bzero(bitmapData, [bitmap bytesPerRow] * [bitmap pixelsHigh]);
}

@implementation NSView (Additions)
- (NSBitmapImageRep*)bitmapImageRepInRect:(NSRect)rect
{
    NSBitmapImageRep* imageRep = [self bitmapImageRepForCachingDisplayInRect:rect];
    ClearBitmapImageRep(imageRep);
    [self cacheDisplayInRect:rect toBitmapImageRep:imageRep];
    return imageRep;
}
@end

However, when I use this code, the Core Animation layers are not rendered.

I have investigated CARenderer, as it appears to do what I need, however I cannot get it to render my existing layer tree. I tried the following:

NSOpenGLPixelFormatAttribute att[] = 
{
    NSOpenGLPFAWindow,
    NSOpenGLPFADoubleBuffer,
    NSOpenGLPFAColorSize, 24,
    NSOpenGLPFAAlphaSize, 8,
    NSOpenGLPFADepthSize, 24,
    NSOpenGLPFANoRecovery,
    NSOpenGLPFAAccelerated,
    0
};

NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:att];
NSOpenGLView* openGLView = [[NSOpenGLView alloc] initWithFrame:[self frame] pixelFormat:pixelFormat];
NSOpenGLContext* oglctx = [openGLView openGLContext];

CARenderer* renderer = [CARenderer rendererWithCGLContext:[oglctx CGLContextObj] options:nil];
renderer.layer = myContentLayer;
[renderer render];
NSBitmapImageRep* bitmap = [oglView bitmapImageRepInRect:[oglView bounds]];

However, when I do this I get an exception:

CAContextInvalidLayer -- layer <CALayer: 0x1092ea0> is already attached to a context

I'm guessing that this must be because the layer tree is hosted in my NSView and therefore attached to its context. I don't understand how I can detach the layer tree from the NSView in order to render it to a bitmap, and it's non-trivial in this case to create a duplicate layer tree.

Is there some other way to get the CALayers to render to a bitmap? I can't find any sample code anywhere for doing this, in fact I can't find any sample code for CARenderer at all.

+4  A: 

There is a great post on "Cocoa is my girlfriend" about recording Core Animations. The author captures the whole animation into a movie, but you could use the part where he grabs a single frame.
Jump to the "Obtaining the Current Frame" section in this article:
http://www.cimgf.com/2009/02/03/record-your-core-animation-animation/

The basic idea is:

  • Create a CGContext
  • Use CALayer's renderInContext:
  • Create a NSBitmapImageRep from the context (using CGBitmapContextCreateImage and NSBitmapImageRep's initWithCGImage)

Update:
I just read, that the renderInContext: method does not support all kind of layers in Mac OS X 10.5. It does not work for the following layers classes:

  • QCCompositionLayer
  • CAOpenGLLayer
  • QTMovieLayer
weichsel
Many thanks, this is exactly what I needed and works perfectly. I wish I'd noticed the `-renderInContext:` method earlier, `CARenderer` led me up the garden path.
Rob Keniger
+1  A: 

If you want sample code for how to render a CALayer hierarchy to an NSImage (or UIImage for the iPhone), you can look at the Core Plot framework's CPLayer and its -imageOfLayer method. We actually created a rendering pathway that is independent of the normal -renderInContext: process used by CALayer, because the normal way does not preserve vector elements when generating PDF representations of layers. That's why you'll see the -recursivelyRenderInContext: method in this code.

However, this won't help you if you are trying to capture from any of the layer types mentioned by weichsel (QCCompositionLayer, CAOpenGLLayer, or QTMovieLayer).

Brad Larson
Thanks for this, it is much more comprehensive than the answer from weichsel but it's more than I actually need in this case. The `-recursivelyRenderInContext:` code is very nice indeed.
Rob Keniger