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.
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:
// 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];
}
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.