Blog

Achieve smaller app downloads by replacing large PNGs with JPEG + mask

I’m using a transparent overlay on top of a fairly common interface element to make it look awesome. I originally did this with a transparent PNG, until I realised the PNG in question for the iPhone 4’s Retina display was truly massive, clocking in at 1 Mb.

Why we don’t have common image format with both transparency and lossy compression is beyond me, but there’s a relatively easy alternative: Using a JPEG and masking it with another JPEG.

Based on Rodney Aiglstorfer’s solution on how to mask an image, I derived a category on UIImage which would apply a mask to an image. The method required a little tweaking to work with JPEG images — the CGImageCreateWithMask function won’t work correctly on source images that don’t have an alpha channel, so one has to create one first, from the original. Jean Regisser figured out the solution which he presents in a comment on the above article, but it needs one more addition: A check on line 37 for kCGImageAlphaNoneSkipLast. Update: Oh, and one more – kCGImageAlphaNoneSkipFirst

So, the complete category for applying a mask to a JPEG image, to achieve the same result as using a PNG but with less download time for your users:

// Header
 
@interface UIImage (TPAdditions)
- (UIImage*)imageByMaskingUsingImage:(UIImage *)maskImage;
@end
 
 
// Implementation
 
CGImageRef CopyImageAndAddAlphaChannel(CGImageRef sourceImage) {
    CGImageRef retVal = NULL;
 
    size_t width = CGImageGetWidth(sourceImage);
    size_t height = CGImageGetHeight(sourceImage);
 
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
 
    CGContextRef offscreenContext = CGBitmapContextCreate(NULL, width, height, 
                                                          8, 0, colorSpace, kCGImageAlphaPremultipliedFirst);
 
    if (offscreenContext != NULL) {
        CGContextDrawImage(offscreenContext, CGRectMake(0, 0, width, height), sourceImage);
 
        retVal = CGBitmapContextCreateImage(offscreenContext);
        CGContextRelease(offscreenContext);
    }
 
    CGColorSpaceRelease(colorSpace);
 
    return retVal;
}
 
 
@implementation UIImage (TPAdditions)
 
 
- (UIImage*)imageByMaskingUsingImage:(UIImage *)maskImage {
 
    CGImageRef maskRef = maskImage.CGImage; 
    CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                        CGImageGetHeight(maskRef),
                                        CGImageGetBitsPerComponent(maskRef),
                                        CGImageGetBitsPerPixel(maskRef),
                                        CGImageGetBytesPerRow(maskRef),
                                        CGImageGetDataProvider(maskRef), NULL, false);
 
    CGImageRef source = [self CGImage];
 
    NSInteger alphaInfo = CGImageGetAlphaInfo(source);
    if ( alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipLast || alphaInfo == kCGImageAlphaNoneSkipFirst ) {
        source = CopyImageAndAddAlphaChannel(source);
    }
 
    CGImageRef masked = CGImageCreateWithMask(source, mask);
    CGImageRelease(mask);
 
    if ( source != [self CGImage] ) {
        CGImageRelease(source);
    }
 
    UIImage *result;
    if ( [UIImage respondsToSelector:@selector(imageWithCGImage:scale:orientation:)] ) {
        result = [UIImage imageWithCGImage:masked scale:self.scale orientation:self.imageOrientation];
    } else {
        result = [UIImage imageWithCGImage:masked];
    }
 
    CGImageRelease(masked);
 
    return result;
}
 
@end

Note that the image mask should be another JPEG (or PNG, if you really like), without transparency, and greyscale, where black represents full opacity, and white represents full transparency.

, , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

6 Comments

  1. Jason
    Posted November 5, 2010 at 3:00 am | Permalink

    Hello. I have tried using this, but every time it gets to the image with the mask the entire image is transparent!

    I have a method that I can pass a filename to, and it loads the file as a UIImage as well as looks for a mask in the format of filename-mask.filextension:

    - (UIImage) imageFileWithMask:(NSString) fileName { // split file NSArray *tempFile = [fileName componentsSeparatedByString: @"."]; //[tempFile autorelease]; NSString *fileNameFirst = [tempFile objectAtIndex:0]; // see if a small image is available NSString *newFile; NSString *maskFile; newFile = fileNameFirst; maskFile = [[NSString alloc] initWithFormat: @"%@-mask",newFile]; UIImage *maskImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:maskFile ofType:[tempFile objectAtIndex:1]]]; UIImage *tempImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:newFile ofType:[tempFile objectAtIndex:1]]]; return [tempImage imageByMaskingUsingImage:maskImage]; }

    I have two files: file1.jpg and file1-mask.jpg (a black/white mask image).

    When I use this, though, it shows file1.jpg as completely transparent – its like it is applying the entire size of the file1-mask.jpg as a mask, regardless of what is black and white.

  2. Jason
    Posted November 5, 2010 at 3:01 am | Permalink

    Ugh – that really messed up my code. Let’s try again:

    • (UIImage) imageFileWithMask:(NSString) fileName { // split file NSArray *tempFile = [fileName componentsSeparatedByString: @”.”]; //[tempFile autorelease]; NSString *fileNameFirst = [tempFile objectAtIndex:0]; // see if a small image is available NSString *newFile; NSString *maskFile; newFile = fileNameFirst; maskFile = [[NSString alloc] initWithFormat: @”%@-mask”,newFile]; UIImage *maskImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:maskFile ofType:[tempFile objectAtIndex:1]]]; UIImage *tempImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:newFile ofType:[tempFile objectAtIndex:1]]]; return [tempImage imageByMaskingUsingImage:maskImage]; }
  3. Jason
    Posted November 5, 2010 at 3:03 am | Permalink

    Arrgh. One more try – assume a dash in front of this one.:

    (UIImage) imageFileWithMask:(NSString) fileName { // split file NSArray *tempFile = [fileName componentsSeparatedByString: @”.”]; //[tempFile autorelease]; NSString *fileNameFirst = [tempFile objectAtIndex:0]; NSString *newFile; NSString *maskFile; newFile = fileNameFirst; maskFile = [[NSString alloc] initWithFormat: @”%@-mask”,newFile]; UIImage *maskImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:maskFile ofType:[tempFile objectAtIndex:1]]]; UIImage *tempImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:newFile ofType:[tempFile objectAtIndex:1]]]; return [tempImage imageByMaskingUsingImage:maskImage]; }

  4. Jason
    Posted November 5, 2010 at 3:03 am | Permalink

    Forget it… stupid comment system… :P

  5. Jason
    Posted November 5, 2010 at 6:12 am | Permalink

    Aha – it may have been a problem with the iPad Simulator all along. I loaded it on my iPad directly and the mask seems to work fine!

    • Posted November 5, 2010 at 1:35 pm | Permalink

      Hey Jason =)

      Yep, that’s right – the simulator’s busted! I have the same issue – I should’ve updated this page to mention it.