Freddie Tilley
Posted: 21 jan. 2016
Tags: appkit, objc

Custom image formats in your AppKit application

Supporting custom image formats in Mac OSX is easy. NSImage was designed to be extendible by using subclasses of NSImageRep to actually handle the loading and drawing of images. The best way to demonstrate how it works is by creating our own simple image format. For the actual storing and drawing of the pixel data we will use Core Graphics. For more information about Core Graphics you should check out the Quartz 2D Programming Guide

Our demo image format shall be called zorq.

The file format will be pretty simple:

  • 4 byte magic header number 0x51524F5A
  • 4 byte width
  • 4 byte height
  • data, each pixel stored as 4 byte RGBA with premultiplied alpha.

In order to support our custom format in AppKit we only need to subclass NSImageRep and implement the required methods. According to the Apple Docs the following methods should be implemented by our subclass for basic support.

  • imageUnfilteredTypes
  • canInitWithData:
  • initWithData:
  • draw

Create a new NSImageRep subclass

@interface ZorqImageRep : NSImageRep
@end

Add the following definitions in the implementation file.

#define kZorqHeaderSize 12
#define kZorqMagicHeaderNo 0x51524F5A
#define kZorqBytesPerPixel 4
#define kZorqBitsPerComponent 8

#define kZorqMaxPixelsWidth 10000
#define kZorqMaxPixelsHeight 10000

Add a backing image private property.

@interface ZorqImageRep ()

@property (assign) CGImageRef backingImage;

@end

Note that we use assign instead of strong, because managing the CGImageRef memory ourselves will prevent a lot of headaches. If you insist on letting ARC handle the Core Foundation object and on unleashing demons from the Necronomicon, it needs to be cast to an NSObject first with __attribute__((NSObject))

Return our UTI type.

+ (NSArray<NSString *> *)imageUnfilteredTypes {
    static dispatch_once_t pred;
    static NSArray *types = nil;

    dispatch_once(&pred, ^{
        types = @[@"public.zorq-image"];
    });

    return types;
}

Even though imageUnfilteredFileTypes has been deprecated since OS X v10.10, [NSImage imageNamed:] still uses it internally to build a list of supported file types. Without it, our zorq images would never be found.

+ (NSArray<NSString *> *)imageUnfilteredFileTypes {
    static dispatch_once_t pred;
    static NSArray *fileTypes = nil;

    dispatch_once(&pred, ^{
        fileTypes = @[@"zorq"];
    });

    return fileTypes;
}

Check to see if we can create the image rep with the supplied data.

+ (BOOL)canInitWithData:(NSData *)data
{
    uint32_t zorqType;
    BOOL validData = NO;

    if (data != nil && data.length >= kZorqHeaderSize) {
        [data getBytes: &zorqType range: NSMakeRange(0,4)];

        if (zorqType == kZorqMagicHeaderNo) {
            validData = YES;
        }
    }

    return validData;
}

The code first checks to see if the length of the data is at least the length equal to the zorq header size. Next, we read the first 4 bytes from the data and check to see if it matches our magic header number.

Implement the imageRepWithData: method.

+ (nullable instancetype)imageRepWithData:(NSData*)data {
    if ([[self class] canInitWithData: data]) {
        return [[[self class] alloc] initWithData: data];
    } else {
        return nil;
    }
}

Creating the image with [NSImage imageNamed:] will call imageRepWithData: internally. However, it will not use the canInitWithData: method to check if the supplied image data is valid. So let's check the data with canInitWithData: just in case.

Parse the image data in the initWithData: method.

