Resize/Scale of an Image – Take 2 – Coding a Thread Safe Approach

Mon, Jan 25

In the first post on image resizing, How to Resize/Scale an Image using an Objective-C Category, I wrote barebones approach to resizing an image. This works well for simple cases, however this approach is not thread safe as it uses the global current context.

I attended a recent Apple Tech Talk and one of the more interesting discussions was on how to create code to dynamically resize images, using an approach that is thread safe. An experienced developer at the event was willing to share an excellent code example that I’ll walk through in this post. The code is a fair amount more complex than the first version I wrote, however, with the complexity comes flexibility.

Building the User Interface

The project to demonstrate thread-safe image resizing is quite simple, essentially a table and a slider, with the later controlling the size of the images in the table. The image below should give you and idea of how things work:

It all starts by building a UITableView and setting the imageView property to one of the images in the application bundle. The code to build (or reload) the table looks as follows:

1
2
3
4
5
6
7
8
9
10
11
-(UITableViewCell *) tableView:(UITableView *)tableView 
   cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell = 
	(UITableViewCell*) [tableView dequeueReusableCellWithIdentifier:kMyCellID];
  ...
 
  cell.imageView.image = [self requestImageForIndex:row];
 
  return cell;
}

The relevant line of code is 8, notice the call to requestImageForIndex, the code for this method is shown below. There is a fair amount going on here, first an ImageStateObject is created, this keeps track of the path of the image, a flag indicating if the image has been loaded and a flag indicating if a (resize) operation is in progress. There is also code for managing a queue of operations -to keep focuses on the task at hand, skip down to LINE 20, where we call UImageFromPathScaledToSize passing in the image state object and the preferred size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-(UIImage*) requestImageForIndex:(NSUInteger)index
{
  ImageStateObject *iso	= [self imageObjectForIndex:index];
  CGSize  theSz = [self preferredImageSize];
 
  if( mUseOperations )
  {
    if( iso.hasImage == NO && iso.queuedOp == NO )	// if we dont have an image and there is no operation pending
    {	
      // Queue up an operation to do the work!
      MyLoadScaleOperation *op = [[MyLoadScaleOperation alloc] initWithPath:iso.path index:index targetSize:theSz];
      op.resultsDelegate = self;		// set the delegate
      [mQueue addOperation:op];
      [op release];
 
      iso.queuedOp = YES;
    }		
    return mPlaceHolderImage;		// just return our placeholder
  }	
  return UImageFromPathScaledToSize( iso.path, theSz );
}

Note that when calling the method on line 20, notice the path to the image is passed in as the first parameter.

ImageStateobject

For completeness, here is the definition of the image state object:

@interface ImageStateObject : NSObject
{
  NSString *path;
  BOOL  hasImage;
  BOOL  queuedOp;
}

The code for working with the queued operations (queuedOp) we will look into in a future post.

UImageFromPathScaledToSize

Given a path to an image and a specified size, create a UIImage object and get the scale value based on the current image size and the size to scale to, and finish by creating a new UIImage object from the original image, scaled as requested.

UIImage *UImageFromPathScaledToSize(NSString* path, CGSize toSize)
{
  UIImage *scaledImg = nil;
  UIImage *img = [[UIImage alloc] initWithContentsOfFile:path];	// get the image
 
  if( img )
  {
    float scale = GetScaleForProportionalResize( img.size, toSize, false, false );
 
    CGImageRef cgImage	= CreateCGImageFromUIImageScaled( img, scale );
 
    [img release];
 
    if( cgImage )
    {
      scaledImg = [UIImage imageWithCGImage:cgImage];	// autoreleased
      CGImageRelease( cgImage );
    }
  }
  return scaledImg;	// autoreleased
}
Get Scale Percentage and Create New Image Reference

Below is the code to determine the scale value as a percentage. For example, if the incoming image size (theSize) is 266 x 401 and the destination size (intoSize) is 225 x 225, the returned scale value is .561 (225/401). That is, we want to scale the image to 56% of its current size.

float GetScaleForProportionalResize( CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize )
{
  float sx = theSize.width;
  float sy = theSize.height;
  float dx = intoSize.width;
  float dy = intoSize.height;
  float scale = 1;
 
  if( sx != 0 && sy != 0 )
  {
    dx = dx / sx;
    dy	 = dy / sy;
 
    // if maximize is true, take LARGER of the scales, else smaller
    if( maximize )
      scale = (dx > dy)	? dx : dy;
    else			
      scale = (dx < dy)	? dx : dy;
 
    if( scale > 1 && onlyScaleDown )	// reset scale
      scale = 1;
  }
  else
  {
    scale = 0;
  }
  return scale;
}

The last step is to create a CGImageRef which will hold bitmap information for the new, scaled image. This reference will then be used to create our final scaled UIImage object.

CGContextRef CreateCGBitmapContextForWidthAndHeight( unsigned int width, unsigned int height, 
    CGColorSpaceRef optionalColorSpace, CGBitmapInfo optionalInfo )
{
  CGColorSpaceRef colorSpace = (optionalColorSpace == NULL) ? GetDeviceRGBColorSpace() : optionalColorSpace;
  CGBitmapInfo alphaInfo	= ( (int32_t)optionalInfo < 0 ) ? kDefaultCGBitmapInfo : optionalInfo;
  return CGBitmapContextCreate( NULL, width, height, 8, 0, colorSpace, alphaInfo );
}
 
CGImageRef CreateCGImageFromUIImageScaled( UIImage* image, float scaleFactor )
{
  CGImageRef newImage = NULL;
  CGContextRef bmContext = NULL;
  BOOL  mustTransform = YES;
  CGAffineTransform  transform = CGAffineTransformIdentity;
  UIImageOrientation orientation = image.imageOrientation;
 
  CGImageRef srcCGImage = CGImageRetain( image.CGImage );
 
  size_t width = CGImageGetWidth(srcCGImage) * scaleFactor;
  size_t height = CGImageGetHeight(srcCGImage) * scaleFactor;
 
  // These Orientations are rotated 0 or 180 degrees, so they retain the width/height of the image
  if ( (orientation == UIImageOrientationUp) || (orientation == UIImageOrientationDown) || (orientation == UIImageOrientationUpMirrored) || (orientation == UIImageOrientationDownMirrored)  )
  {	
    bmContext	= CreateCGBitmapContextForWidthAndHeight( width, height, NULL, kDefaultCGBitmapInfo );
  }
  else	// The other Orientations are rotated ±90 degrees, so they swap width & height.
  {	
    bmContext	= CreateCGBitmapContextForWidthAndHeight( height, width, NULL, kDefaultCGBitmapInfo );
  }
 
  CGContextSetBlendMode( bmContext, kCGBlendModeCopy );	// we just want to copy the data
 
  switch(orientation)
  {
    case UIImageOrientationDown:		// 0th row is at the bottom, and 0th column is on the right - Rotate 180 degrees
    transform = CGAffineTransformMake(-1.0, 0.0, 0.0, -1.0, width, height);
    break;
 
    case UIImageOrientationLeft:		// 0th row is on the left, and 0th column is the bottom - Rotate -90 degrees
    transform = CGAffineTransformMake(0.0, 1.0, -1.0, 0.0, height, 0.0);
    break;
 
    case UIImageOrientationRight:		// 0th row is on the right, and 0th column is the top - Rotate 90 degrees
    transform = CGAffineTransformMake(0.0, -1.0, 1.0, 0.0, 0.0, width);
    break;
 
    case UIImageOrientationUpMirrored:	// 0th row is at the top, and 0th column is on the right - Flip Horizontal
    transform = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, width, 0.0);
    break;
 
    case UIImageOrientationDownMirrored:	// 0th row is at the bottom, and 0th column is on the left - Flip Vertical
    transform = CGAffineTransformMake(1.0, 0.0, 0, -1.0, 0.0, height);
    break;
 
    case UIImageOrientationLeftMirrored:	// 0th row is on the left, and 0th column is the top - Rotate -90 degrees and Flip Vertical
    transform = CGAffineTransformMake(0.0, -1.0, -1.0, 0.0, height, width);
    break;
 
    case UIImageOrientationRightMirrored:	// 0th row is on the right, and 0th column is the bottom - Rotate 90 degrees and Flip Vertical
    transform = CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0);
    break;
 
    default:
    mustTransform = NO;
    break;
  }
 
  if ( mustTransform )	
    CGContextConcatCTM( bmContext, transform );
 
  CGContextDrawImage( bmContext, CGRectMake(0.0, 0.0, width, height), srcCGImage );
  CGImageRelease( srcCGImage );
  newImage = CGBitmapContextCreateImage( bmContext );
  CFRelease( bmContext );
 
  return newImage;
}
Project Source Code

You can download the entire project source code here. You’ll find the image processing code shown above is in the source file ImageHelpers.m.

There are many other worthwhile code snippets to look at in this project, including code to create and display gradients (for the table background) as well as working with NSOperation and NSOperationQueue to place resize requests into a queue for processing.

7 comments

Thank you: that is very nice, helpful code.
I would suggest though: whenever possible(e.g. when you load the images from your own service/server) you should load them in the final size. The iphone(at least 2g&3g) are pretty slow doing image resize.
Actually it is slower then requesting thumbnails from a server even on edge.

Sebastian

by Sebastian on Jan 25, 2010. #

John,

Thanks so much! for sharing this code… I have been trying to figure out how to resize images without crashing the iphone for a few weeks now. I will give this code a try later tonight.

Thanks… love the site! Keep up the great work.

-=Ryan
http://www.modelrailcast.com
http://www.iphonedevcast.com

by Ryan on Mar 4, 2010. #

Superb! Thanks so much for this. I was struggling to maintain performance while generating thumbnails on the main thread.

by Neil on Apr 12, 2010. #

Hey, I’m having a problem with this, and maybe you can help. I’m trying to resize a normal PNG image, but it gives me this error

: CGBitmapContextCreate: unsupported parameter combination: 8 integer bits/component; 24 bits/pixel; 3-component color space; kCGImageAlphaNone; 576 bytes/row.

by tadej5553 on Sep 5, 2010. #

2 years, and you really save me tonight. Thanks alot!

by binhnx on Jan 18, 2012. #

Awesome…..
Thank you.. :)

by Roshit on Feb 23, 2012. #

Wow, thanks. Will try it, no doubt it is what I was looking for! :D

by Andrey on Apr 28, 2012. #