Search code examples
iosuikitcore-graphicsuibarbuttonitem

How to convert UIImage/CGImageRef's alpha channel to mask?


How can I extract the alpha channel of a UIImage or CGImageRef and convert it into a mask that I can use with CGImageMaskCreate?

For example:

example image

Essentially, given any image, I don't care about the colors inside the image. All I want is to create a grayscale image that represents the alpha channel. This image can then be used to mask other images.

An example behavior of this is in the UIBarButtonItem when you supply it an icon image. According to the Apple docs it states:

The images displayed on the bar are derived from this image. If this image is too large to fit on the bar, it is scaled to fit. Typically, the size of a toolbar and navigation bar image is 20 x 20 points. The alpha values in the source image are used to create the images—opaque values are ignored.

The UIBarButtonItem takes any image and looks only at the alpha, not the colors of the image.


Solution

  • To color icons the way the bar button items do, you don't want the traditional mask, you want the inverse of a mask-- one where the opaque pixels in the original image take on your final coloring, rather than the other way around.

    Here's one way to accomplish this. Take your original RBGA image, and process it by:

    • Drawing it into an alpha-only one-channel bitmap image
    • Invert the alpha values of each pixel to get the opposite behavior as noted above
    • Turn this inverted-alpha image into an actual mask
    • Use it.

    E.g.

    #define ROUND_UP(N, S) ((((N) + (S) - 1) / (S)) * (S))
    
    // Original RGBA image
    CGImageRef originalMaskImage = [[UIImage imageNamed:@"masktest.png"] CGImage];
    float width = CGImageGetWidth(originalMaskImage);
    float height = CGImageGetHeight(originalMaskImage);
    
    // Make a bitmap context that's only 1 alpha channel
    // WARNING: the bytes per row probably needs to be a multiple of 4 
    int strideLength = ROUND_UP(width * 1, 4);
    unsigned char * alphaData = calloc(strideLength * height, sizeof(unsigned char));
    CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
                                                          width, 
                                                          height,
                                                          8, 
                                                          strideLength, 
                                                          NULL, 
                                                          kCGImageAlphaOnly);
    
    // Draw the RGBA image into the alpha-only context.
    CGContextDrawImage(alphaOnlyContext, CGRectMake(0, 0, width, height), originalMaskImage);
    
    // Walk the pixels and invert the alpha value. This lets you colorize the opaque shapes in the original image.
    // If you want to do a traditional mask (where the opaque values block) just get rid of these loops.
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            unsigned char val = alphaData[y*strideLength + x];
            val = 255 - val;
            alphaData[y*strideLength + x] = val;
        }
    }
    
    CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
    CGContextRelease(alphaOnlyContext);
    free(alphaData);
    
    // Make a mask
    CGImageRef finalMaskImage = CGImageMaskCreate(CGImageGetWidth(alphaMaskImage),
                                                  CGImageGetHeight(alphaMaskImage),
                                                  CGImageGetBitsPerComponent(alphaMaskImage),
                                                  CGImageGetBitsPerPixel(alphaMaskImage),
                                                  CGImageGetBytesPerRow(alphaMaskImage),
                                                  CGImageGetDataProvider(alphaMaskImage), NULL, false);
    CGImageRelease(alphaMaskImage);
    

    Now you can use finalMaskImage as the mask in CGContextClipToMask etc, or etc.