- (nullable instancetype)initWithData:(NSData *)data
{
    self = [self init];

    if (self != nil)
    {
        uint32_t width;
        uint32_t height;

        [data getBytes: &width range:NSMakeRange(4,4)];
        [data getBytes: &height range:NSMakeRange(8,4)];

        if ((width > 0 && width <= kZorqMaxPixelsWidth) &&
            (height > 0 && height <= kZorqMaxPixelsHeight))
        {

First we get the width and the height in pixels and make sure that the values are in a valid range

            self.alpha = YES;
            self.pixelsHigh = height;
            self.pixelsWide = width;
            self.bitsPerSample = 8;
            self.size = NSMakeSize(self.pixelsWide, self.pixelsHigh);

Next we set some default NSImageRep properties. This will give NSImage a bit of info about the image representation.

            size_t imageBytes = self.pixelsWide * self.pixelsHigh * kZorqBytesPerPixel;
            void *imageData = malloc(imageBytes);

            if (imageData != NULL)
            {
                memset(imageData, 0, imageBytes);

The above allocates a buffer large enough to hold the pixel data and if it succeeds (and it should, or else you have a problem) zero its contents.

                NSRange dataRange = NSMakeRange(kZorqHeaderSize, data.length - kZorqHeaderSize);

                if (dataRange.length > imageBytes) {
                    dataRange.length = imageBytes;
                }

                [data getBytes: imageData range: dataRange];

Here we set the range of bytes of our pixel data, make sure its not larger than the number of bytes we allocated in the buffer and then fill the buffer with our pixel data.

                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

                if (colorSpace != NULL)
                {
                    uint32_t bytesPerRow = width * kZorqBytesPerPixel;

                    CGContextRef context = CGBitmapContextCreate(imageData, width,
                            height, self.bitsPerSample,
                            bytesPerRow, colorSpace,
                            kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

                    if (context != NULL) {
                        _backingImage = CGBitmapContextCreateImage(context);
                        CGContextRelease(context);
                    }

                    CGColorSpaceRelease(colorSpace);
                }

                free(imageData);
            }
        }
    }

Create a bitmap context with the pixel data and a device RGB color space. Note that the kCGImageAlphaPremultipliedLast option means that the pixel data format is a premultiplied alpha with the channels in RGBA order. The kCGBitmapByteOrder32Big flag indicates that the pixel data is stored in big endian format. On success use the context to create the backing image

    if (_backingImage == NULL) {
        NSLog(@"Unable to create Zorq image");
        return nil;
    } else {
        return self;
    }
}

Return the IMZorqImageRep instance if the _backingImage was created successfully.

Our drawInRect: implementation

- (BOOL)drawInRect:(NSRect)rect
{
    CGContextDrawImage([[NSGraphicsContext currentContext] graphicsPort],
                        NSRectToCGRect(rect), _backingImage);

    return YES;
}

[[NSGraphicsContext currentContext] graphicsPort] returns its underlying CGContextRef

The draw method just calls drawInRect with the size of the representation

- (BOOL)draw {
    NSRect drawRect = NSMakeRect(0.0f, 0.0f, self.size.width, self.size.height);
    return [self drawInRect: drawRect];
}

Because our backing image does not use ARC, do not forget to release it when this IMZorqImageRep instance gets deallocated.

- (void)dealloc {
    CGImageRelease(_backingImage);
}

Now that we've created our custom image rep, how do we actually tell NSImage to use it? That is the easy part! We register the class in NSImageRep by calling +registerImageRepClass:

But the question is where do we load it? That depends.

If the custom image type will only be created via code, then the applicationDidFinishLaunching: delegate method should be sufficient. If however, you would like to refer to a zorq image from a nib/storyboard, then it needs to be called a little earlier.

A simple solution could be to register the class in the +initialize method of an NSApplication subclass and just make sure the NSPrincipalClass property in the applications Info.plist file points to it.

In our example we'll register the class in its own +load method. The method gets called when the class is loaded. It guarantees that when the method is called, the application frameworks will be loaded as well as its superclass. Since our class is not dependant on other non-system classes, using this method should be safe.

+ (void)load {
    [NSImageRep registerImageRepClass: [self class]];
}

Once the class is registered, loading our image is as simple as:

NSImage *zorqImage = [NSImage imageNamed: @"testImage"];

Sample code including a command line tool for converting images into the Zorq format can be found on my Github Page

See you next time!

For comments, questions or remarks you can mail me at freddie@impending.nl