Scaling images and creating thumbnails from UIViews

In this blog post I would like to show how you can do some common image-related tasks, like scaling an UIImage object or creating a thumbnail image from a UIView. I’m implementing these features as category for the UIImage class, so these can be used with all UIImage objects without the need to subclass the UIImage class.

Scaling UIImages

Scaling images is important for example when you want to create thumbnail images which are used as an overview for real documents or larger pictures. The standard UIImage class doesn’t provide a method to scale images, but scaling an image isn’t very complicated. You can do it with just 4 lines of code…

UIImage *originalImage = ...;
CGSize destinationSize = ...;

UIGraphicsBeginImageContext(destinationSize);
[originalImage drawInRect:CGRectMake(0,0,destinationSize.width,destinationSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

At first we have to create and start an image context with the size of the resulting image. All drawing operations which take place after the UIGraphicsBeginImageContext() call will be done in the newly created image context. This means, we are no longer drawing on the screen. All we need to do now is to draw the original image in the size of the final image. The drawing takes place in the newly created image context. Afterwards we can use UIGraphicsGetImageFromCurrentImageContext() to get an image from the image context. Finally we need to call UIGraphicsEndImageContext() to make sure that the system will release the image context again, because we don’t need it anymore. And of course the drawing will take place on the screen again after calling this function.

Creating an UIImage from a UIView

If you need an image from the content of a UIView object (like a whole web page that is displayed in a UIWebView object) or even a complex hierarchy of UIView objects so you can save it as PNG or JPEG image or use it as thumbnail image, you can do this also with just a few lines of code…

UIView *view = ...;

CGSize size = [view bounds].size;
UIGraphicsBeginImageContext(size);
[[view layer] renderInContext:UIGraphicsGetCurrentContext()];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

The code is very similar to the code from above to scale the image. But instead of drawing an image into the new image context, we use the “renderInContext:” method to draw the content of the view (which includes its subviews) into the image context. The resulting image will have the same size as the original UIView.

Retina Display

With the new iPhone 4 and the 4th generation iPod Touch, the iOS does also support high resolution screens. But the larger screen resolution is not used to display more content, instead it is used to display a sharper image with more details. Basically the iOS uses the same coordinate system on all screens with a size of 480×320 points. The only difference of the retinal displays is that there’re additional pixels between these points. Most drawing functions of the iOS automatically use these additional pixels to draw sharper and more detailed content. But there are exceptions when the iOS can not create more details itself. For example for bitmap images (UIImage objects), there’s no way to automatically generate a more detailed image for the retina displays. So iOS 4.0 introduces a new “scale” property for images. This property can be 1.0 (the default value) which is used for standard resolution and 2.0 for the more detailed retina resolution with twice as much details. If the “scale” property has a value of 1.0, one pixel of the image will be drawn on 2*2 (= 4 pixels) on the retina display, if the “scale” property has a value of 2, one image pixel is drawn on one screen pixel.

The above code will work with all iOS releases (including iOS 3.x which is the latest version that can be used on the first generation iPhone and iPod Touch devices), but it will always use the default “scale” property of 1.0 for images. Which means the code can not improve the image quality on the new retina displays. In order to get more detailed images on retina displays, we need to use “UIGraphicsBeginImageContextWithOptions()” instead of “UIGraphicsBeginImageContext()”, so we can define the “scale” property for the new image context as well. But “UIGraphicsBeginImageContextWithOptions()” is only available since iOS 4, so we have to check if we can use this function. If it is not available or if the device does not have a retina display, we still need to use the default scaling of 1.0 …

if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
     if ([[UIScreen mainScreen] scale] == 2.0) {
        UIGraphicsBeginImageContextWithOptions(size, YES, 2.0);
     } else {
        UIGraphicsBeginImageContext(size);
    }
} else {
    UIGraphicsBeginImageContext(size);
}

Combining everything into a Objective-C Category

Now it’s time to combine all of the above and create a category for the UIImage class. I’m implementing three simple methods to create a new UIImage from a UIView or UIImage object.

  • imageWithImage:scaledToSize:” creates an image from a UIImage and scales it to a given size
  • imageFromView:” creates an image from a UIView
  • imageFromView:” creates an image from a UIView and scales it to a given size.

MyImage.h:

@interface UIImage (MyImage)
+ (UIImage*)imageFromView:(UIView*)view;
+ (UIImage*)imageFromView:(UIView*)view scaledToSize:(CGSize)newSize;
+ (UIImage*)imageWithImage:(UIImage*)image scaledToSize:(CGSize)newSize;
@end

MyImage.m:

#import "MyImage.h"

@implementation UIImage (MyImage)

+ (void)beginImageContextWithSize:(CGSize)size
{
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        if ([[UIScreen mainScreen] scale] == 2.0) {
            UIGraphicsBeginImageContextWithOptions(size, YES, 2.0);
        } else {
            UIGraphicsBeginImageContext(size);
        }
    } else {
        UIGraphicsBeginImageContext(size);
    }
}

+ (void)endImageContext
{
    UIGraphicsEndImageContext();
}

+ (UIImage*)imageFromView:(UIView*)view
{
    [self beginImageContextWithSize:[view bounds].size];
    BOOL hidden = [view isHidden];
    [view setHidden:NO];
    [[view layer] renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    [view setHidden:hidden];
    return image;
}

+ (UIImage*)imageFromView:(UIView*)view scaledToSize:(CGSize)newSize
{
    UIImage *image = [self imageFromView:view];
    if ([view bounds].size.width != newSize.width ||
            [view bounds].size.height != newSize.height) {
        image = [self imageWithImage:image scaledToSize:newSize];
    }
    return image;
}

+ (UIImage*)imageWithImage:(UIImage*)image scaledToSize:(CGSize)newSize
{
    [self beginImageContextWithSize:newSize];
    [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    return newImage;
}
@end

Optimizations

Scaling images can temporarily require lots of additional memory, depending of the size of the images. The original image is first drawn in an image context. This image context must provide a bitmap buffer internally in which the drawing takes place. This buffer needs some memory. Also when the final image is created from this image context, some more memory is required. Only afterwards it is possible to free up the memory again by releasing the image context and maybe the original image.

For retina displays, the bitmap buffers for images where the “scale” property has a value of 2.0 are four times larger as for non-retina displays (scale = 1.0). So a single full screen image requires approximately 2.5 MB of memory. And because multiple bitmap buffers are required while scaling, the memory requirements can be much higher than without retina display.

This is not a big deal for the iPhone 4 which has much more main memory (512 MB) than all other devices. But for the 4th generation iPod Touch, the increased memory usage for the image buffers for the retina display is a problem. The 4th generation iPod Touch only has 256 MB of main memory, so it’s almost always running on its memory limits.

So to reduce the memory usage when creating a small thumbnail image from a UIView (which means you’re only interested in a scaled-down(!) image from a UIView), you should first create an image from the UIView with a “scale” property of 1.0. This will reduce the memory usage for the newly created image from the view. And because you’re about to scale down this image, it doesn’t matter that it doesn’t have all the details it would have with scale=2.0. When you then scale down this image using a “scale” property of 2.0, the final (smaller) image can still can get all the details it needs from the image you’ve created before.

If you have to create many images form large UIViews, you may consider to try to reduce the memory requirements this way (especially for iPod Touch with its limited amount of RAM). If you only need to deal with a few smaller views, you probably don’t need to care about this.