How to save a NSImage as a new file

save as PNG using swift3

import AppKit

extension NSImage {
    @discardableResult
    func saveAsPNG(url: URL) -> Bool {
        guard let tiffData = self.tiffRepresentation else {
            print("failed to get tiffRepresentation. url: \(url)")
            return false
        }
        let imageRep = NSBitmapImageRep(data: tiffData)
        guard let imageData = imageRep?.representation(using: .PNG, properties: [:]) else {
            print("failed to get PNG representation. url: \(url)")
            return false
        }
        do {
            try imageData.write(to: url)
            return true
        } catch {
            print("failed to write to disk. url: \(url)")
            return false
        }
    }
}

Do something like this:

NSBitmapImageRep *imgRep = [[image representations] objectAtIndex: 0];
NSData *data = [imgRep representationUsingType: NSPNGFileType properties: nil];
[data writeToFile: @"/path/to/file.png" atomically: NO];

Not sure about the rest of you, but I prefer eating my full enchilada. What is described above will work and nothing is wrong with it, but I found a few things left out. Here I will highlight my observations:

  • First the best representation is provided which seems to be 72 DPI even if the image resolution is greater than that. So you are losing resolution
  • Second what about multi-page images such as those in animated GIFs or PDFs. Go ahead, try an animated GIF, you will find the animation lost
  • Lastly any metadata such as EXIF, GPS, etc...data will be lost.

So if you want to convert that image, do you really want to lose out on all of this? If you wish to eat your full meal, then lets read on...

Sometimes and I do mean sometimes there is nothing better than good ole old school development. Yeah that means we have to do a bit of work!

Let's get started:

I create a category in NSData. These are class methods because you want these things to be thread safe and there is nothing safer than putting your stuff on the stack. There are two types of methods, one for the output of non-multi-page images and one for the output of multi-page images.

List of single images: JPG, PNG, BMP, JPEG-2000

List of multiple images: PDF, GIF, TIFF

First create a mutable data space in memory.

NSMutableData * imageData    = [NSMutableData data];

Second get a CGImageSourceRef. Yeah sounding ugly already isn't it. It's not that bad, let's keep going... You really want the source image not a representation or NSImage chunk of data. However, we do have a small issue. The source might not be compatible, so make sure you check your UTI against those listed from CGImageSourceCopyTypeIdentifiers()

Some code:

CGImageSourceRef imageSource = nil;
if ( /* CHECK YOUR UTI HERE */ )
    return CGImageSourceCreateWithURL( (CFURLRef)aURL, nil );

NSImage * anImage = [[NSImage alloc] initWithContentsOfURL:aURL];

if ( anImage )
    return CGImageSourceCreateWithData( (CFDataRef)[anImage TIFFRepresentation], nil );

Wait a sec, why is NSImage there? Well there are some formats that don't have metadata and CGImageSource does not support, but these are valid images. An example is old style PICT images.

Now we have a CGImageSourceRef, make sure it's not nil and then let's now get a CGImageDestinationRef. Wow all these ref's to keep track of. So far we are at 2!

We will use this function: CGImageDestinationCreateWithData()

  • 1st Param is your imageData (cast CFMutableDataRef)
  • 2nd Param is your output UTI, remember the list above. (e.g. kUTTypePNG)
  • 3rd Param is the count of images to save. For single image files this is 1 otherwise you can simply use the following:

    CGImageSourceGetCount( imageSource );

  • 4th Param is nil.

Check to see if you have this CGImageDestinationRef and now let's add our images from the source into it...this will also include any/all metadata and retain the resolution.

For multiple images we loop:

for ( NSUInteger i = 0; i < count; ++i )
                CGImageDestinationAddImageFromSource( imageDest, imageSource, i, nil );

For single image it's one line of code at index 0:

CGImageDestinationAddImageFromSource( imageDest, imageSource, 0, nil);

Ok, finalize it which writes it to disk or data container:

CGImageDestinationFinalize( imageDest );

So that Mutable Data from the beginning has all of our image data and metadata in it now.

Are we done yet? Almost, even with garbage collection you have to clean-up! Remember two Ref's one for the source and one for the destination so do a CFRelease()

Now we are done and what you end up with is a converted image retaining all of its metadata, resolution, etc...

My NSData category methods look like this:

+ (NSData *) JPGDataFromURL:(NSURL *)aURL;
+ (NSData *) PNGDataFromURL:(NSURL *)aURL;
+ (NSData *) BMPDataFromURL:(NSURL *)aURL;
+ (NSData *) JPG2DataFromURL:(NSURL *)aURL;

+ (NSData *) PDFDataFromURL:(NSURL *)aURL;
+ (NSData *) GIFDataFromURL:(NSURL *)aURL;
+ (NSData *) TIFFDataFromURL:(NSURL *)aURL;

What about resizing or ICO / ICNS? This is for another day, but in summary you first tackle resizing...

  1. Create a context with new size: CGBitmapContextCreate()
  2. Get the an image ref from index: CGImageSourceCreateImageAtIndex()
  3. Get a copy of the metadata: CGImageSourceCopyPropertiesAtIndex()
  4. Draw the image into the context: CGContextDrawImage()
  5. Get the resized image from the context: CGBitmapContextCreateImage()
  6. Now add the image and metadata to the Dest Ref: CGImageDestinationAddImage()

Rinse and repeat for multiple-images embedded in the source.

The only difference between an ICO and ICNS is that one is a single image while the other one is multiple-images in one file. Bet you can guess which is which?! ;-) For these formats you have to resize down to a particular size otherwise ERROR will ensue. The process though is exactly the same where you use the proper UTI, but the resizing is a bit more strict.

Ok hope this helps others out there and you are as full as I am now!

Opps, forgot to mention. When you get the NSData object do as you want with it such as writeToFile, writeToURL, or heck create another NSImage if you want.

Happy coding!


You could add a category to NSImage like this

@interface NSImage(saveAsJpegWithName)
- (void) saveAsJpegWithName:(NSString*) fileName;
@end

@implementation NSImage(saveAsJpegWithName)

- (void) saveAsJpegWithName:(NSString*) fileName
{
    // Cache the reduced image
    NSData *imageData = [self TIFFRepresentation];
    NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData];
    NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:1.0] forKey:NSImageCompressionFactor];
    imageData = [imageRep representationUsingType:NSJPEGFileType properties:imageProps];
    [imageData writeToFile:fileName atomically:NO];        
}

@end

The call to "TIFFRepresentation" is essential otherwise you may not get a valid image.