28

I have an NSBitmapImageRep which is WxH size.

I create NSImage and call addRepresentation:. Then I need to resize the NSImage.

I tried setSize method but it doesn't work. What should I do?

pkamb
  • 33,281
  • 23
  • 160
  • 191
hockeyman
  • 1,141
  • 6
  • 27
  • 57

12 Answers12

41

Edit: Since this answer is still the accepted answer, but was written without Retina screens in mind, I will straight up link to a better solution further down the thread: Objective-C Swift 4


Because the method of Paresh is totally correct but deprecated since 10.8 I'll post the working 10.8 code below. All credit to Paresh's answer though.

- (NSImage *)imageResize:(NSImage*)anImage newSize:(NSSize)newSize {
    NSImage *sourceImage = anImage;
    [sourceImage setScalesWhenResized:YES];
    
    // Report an error if the source isn't a valid image
    if (![sourceImage isValid]){
        NSLog(@"Invalid Image");
    } else {
        NSImage *smallImage = [[NSImage alloc] initWithSize: newSize];
        [smallImage lockFocus];
        [sourceImage setSize: newSize];
        [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
        [sourceImage drawAtPoint:NSZeroPoint fromRect:CGRectMake(0, 0, newSize.width, newSize.height) operation:NSCompositingOperationCopy fraction:1.0];
        [smallImage unlockFocus];
        return smallImage;
    }
    return nil;
}
wcochran
  • 10,089
  • 6
  • 61
  • 69
Git.Coach
  • 3,032
  • 2
  • 37
  • 54
  • 4
    [sourceImage setScalesWhenResized:YES]; is deprecated because it is no longer necessary. You can just delete it and this routine works perfectly. – user2002649 May 05 '16 at 00:04
  • 2
    Note that this resizes to screen points, not pixel dimensions. On Retina screens, the resulting image will be twice the requested size in pixels. For exact pixel sizing on Retina screens, use this method: http://stackoverflow.com/a/38442746/30480 – Marco Jul 18 '16 at 17:32
  • `[NSGraphicsContext currentContext]` returns nil for me. any idea? – prabhu Jul 23 '20 at 13:02
  • Doesn't this have the side affect of resizing the `sourceImage`? https://developer.apple.com/documentation/appkit/nsimage/1519987-size?language=objc This may not be what the caller wants, – wcochran Dec 13 '20 at 20:19
39

Thomas Johannesmeyer's answer using lockFocus doesn't work as you may intend on Retina/HiDPI screens: it resizes to the desired points in the screen's native scale, not pixels.

  • If you're resizing for display on screen, use that method.
  • If you're resizing for a file with exact pixel dimensions, it'll be twice as large when running on Retina (2x DPI) screens.

This method, cobbled together from various answers including some in this related question, resizes to the specified pixel dimensions regardless of current screen DPI:

+ (NSImage *)resizedImage:(NSImage *)sourceImage toPixelDimensions:(NSSize)newSize
{
    if (! sourceImage.isValid) return nil;

    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
              initWithBitmapDataPlanes:NULL
                            pixelsWide:newSize.width
                            pixelsHigh:newSize.height
                         bitsPerSample:8
                       samplesPerPixel:4
                              hasAlpha:YES
                              isPlanar:NO
                        colorSpaceName:NSCalibratedRGBColorSpace
                           bytesPerRow:0
                          bitsPerPixel:0];
    rep.size = newSize;

    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:rep]];
    [sourceImage drawInRect:NSMakeRect(0, 0, newSize.width, newSize.height) fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
    [NSGraphicsContext restoreGraphicsState];

    NSImage *newImage = [[NSImage alloc] initWithSize:newSize];
    [newImage addRepresentation:rep];
    return newImage;
}
Community
  • 1
  • 1
Marco
  • 14,977
  • 7
  • 36
  • 33
  • 1
    Thanks Marco I have been searching for this for a while – Douglas Cobb Jan 04 '17 at 16:13
  • The advice to use the `lockFocus()` method is dated; that method is now soft-deprecated. Marco's approach will produce non-retina images on-screen but you can solve that by adding `* 2` to the values for the `pixelsWide` and `pixelsHigh` parameters of the `NSBitmapImageRep` initializer. That makes this approach work well on-screen for retina. (I have no idea what it does for *printed* images...who prints stuff in 2022?) – Bryan Dec 11 '22 at 05:45
37

@Marco's answer written in Swift 4:

extension NSImage {
    func resized(to newSize: NSSize) -> NSImage? {
        if let bitmapRep = NSBitmapImageRep(
            bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height),
            bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
            colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0
        ) {
            bitmapRep.size = newSize
            NSGraphicsContext.saveGraphicsState()
            NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmapRep)
            draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: .zero, operation: .copy, fraction: 1.0)
            NSGraphicsContext.restoreGraphicsState()

            let resizedImage = NSImage(size: newSize)
            resizedImage.addRepresentation(bitmapRep)
            return resizedImage
        }

        return nil
    }
}

