Loading an image mask from a file

July 23rd, 2010 § 1 comment § permalink

illustration of an image being masked
Core Graphics image masks are handy, but if you want to load the mask image from a file, things don’t always work the way you expect.

The function CGImageCreateWithMask() can take either a mask or an image as the second parameter, but it turns out that Core Graphics (at least on iOS) is pretty picky about what is an acceptable image for the mask.

I’ve seen this snippet of code suggested in a few places:

CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(image),
CGImageGetHeight(image), CGImageGetBitsPerComponent(image),
CGImageGetBitsPerPixel(image), CGImageGetBytesPerRow(image),
CGImageGetDataProvider(image), NULL, false);

the idea being that you create a mask with the pixels that are in the loaded image, but it turns out that this code is not 100% reliable either.

The truth of the matter is that CGImage is an incredibly versatile object. The bits that represent the image can be in a variety of formats, bit depths, and colour space. When you load an image from a file, you are not guaranteed what format those bits are going to be in—for example, there are reports online of how people can get image masks to work if they save it in one way from an image editing program, but not if they save it a different way (e.g. http://stackoverflow.com/questions/1133248/any-idea-why-this-image-masking-code-does-not-work )

Thus, I’ve found that the best and most reliable way to generate an image mask from an arbitrary image is to do this:

  1. Create a bitmap graphics context that is in an acceptable format for image masks
  2. Draw your image into this bitmap graphics context
  3. Create the image mask from the bits of the bitmap graphics context.

The following function has worked well for me so far:

CGImageRef createMaskWithImage(CGImageRef image)
{
    int maskWidth               = CGImageGetWidth(image);
    int maskHeight              = CGImageGetHeight(image);
    //  round bytesPerRow to the nearest 16 bytes, for performance's sake
    int bytesPerRow             = (maskWidth + 15) & 0xfffffff0;
    int bufferSize              = bytesPerRow * maskHeight;
 
    //  we use CFData instead of malloc(), because the memory has to stick around
    //  for the lifetime of the mask. if we used malloc(), we'd have to
    //  tell the CGDataProvider how to dispose of the memory when done. using
    //  CFData is just easier and cleaner.
 
    CFMutableDataRef dataBuffer = CFDataCreateMutable(kCFAllocatorDefault, 0);
    CFDataSetLength(dataBuffer, bufferSize);
 
    //  the data will be 8 bits per pixel, no alpha
    CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceGray();
    CGContextRef ctx            = CGBitmapContextCreate(CFDataGetMutableBytePtr(dataBuffer),
                                                        maskWidth, maskHeight,
                                                        8, bytesPerRow, colourSpace, kCGImageAlphaNone);
    //  drawing into this context will draw into the dataBuffer.
    CGContextDrawImage(ctx, CGRectMake(0, 0, maskWidth, maskHeight), image);
    CGContextRelease(ctx);
 
    //  now make a mask from the data.
    CGDataProviderRef dataProvider  = CGDataProviderCreateWithCFData(dataBuffer);
    CGImageRef mask                 = CGImageMaskCreate(maskWidth, maskHeight, 8, 8, bytesPerRow,
                                                        dataProvider, NULL, FALSE);
 
    CGDataProviderRelease(dataProvider);
    CGColorSpaceRelease(colourSpace);
    CFRelease(dataBuffer);
 
    return mask;
}

Example of use:

UIImage *maskSource = [UIImage imageNamed:@"mask.png"];
CGImageRef mask = createMaskWithImage(maskSource.CGImage);

Then use the mask as you wish, for example in the aforementioned CGImageCreateWithMask() or CGContextClipToMask()

And don’t forget to dispose of the mask when you’re done. createMaskWithImage() returns the mask with a retain count of 1, and expects the caller to take ownership.

CGImageRelease(mask);

iPhone VR: Viewing 3D panoramas in Safari using JavaScript and -webkit-transform

October 13th, 2008 § 4 comments § permalink

Apple’s 2.0 iPhone update brought some interesting enhancements to Mobile Safari, including 3-D perspective and access to multitouch events. Armed with the documentation (of varying quality) from Apple’s Web Apps Dev Centre, and invaluable information from blog articles such as Idean’s “Spin the Bottle”, SitePen’s “Touching and Gesturing on the iPhone” and Paul Bakaus’s “3D CSS Transforms on the iPhone”, I put together a simple web app that displays a cubic panorama in Mobile Safari. Look left, right, up and down by dragging your finger across the display. No zoom, currently, though. It was pretty straightforward, but I did learn a few things while creating it.

Go ahead, take a look :D Visit http://tinyurl.com/iphonevr on your iPhone or iPod Touch.

The images used for the panorama are taken from one of the QuickTime VR movies in Apple’s Cubic VR gallery. I extracted the images from the movie using the procedure explained here (in French, but it’s easy to follow).

I may write a full tutorial on this eventually, but for now I will just share a few notes (and the source code):

  • It’s a lot easier to get the 3-d transforms right if the elements you want to transform start out centred in their containing block (so that they start at (0, 0, 0), essentially). I did this with regular CSS, but I suppose this could also be done as part of the transform.
  • The above also applies (especially) to any DIV that contains nothing but other DIVs. At first my cube was spinning around its top edge, instead of its centre, because its children (the cube faces) were absolute-positioned, and thus the “cube” DIV itself had no height! Giving it an explicit width and height solved this.
  • Touch events on 3-D transformed elements seemed to be little bit unreliable. I worked around this by putting a normal transparent DIV on top of the whole thing and listened to events on that instead.

Download the source code (MIT Licence).