BuySellAds.com

Until Dec 3rd, 44% off all Manning books, including Barcodes with iOS! Promo code: mobicftw
Our DNA is written in Objective-C
Jump

Drawing on UIImages

Jayesh asks:

Thanks for your article on UIImage from UIView.

Need one more favor; I am in situation where I want to draw line or area (e.g. rectangle) on UI Image (e.g. Blue print) and save it again.

Basically I will show image and put circle / rectangle on image showing area in blue print and save it.

How should I do that? Can you please suggest approach, sample codes etc?

So the task is to take a UIImage, make it writable in some way and then make a new image out of that for later use. Off the top of my head, I can immediately think of two ways to do that: with UIKit and with CoreGraphics. CoreGraphics has a slight advantage, being lower level, of being thread-safe. But for a simple graphical addition to an existing image I see nothing wrong with UIKit. As usual you should only do very quick operations with UIKit because it requires to be run on the main thread which is the only thread updating the user interface.

So, first, let’s create a method that adds a circle to a passed UIImage.

- (UIImage *)imageByDrawingCircleOnImage:(UIImage *)image
{
	// begin a graphics context of sufficient size
	UIGraphicsBeginImageContext(image.size);
 
	// draw original image into the context
	[image drawAtPoint:CGPointZero];
 
	// get the context for CoreGraphics
	CGContextRef ctx = UIGraphicsGetCurrentContext();
 
	// set stroking color and draw circle
	[[UIColor redColor] setStroke];
 
	// make circle rect 5 px from border
	CGRect circleRect = CGRectMake(0, 0,
				image.size.width,
				image.size.height);
	circleRect = CGRectInset(circleRect, 5, 5);
 
	// draw circle
	CGContextStrokeEllipseInRect(ctx, circleRect);
 
	// make image out of bitmap context
	UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();
 
	// free the context
	UIGraphicsEndImageContext();
 
	return retImage;
}

This code is only using UIKit methods, except for the lonely CGContextStrokeEllipseInRect. This is the reason why we need to retrieve a handle for the current context. UIKit has it’s own reference so that all UI* methods work without specifically setting a context. Should you ever need to force a specific context that you had created elsewhere, then you can use UIGraphicsPushContext to put this other context “topmost” and UIGraphicsPopContext to clean up.

To use the above mentioned function all you need is this:

// load image from bundle
UIImage *image = [UIImage imageNamed:@"user-1.png"];
 
// call circle drawing
UIImage *imageWithCircle = [self imageByDrawingCircleOnImage:image];
 
// save it to documents
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
							NSUserDomainMask, YES) lastObject];
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"output.png"];
NSData *imageData = UIImagePNGRepresentation(imageWithCircle);
[imageData writeToFile:filePath atomically:YES];
 
NSLog(@"Saved new image to %@", filePath);

So, that’s the UIKit variant. Sometimes though you need to draw on a background thread and thus have to be sure to use thread-safe methods.

The iOS 4 release notes mention that drawing images and fonts is thread-safe as of SDK 4.0.

Drawing to a graphics context in UIKit is now thread-safe. Specifically:

  • The routines used to access and manipulate the graphics context can now correctly handle contexts residing on different threads.
  • String and image drawing is now thread-safe.

Using color and font objects in multiple threads is now safe to do.

So the above sample should be fine for most purposes. Calling it on a background thread would work on 4.0 and above, but cause weird error messages on earlier SDK versions. One example might be if you needed to draw contents of a CATiledLayer, which is occuring on a special background thread.

So how would this example look in CoreGraphics and without UIKit functions?

First we would require a method to create a bitmap context of sufficient size. I took an example I found somewhere and modified it to my needs. I actually discovered that you don’t have to allocate the storage for the bitmap context yourself, it works fine as of 3.2, even though the documentation states that you should not do that before 4.0. Well, it works, I’ll stick with it.

CGContextRef newBitmapContextSuitableForSize(CGSize size)
{
	int pixelsWide = size.width;
	int pixelsHigh = size.height;
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
   // void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;
 
    bitmapBytesPerRow   = (pixelsWide * 4); //4
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);
 
   /* bitmapData = malloc( bitmapByteCount );
 
	memset(bitmapData, 0, bitmapByteCount);  // set memory to black, alpha 0
 
    if (bitmapData == NULL)
    {
        return NULL;
    }
*/
	colorSpace = CGColorSpaceCreateDeviceRGB();
 
	context = CGBitmapContextCreate ( NULL, // instead of bitmapData
					pixelsWide,
					pixelsHigh,
					8,      // bits per component
					bitmapBytesPerRow,
					colorSpace,
					kCGImageAlphaPremultipliedFirst);
	CGColorSpaceRelease( colorSpace );
 
    if (context== NULL)
    {
       // free (bitmapData);
        return NULL;
    }
 
    return context;
}

Note that if you would put the malloc back in then you also need to free the memory pointer after you’re done. Previously I did not know that so all this memory leaked. This approach of passing NULL instead of a pointer apparently works on 3.2 and above, officially from 4.0.

- (UIImage *)imageByDrawingCircleOnImageCG:(UIImage *)image
{
	// begin a graphics context of sufficient size
	CGContextRef ctx = newBitmapContextSuitableForSize(image.size);
 
	// need to flip the transform matrix
	// CoreGraphics has (0,0) in lower left
	CGContextScaleCTM(ctx, 1, -1);
	CGContextTranslateCTM(ctx, 0, -image.size.height);
 
	// draw original image into the context
	CGRect imageRect = CGRectMake(0, 0, image.size.width, image.size.height);
	CGContextDrawImage(ctx, imageRect, image.CGImage);
 
	// set stroking color and draw circle
	CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
 
	// make circle rect 5 px from border
	CGRect circleRect = CGRectMake(0, 0,
			image.size.width,
			image.size.height);
	circleRect = CGRectInset(circleRect, 5, 5);
 
	// draw circle
	CGContextStrokeEllipseInRect(ctx, circleRect);
 
	// make image out of bitmap context
	CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
	UIImage *retImage = [UIImage imageWithCGImage:cgImage];
	CGImageRelease(cgImage);
 
	// free the context
	CGContextRelease(ctx);
 
	return retImage;
}

Besides of having to use this helper function to create a suitable bitmap context you should also note that we have to go via creating a CGImage first. From this we can then create a UIImage. Also you might have spotted that we need to turn the transformation matrix upside down because otherwise all our graphics would be that. CoreGraphics generally assumes the origin of drawing in the lower left hand corner.

I’ve shown you both methods of drawing on images, now you can take your pick which one you prefer.


Categories: Q&A

%d bloggers like this: