2011年10月18日星期二

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

Resize/Scale of an Image – Take 2 – Coding a Thread Safe Approach:
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.

没有评论:

发表评论