let targetSize = NSSize(width: 256.0, height: 256.0)
let newImageResized = myimage.resized(to: targetSize)
Jeehut
  • 20,202
  • 8
  • 59
  • 80
Atika
  • 1,560
  • 18
  • 18
  • 3
    Look no further: I have now tested almost every answer to 'how to resize an image regardless of resolution' that was given here or elsewhere, and this is the only method that works with different image resolutions and regardless whether you have a RetinaDisplay or not. – green_knight Feb 25 '19 at 16:02
  • still works in macOS Catalina, Xcode 11.3, Swift 5. Great answer.. +1 – ICL1901 Jan 29 '20 at 23:26
  • Why do i keep getting a white border when i use this code,Can this be replicated? – techno Apr 05 '20 at 07:46
  • Thanks for that. This is the only solution I found that would preserve the original bitsPerSample and not turn 8bit images to 16bit images. I had to do explicitly call the correct bytesPerRow and bitsPerPixel tho and on the other hand 16bit images could not be created with this and I had to also use drawing into image like @ThomasJohannesmeyer's answer suggests. – Enie May 13 '20 at 10:32
  • For those who do not want anti-aliasing, add `NSGraphicsContext.current.imageInterpolation = .none` before the draw statement. – hotdogsoup.nl Mar 30 '22 at 15:22
  • When this produces fuzzy, non-retina images on your retina screens, simply add `* 2` to the values for `pixelsWide` and `pixelsHigh` in the `NSBitmapRepresentation` initializer. The scaled images will then match those from the older `lockFocus()` scaling approach discussed in other answers. – Bryan Dec 11 '22 at 05:47
17

EDIT You can resize image using below function:

- (NSImage *)imageResize:(NSImage*)anImage
         newSize:(NSSize)newSize 
{
 NSImage *sourceImage = anImage;
 [sourceImage setScalesWhenResized:YES];

 // Report an error if the source isn't a valid image
 if (![sourceImage isValid])
 {
    NSLog(@"Invalid Image");
 } else
 {
    NSImage *smallImage = [[[NSImage alloc] initWithSize: newSize] autorelease];
    [smallImage lockFocus];
    [sourceImage setSize: newSize];
    [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
    [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
    [smallImage unlockFocus];
    return smallImage;
 }
 return nil;
}

Secondly like this:

NSData *imageData = [yourImg  TIFFRepresentation]; // converting img into data
NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; // converting into BitmapImageRep 
NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:0.9] forKey:NSImageCompressionFactor]; // any number betwwen 0 to 1
imageData = [imageRep representationUsingType:NSJPEGFileType properties:imageProps]; // use NSPNGFileType if needed
NSImage *resizedImage = [[NSImage alloc] initWithData:imageData]; // image created from data
Paresh Navadiya
  • 38,095
  • 11
  • 81
  • 132
  • 1
    If you're using 10.8 or later, replace compositeToPoint:operation: with [sourceImage drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.]; – senojsitruc Apr 09 '14 at 14:22
  • I don't quite understand your "Secondly" code. It doesn't itself resize the image, only format conversion … what is this necessary for and where do you use this code together with your first listing? – konran Jun 19 '16 at 16:52
16

Actually it is not necessary to modify any source image parameters like size. The following snippet is already in Swift, but I think you can infer the Objective-C version from it:

func resized(to: CGSize) -> NSImage {
    let img = NSImage(size: to)

    img.lockFocus()
    defer {
        img.unlockFocus()
    }

    if let ctx = NSGraphicsContext.current {
        ctx.imageInterpolation = .high
        draw(in: NSRect(origin: .zero, size: to),
             from: NSRect(origin: .zero, size: size),
             operation: .copy,
             fraction: 1)
    }

    return img
}
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
  • works great except when `to:` is set to 16x16 the image becomes 32x32. Possibly retina calculation? How to correct this? – hotdogsoup.nl Mar 30 '22 at 15:15
5

Here's a Swift 4 version of Thomas Johannesmeyer's answer:

func resize(image: NSImage, w: Int, h: Int) -> NSImage {
    var destSize = NSMakeSize(CGFloat(w), CGFloat(h))
    var newImage = NSImage(size: destSize)
    newImage.lockFocus()
    image.draw(in: NSMakeRect(0, 0, destSize.width, destSize.height), from: NSMakeRect(0, 0, image.size.width, image.size.height), operation: NSCompositingOperation.sourceOver, fraction: CGFloat(1))
    newImage.unlockFocus()
    newImage.size = destSize
    return NSImage(data: newImage.tiffRepresentation!)!
}

