Thursday, December 16, 2010

Offscreen drawing on the iPhone using NSOperations, CGImage, CGLayer


What?
Creating and or modifying images offscreen or in a background thread.

Why?
Chiefly for performance. The main thread of an iPhone application is usually fairly busy doing all sorts of things. By rendering complex images in the background, you can do all manor of things. In my case I wanted to know how to render an image larger than the size of the device screen, and then save that image to disk.

For my fellow lovers of NSOperationQueue(aka possibly the most awesome class in Cocoa), this allows you to bundle image processing and generation in to NSOperation subclasses or if you like the new hotness a block, and add it to the queue.

How?
Well with a lot of C. If you have ever overridden UIView's drawRect: then most of what is below should make perfect sense.

First thing is first, as we want to operate on our own thread we have to create everything ourselves. Primarily that means no quick calls to UIGraphicsGetCurrentContext().

The key thing to remember is that the drawing coordinates are inverted. This means that CGPoint(0.0f,0.0f) is actually the bottom left not the top left corner. I'm lead to believe this is a hold over from the Postscript drawing system that originated on the mac, and this is how the big boys do it. So stop whining and code.

Nicely it would appear that the UIImage representation methods automatically invert the image, so you only have to invert your coordinates for CGContext drawing calls.

Exactly How?

This is the process
*Create a CGColorSpace
*Create a CGBitmapContext
*Create a new CGLayer
*Get the the CGLayer's context

Draw into the context as you would normally, using the CGContext methods

*Render that CGLayer into the CGBitmapContext
(if you want an image)
*Create a CGImage from the CGBitmapContext
*Convert the CGImage into a UIImage
*Use one of the UIImageJPEGRepresentation or UIImagePNGRepresentation methods to get an image that can be saved to disk or sent over the wire etc.

NOTE: This code will not work unless you have a image named sample.jpg in your bundle or change the assignment to the backgroundImg variable.

Sample code
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
@interface FRImageOperation : NSOperation {
}
@end
@implementation FRImageOperation
- (id)init{
if( (self = [super init]) ){
//Do some custom initialization
}
return self;
}
- (void)dealloc {
[super dealloc];
}
- (void) main{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//////////////////////////////////////////
UIImage *backgroundImg;
CGLayerRef layer;
CGImageRef resultImage;
CGContextRef context, layerContext;
void *bitmapData;
CGColorSpaceRef colorSpace;
CGSize canvasSize;
int bitmapByteCount;
int bitmapBytesPerRow;
//Get the background image
backgroundImg = [UIImage imageNamed:@"sample.jpg"];
//Initialize the canvas size!
canvasSize = [backgroundImg size];
//
bitmapBytesPerRow = (canvasSize.width * 4);
bitmapByteCount = (bitmapBytesPerRow * canvasSize.height);
//Create the color space
colorSpace = CGColorSpaceCreateDeviceRGB();
bitmapData = malloc( bitmapByteCount );
//Check the the buffer is alloc'd
if( bitmapData == NULL ){
DebugLog(@"Buffer could not be alloc'd");
}
//Create the context
context = CGBitmapContextCreate(bitmapData, canvasSize.width, canvasSize.height, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
if( context == NULL ){
DebugLog(@"Context could not be created");
}
layer = CGLayerCreateWithContext(context, canvasSize, NULL);
if( layer == NULL ){
DebugLog(@"Layer could not be created");
}
layerContext = CGLayerGetContext(layer);
if( layerContext == NULL){
DebugLog(@"No Layer context");
}
//Draw the image into the bitmap context
CGContextDrawImage(context, CGRectMake(0.0f,0.0f,backgroundImg.size.width,backgroundImg.size.height), [backgroundImg CGImage]);
//Fill color set to green
CGContextSetRGBFillColor(layerContext,0.33f,0.66f,0.33f,1.0f );
//Draw a green square
CGContextFillRect(layerContext, CGRectMake(0.0f,0.0f,200.0f,200.0f));
//Draw the layer in the context
CGContextDrawLayerAtPoint( context, CGPointMake(0.0f, 0.0f), layer);
//Get the result image
resultImage = CGBitmapContextCreateImage(context);
//Save the image to the photos album
UIImageWriteToSavedPhotosAlbum( [UIImage imageWithCGImage:resultImage],self,@selector(image:error:context:),NULL);
//Cleanup
free(bitmapData);
CGColorSpaceRelease(colorSpace);
CGImageRelease(resultImage);
DebugLog(@"Complete Processing");
//////////////////////////////////////////
[pool drain];
}
-(void) image:(UIImage*) aImage error:(NSError*) aError context:(void *) aContext{
DebugLog(@"Completed saving");
}
@end


Go forth and code.

2 comments:

Anonymous said...

Thanks!, this really helped me.

SSL Certificates said...

Perfect just what I was looking for!