7

I am trying to create a NSImage or NSImageCell with rounded corners inside a NSTableView. I can't get anything to work. Here is the best I have so far inside my custom NSCell:

- (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView *)controlView { 
  if (thumbnailLink) {
    NSURL *url = [NSURL URLWithString:thumbnailLink];
    if (url) {
        NSRect imageFrame = [self _imageFrameForInteriorFrame:frame];
        NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
        [image setScalesWhenResized:YES];
        [image setSize:NSMakeSize(IMAGE_HEIGHT, IMAGE_WIDTH)];

        [NSGraphicsContext saveGraphicsState];
        imageFrame = NSInsetRect(imageFrame, 1, 1);
        NSBezierPath *clipPath = [NSBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:5.0];
        [clipPath setWindingRule:NSEvenOddWindingRule];
        [clipPath addClip];
        [NSGraphicsContext restoreGraphicsState];   
        [image drawInRect:imageFrame fromRect:NSMakeRect(0, 0, 0, 0) operation:NSCompositeSourceIn fraction:1.0];
        [image release];
    }
}
...

Any ideas on how to this?

Adrian
  • 6,013
  • 10
  • 47
  • 68
John Wright
  • 2,418
  • 4
  • 29
  • 34

4 Answers4

23

You need to keep the clip set when drawing your image, and restore the context after instead. Also I don't think you want to be using "in" compositing but rather "over" if you just want to draw your image normally without taking the destination opacity in consideration. Try something like:

[NSGraphicsContext saveGraphicsState];

NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:imageFrame
                                                     xRadius:5
                                                     yRadius:5];
[path addClip];

[image drawInRect:imageFrame
         fromRect:NSZeroRect
        operation:NSCompositeSourceOver
         fraction:1.0];

[NSGraphicsContext restoreGraphicsState];
Rhult
  • 1,646
  • 15
  • 10
10
+ (NSImage*)roundCorners:(NSImage *)image
{

NSImage *existingImage = image;
NSSize existingSize = [existingImage size];
NSSize newSize = NSMakeSize(existingSize.width, existingSize.height);
NSImage *composedImage = [[[NSImage alloc] initWithSize:newSize] autorelease];

[composedImage lockFocus];
[[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];

NSRect imageFrame = NSRectFromCGRect(CGRectMake(0, 0, 1024, 1024));
NSBezierPath *clipPath = [NSBezierPath bezierPathWithRoundedRect:imageFrame xRadius:200 yRadius:200];
[clipPath setWindingRule:NSEvenOddWindingRule];
[clipPath addClip];

[image drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, newSize.width, newSize.height) operation:NSCompositeSourceOver fraction:1];

[composedImage unlockFocus];

return composedImage;
}
Keith Smiley
  • 61,481
  • 12
  • 97
  • 110
Alejandro Luengo
  • 1,256
  • 1
  • 17
  • 27
  • Good work... You have width and height backwards for newSize, though. – El Tomato May 18 '13 at 03:28
  • Great answer. This post actually clips the image while the marked answer just masks the image. This mattered in my case because I had to draw a white border around a circular image. Using the marked answer method, pixels from the image bled into the border (making it look bad). This post makes the border look perfect! – rocky Oct 10 '16 at 23:40
7

Swift version:

func roundCorners(image: NSImage, width: CGFloat = 192, height: CGFloat = 192) -> NSImage {
    let xRad = width / 2
    let yRad = height / 2
    let existing = image
    let esize = existing.size
    let newSize = NSMakeSize(esize.width, esize.height)
    let composedImage = NSImage(size: newSize)

    composedImage.lockFocus()
    let ctx = NSGraphicsContext.currentContext()
    ctx?.imageInterpolation = NSImageInterpolation.High

    let imageFrame = NSRect(x: 0, y: 0, width: width, height: height)
    let clipPath = NSBezierPath(roundedRect: imageFrame, xRadius: xRad, yRadius: yRad)
    clipPath.windingRule = NSWindingRule.EvenOddWindingRule
    clipPath.addClip()

    let rect = NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height)
    image.drawAtPoint(NSZeroPoint, fromRect: rect, operation: NSCompositingOperation.CompositeSourceOver, fraction: 1)
    composedImage.unlockFocus()

    return composedImage
}

// then
roundCorners(image)
roundCorners(image, width: 512, height: 512)
Paul Miller
  • 1,960
  • 1
  • 13
  • 15
  • Thanks, Paul; simple and sweet :) (Just fixed some hardcoded values; awaiting peer review.) – Aral Balkan Jan 06 '15 at 14:30
  • Nice work, thanks. For those arriving from the interwebs, be sure to alter the `width`, `height`, `xRadius`, and `yRadius` to match the dimensions of the object you are rounding. – Clifton Labrum Jan 27 '15 at 21:41
  • The parameters of NSMakeSize() are inverted, and you're better off using NSize initializer anyway : let newSize = NSSize(width:esize.width, height:esize.height) – Guillaume Laurent Jan 16 '16 at 17:56
3

Just to provide a bit more info on what you did wrong:

The purpose of saving and restoring the gstate is to be able to undo your changes to the gstate. In your case, restoring the gsave after clipping undid the clip. That's why the solution (as explained by Rhult) is to draw what you want clipped before you restore the gstate.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370