And Swift 4 version of Marco's answer:

func resize(image: NSImage, w: Int, h: Int) -> NSImage {
    let destSize = NSMakeSize(CGFloat(w), CGFloat(h))
    let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(destSize.width), pixelsHigh: Int(destSize.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0)
    rep?.size = destSize
    NSGraphicsContext.saveGraphicsState()
    if let aRep = rep {
        NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: aRep)
    }
    image.draw(in: NSMakeRect(0, 0, destSize.width, destSize.height),     from: NSZeroRect, operation: NSCompositingOperation.copy, fraction: 1.0)
    NSGraphicsContext.restoreGraphicsState()
    let newImage = NSImage(size: destSize)
    if let aRep = rep {
        newImage.addRepresentation(aRep)
    }
    return newImage
}
Liam R
  • 536
  • 5
  • 10
3

Complete Swift 3 answer (modified from @Erik Aigner above):

extension NSImage {
    func resizeImage(width: CGFloat, _ height: CGFloat) -> NSImage {
        let img = NSImage(size: CGSize(width:width, height:height))

        img.lockFocus()
        let ctx = NSGraphicsContext.current()
        ctx?.imageInterpolation = .high
        self.draw(in: NSMakeRect(0, 0, width, height), from: NSMakeRect(0, 0, size.width, size.height), operation: .copy, fraction: 1)
        img.unlockFocus()

        return img
    }
}
Community
  • 1
  • 1
Chris Gregg
  • 2,376
  • 16
  • 30
2

Here is a Swift 3 version keeping ratio of image, just set minimumSize as the minimum height or width you want:

func imageResized(image: NSImage) -> NSImage {
    let ratio = image.size.height / image.size.width

    let width: CGFloat
    let height: CGFloat
    // We keep ratio of image
    if ratio > 1 {
        width = minimumSize
        height = minimumSize * ratio
    } else {
        width = minimumSize
        height = minimumSize * (1 / ratio)
    }
    let destSize = NSSize(width: width, height: height)

    let newImage = NSImage(size: destSize)
    newImage.lockFocus()
    image.draw(in: NSRect(x: 0, y: 0, width: destSize.width, height: destSize.height), from: NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height), operation: .sourceOver, fraction: 1.0)
    newImage.unlockFocus()
    newImage.size = destSize
    return NSImage(data: newImage.tiffRepresentation!)!
}
Beninho85
  • 3,273
  • 27
  • 22
2

2020 | SWIFT 4 and 5:

usage:

let resizedImg = someImage.resizedCopy(w: 500.0, h:500.0)
let scaledImg = someImage.scaledCopy( sizeOfLargerSide: 1000.0)

//and bonus:
scaledImg.writePNG(toURL: someUrl )

code:

extension NSImage {
    func scaledCopy( sizeOfLargerSide: CGFloat) ->  NSImage {
        var newW: CGFloat
        var newH: CGFloat
        var scaleFactor: CGFloat
        
        if ( self.size.width > self.size.height) {
            scaleFactor = self.size.width / sizeOfLargerSide
            newW = sizeOfLargerSide
            newH = self.size.height / scaleFactor
        }
        else{
            scaleFactor = self.size.height / sizeOfLargerSide
            newH = sizeOfLargerSide
            newW = self.size.width / scaleFactor
        }
        
        return resizedCopy(w: newW, h: newH)
    }
    
    
    func resizedCopy( w: CGFloat, h: CGFloat) -> NSImage {
        let destSize = NSMakeSize(w, h)
        let newImage = NSImage(size: destSize)
        
        newImage.lockFocus()
        
        self.draw(in: NSRect(origin: .zero, size: destSize),
                  from: NSRect(origin: .zero, size: self.size),
                  operation: .copy,
                  fraction: CGFloat(1)
        )
        
        newImage.unlockFocus()
        
        guard let data = newImage.tiffRepresentation,
              let result = NSImage(data: data)
        else { return NSImage() }
        
        return result
    }
    
    public func writePNG(toURL url: URL) {
        guard let data = tiffRepresentation,
              let rep = NSBitmapImageRep(data: data),
              let imgData = rep.representation(using: .png, properties: [.compressionFactor : NSNumber(floatLiteral: 1.0)]) else {

            Swift.print("\(self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)")
            return
        }

        do {
            try imgData.write(to: url)
        }catch let error {
            Swift.print("\(self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)")
        }
    }
}

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
1

For just scaling NSBitmapImageRep

