Search code examples
cocoacore-graphicsnsdatansimage

how can I save an NSImageView layer to a PNG on disk?


I'm trying to do the following batch processing:

  1. load an image
  2. scale it
  3. add a rounded border to it
  4. save the new image as PNG

So far, I came up with this:

CGPoint center = CGPointMake(self.window.frame.size.width / 2., self.window.frame.size.height / 2.);
      [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
      NSImage * newThumbnail = [[NSImage alloc] initWithData:[NSData dataWithContentsOfURL:[files objectAtIndex:0]]];
      NSImageView *imageView = [[NSImageView alloc] initWithFrame:CGRectMake(center.x - (57/2.), center.y - (80/2.), 57, 80)];
      [imageView setWantsLayer:YES];

      imageView.layer.borderColor = [[NSColor blackColor] CGColor];
      imageView.layer.cornerRadius = 4;
      imageView.layer.masksToBounds = YES;
      imageView.layer.borderWidth = 1.0;
      imageView.image = newThumbnail;

And now I think I'm missing telling the layer to redender to some context? maybe something like [imageView.layer drawInContext: some context (NS o CG?)]; and saving that context to disk. But I'm confused as to:

what context,

whether to go from context to NSData to disk,

or if I need an intermediate image somewhere before saving.

Basically, between CALayers and CGLayers and NS objects and UI objects (I know I'm in OS X Cocoa here and it's no UIKit and I'm also not using CG stuff so far) I have no idea how to turn the above into a png.


Solution

  • You shouldn't be using a view at all, since you're not needing to display to screen. You should be using an offscreen bitmap (an NSBitmapImageRep) and using drawing commands to draw the image.

    You have two choices with drawing in Cocoa. The easiest way is to use the various Cocoa drawing classes such as NSBezierPath and friends. The other option is the more powerful but also more low-level and complex Quartz APIs, which are not object-oriented but use a C function syntax.

    Here is an example of how to do what you want using Cocoa drawing:

    NSImage* anImage = [NSImage imageNamed:@"Lenna.tiff"]; //or some other source
    
    //create a bitmap at a specific size
    NSRect offscreenRect = NSMakeRect(0.0, 0.0, 250.0, 250.0);
    NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
                                                           pixelsWide:offscreenRect.size.width
                                                           pixelsHigh:offscreenRect.size.height
                                                        bitsPerSample:8
                                                      samplesPerPixel:4
                                                             hasAlpha:YES
                                                             isPlanar:NO
                                                       colorSpaceName:NSCalibratedRGBColorSpace
                                                         bitmapFormat:0
                                                          bytesPerRow:(4 * offscreenRect.size.width)
                                                         bitsPerPixel:32];
    
    //save the current graphics context and lock focus on the bitmap
    NSGraphicsContext* originalContext = [NSGraphicsContext currentContext];
    [NSGraphicsContext setCurrentContext:[NSGraphicsContext
                                          graphicsContextWithBitmapImageRep:bitmap]];
    [NSGraphicsContext saveGraphicsState];
    
    //clear the image rep. This is faster than filling with [NSColor clearColor].
    unsigned char *bitmapData = [bitmap bitmapData];
    if (bitmapData)
        bzero(bitmapData, [bitmap bytesPerRow] * [bitmap pixelsHigh]);
    
    //create the border path
    CGFloat borderWidth = 2.0;
    CGFloat cornerRadius = 14.0;
    NSRect borderRect = NSInsetRect(offscreenRect, borderWidth/2.0, borderWidth/2.0);
    NSBezierPath* border = [NSBezierPath bezierPathWithRoundedRect:borderRect xRadius:cornerRadius yRadius:cornerRadius];
    [border setLineWidth:borderWidth];
    
    //set the border as a clipping path
    [NSGraphicsContext saveGraphicsState];
    [border addClip];
    
    //scale and draw the image
    [anImage setSize:offscreenRect.size];
    [anImage drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
    [NSGraphicsContext restoreGraphicsState];
    
    //set the border color
    [[NSColor blackColor] set];
    
    //draw the border
    [border stroke];
    
    //restore the original graphics context
    [NSGraphicsContext restoreGraphicsState];
    [NSGraphicsContext setCurrentContext:originalContext];
    
    //get PNG data from the image rep
    NSData* pngData = [bitmap representationUsingType:NSPNGFileType properties:nil];
    NSError* error;
    if(![pngData writeToURL:[NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:@"test.png"]] options:NSDataWritingAtomic error:&error])
    {
        NSLog(@"%@",error);
    }