Johan Sørensen

Diffing images with Core Graphics

Few of the popular version control systems treat images as much more than the binary blobs they technically are. So it’s up to third party tools such as Github or Kaleidoscope to provide more reasonable and user-friendly means of comparing images.

On a whim, let’s explore how to implement one of the ways the above tools provides image comparison with Core Graphics; difference comparison. Conceptually it means we’ll blend the new image onto the old image and subtract any pixels that are different from the other and pixels that are unchanged will be black. It’s the same blending mode you’ll find in image editors such as Photoshop and Pixelmator.

Doing it with Core Graphics is pretty easy: draw the old image as-is into a drawing context, change the blendmode and draw the new image and extract the composed image.

First we need a method for creating a CGContextRef from a CGImage. I like boilerplate things like this to stay out of the main method and live in its own private method:

- (CGContextRef)createCGContextFromCGImage:(CGImageRef)img
{
    size_t width = CGImageGetWidth(img);
    size_t height = CGImageGetHeight(img);
    size_t bitsPerComponent = CGImageGetBitsPerComponent(img);
    size_t bytesPerRow = CGImageGetBytesPerRow(img);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef ctx = CGBitmapContextCreate(NULL, // Let CG allocate it for us
                                             width,
                                             height,
                                             bitsPerComponent,
                                             bytesPerRow,
                                             colorSpace,
                                             kCGImageAlphaPremultipliedLast); // RGBA
    CGColorSpaceRelease(colorSpace);
    NSAssert(ctx, @"CGContext creation fail");

    return ctx;
}

In other words, pretty much standard Core Graphics creation of a CGContext. In this example we just extract the size and pixel format from the supplied CGImageRef. As per normal Cocoa memory semantics we return an object which the caller is responsible for releasing since our method starts with create.

Now for the actual blending of the two images, represented as the _oldImage and _newImage UIImage’s:

- (UIImage *)diffedImage
{
    // We assume both images are the same size, but it's just a matter of finding the biggest
    // CGRect that contains both image sizes and create the CGContext with that size
    CGRect imageRect = CGRectMake(0, 0,
                                  CGImageGetWidth(_oldImage.CGImage),
                                  CGImageGetHeight(_oldImage.CGImage));
    // Create our context based on the old image
    CGContextRef ctx = [self createCGContextFromCGImage:_oldImage.CGImage];

    // Draw the old image with the default (normal) blendmode 
    CGContextDrawImage(ctx, imageRect, _oldImage.CGImage);
    // Change the blendmode for the remaining drawing operations
    CGContextSetBlendMode(ctx, kCGBlendModeDifference);
    // Draw the new image "on top" of the old one
    CGContextDrawImage(ctx, imageRect, _newImage.CGImage);

    // Grab the composed CGImage
    CGImageRef diffed = CGBitmapContextCreateImage(ctx);
    // Cleanup and return a UIImage
    CGContextRelease(ctx);
    UIImage *diffedImage = [UIImage imageWithCGImage:diffed];
    CGImageRelease(diffed);

    return diffedImage;
}

Core Graphics will do the hard work of subtracting the pixels, even if the two images are of different formats or sizes, however the example here doesn’t accommodate different sizes to keep things brief.

I always enjoy working with Apple’s C-based APIs, they’re often well designed both when it comes to naming and functionality, while feeling strangely familiar due to their memory management semantics.