static NSBitmapImageRep *i_scale_bitmap(const NSBitmapImageRep *bitmap, const uint32_t width, const uint32_t height)
{
    NSBitmapImageRep *new_bitmap = NULL;
    CGImageRef dest_image = NULL;
    CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    CGContextRef context = CGBitmapContextCreate(NULL, (size_t)width, (size_t)height, PARAM(bitsPerComponent, 8), PARAM(bytesPerRow, (size_t)(width * 4)), space, kCGImageAlphaPremultipliedLast);
    CGImageRef src_image = [bitmap CGImage];
    CGRect rect = CGRectMake((CGFloat)0.f, (CGFloat)0.f, (CGFloat)width, (CGFloat)height);
    CGContextDrawImage(context, rect, src_image);
    dest_image = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(space);
    new_bitmap = [[NSBitmapImageRep alloc] initWithCGImage:dest_image];
    CGImageRelease(dest_image);
    return new_bitmap;
}

And for scaling a NSImage based on NSBitmapImageRep

ImageImp *imgimp_create_scaled(const ImageImp *image, const uint32_t new_width, const uint32_t new_height)
{
    NSImage *src_image = (NSImage*)image;
    NSBitmapImageRep *src_bitmap, *dest_bitmap;
    NSImage *scaled_image = nil;
    cassert_no_null(src_image);
    cassert([[src_image representations] count] == 1);
    cassert([[[src_image representations] objectAtIndex:0] isKindOfClass:[NSBitmapImageRep class]]);
    src_bitmap = (NSBitmapImageRep*)[[(NSImage*)image representations] objectAtIndex:0];
    cassert_no_null(src_bitmap);
    dest_bitmap = i_scale_bitmap(src_bitmap, new_width, new_height);
    scaled_image = [[NSImage alloc] initWithSize:NSMakeSize((CGFloat)new_width, (CGFloat)new_height)];
    [scaled_image addRepresentation:dest_bitmap];
    cassert([scaled_image retainCount] == 1);
    [dest_bitmap release];
    return (ImageImp*)scaled_image;
}

Draw directly over NSImage ([NSImage lockFocus], etc) will create a NSCGImageSnapshotRep not a NSBitmapImageRep.

frang
  • 69
  • 5
0

You can resize the image to the desired size using the following functions:

import AppKit

extension NSImage {
    
    /**
     Resizes the image to the given size.
     */
    func resize(withSize targetSize: NSSize) -> NSImage {
        let newImage = NSImage(size: targetSize)
        newImage.lockFocus()
        draw(in: CGRect(origin: .zero, size: targetSize), from: CGRect(origin: .zero, size: size), operation: .sourceOver, fraction: 1.0) 
        newImage.unlockFocus()
        return newImage
    }
    
    /**
     Resizes the image to the given size maintaining its original aspect ratio.
     */
    func resizeMaintainingAspectRatio(withSize targetSize: NSSize) -> NSImage {
        let newSize: NSSize
        let widthRatio = targetSize.width / size.width
        let heightRatio = targetSize.height / size.height
        if(widthRatio > heightRatio) {
            newSize = NSSize(width: floor(size.width * widthRatio), height: floor(size.height * widthRatio))
        } else {
            newSize = NSSize(width: floor(size.width * heightRatio), height: floor(size.height * heightRatio))
        }
        return resize(withSize: newSize)
    }

}
gasol95
  • 1
  • 2
0

Based on Marco's answer:

In my case, resampling and preservation of bitsPerSample did not work with the proposed method (my greyscale image of 8 bps changed to 64 bps). With small modifications I managed to make it work.

If anybody is facing the same issue, here's my solution:

- (NSImage*) resizeImage:(NSImage*)smallImage newSize:(NSSize)newSize {

NSBitmapImageRep *resizeRep =
    [[NSBitmapImageRep alloc]
        initWithBitmapDataPlanes: nil
                      pixelsWide: newSize.width
                      pixelsHigh: newSize.height
                   bitsPerSample: 8
                 samplesPerPixel: 1
                        hasAlpha: NO
                        isPlanar: NO
                  colorSpaceName: NSCalibratedWhiteColorSpace
                     bytesPerRow: 0
                    bitsPerPixel: 8];

[resizeRep setSize: newSize];
[smallImage setSize: newSize];  // ADDITION
[NSGraphicsContext saveGraphicsState];
NSGraphicsContext *newCurrentContext = [NSGraphicsContext graphicsContextWithBitmapImageRep: resizeRep];
[newCurrentContext setImageInterpolation:NSImageInterpolationHigh]; // ADDITION
[NSGraphicsContext setCurrentContext: newCurrentContext];
[smallImage drawAtPoint:NSZeroPoint fromRect:CGRectMake(0, 0, newSize.width, newSize.height) operation:NSCompositingOperationCopy fraction:1.0];
[NSGraphicsContext restoreGraphicsState];

NSImage *resizedImage = [[NSImage alloc] initWithSize:newSize];
[resizedImage addRepresentation: resizeRep];

return resizedImage;
Sili
  • 26
  • 4