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](http://iPhoneDeveloperTips.com/cocoa/how-to-mask-an-image.html), 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](http://pastie.org/418627) 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.
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.
Ugh – that really messed up my code. Let’s try again:
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]; }
Forget it… stupid comment system… :P
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!
Hey Jason =)
Yep, that’s right – the simulator’s busted! I have the same issue – I should’ve updated this page to mention it.