Search code examples
swiftimagemacoscocoansimage

Scaling an image OSX Swift


Im currently trying to scale an image using swift. This shouldnt be a difficult task, since i've implemented a scaling solution in C# in 30 mins - however, i've been stuck for 2 days now.

I've tried googling/crawling through stack posts but to no avail. The two main solutions i have seen people use are:

A function written in Swift to resize an NSImage proportionately

and

resizeNSImage.swift

An Obj C Implementation of the above link

So i would prefer to use the most efficient/least cpu intensive solution, which according to my research is option 2. Due to option 2 using NSImage.lockfocus() and NSImage.unlockFocus, the image will scale fine on non-retina Macs, but double the scaling size on retina macs. I know this is due to the pixel density of Retina macs, and is to be expected, but i need a scaling solution that ignores HiDPI specifications and just performs a normal scale operation.

This led me to do more research into option 1. It seems like a sound function, however it literally doesnt scale the input image, and then doubles the filesize as i save the returned image (presumably due to pixel density). I found another stack post with someone else having the exact same problem as i am, using the exact same implementation (found here). Of the two suggested answers, the first one doesnt work, and the second is the other implementation i've been trying to use.

If people could post Swift-ified answers, as opposed to Obj C, i'd appreciate it very much!

EDIT: Here's a copy of my implementation of the first solution - I've divided it into 2 functions:

func getSizeProportions(oWidth: CGFloat, oHeight: CGFloat) -> NSSize {

    var ratio:Float = 0.0
    let imageWidth = Float(oWidth)
    let imageHeight = Float(oHeight)

    var maxWidth = Float(0)
    var maxHeight = Float(600)

    if ( maxWidth == 0 ) {
      maxWidth = imageWidth
    }

    if(maxHeight == 0) {
      maxHeight = imageHeight
    }

    // Get ratio (landscape or portrait)
    if (imageWidth > imageHeight) {
      // Landscape
      ratio = maxWidth / imageWidth;
    }
    else {
      // Portrait
      ratio = maxHeight / imageHeight;
    }

    // Calculate new size based on the ratio
    let newWidth = imageWidth * ratio
    let newHeight = imageHeight * ratio

    return NSMakeSize(CGFloat(newWidth), CGFloat(newHeight))
}


func resizeImage(image:NSImage) -> NSImage {
    print("original: ", image.size.width, image.size.height )

    // Cast the NSImage to a CGImage
    var imageRect:CGRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
    let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)

    // Create a new NSSize object with the newly calculated size
    let newSize = NSSize(width: CGFloat(450), height: CGFloat(600))
    //let newSize = getSizeProportions(oWidth: CGFloat(image.size.width), oHeight: CGFloat(image.size.height))

    // Create NSImage from the CGImage using the new size
    let imageWithNewSize = NSImage(cgImage: imageRef!, size: newSize)

    print("scaled: ", imageWithNewSize.size.width, imageWithNewSize.size.height )

    return NSImage(data: imageWithNewSize.tiffRepresentation!)!
}

EDIT 2:

As pointed out by Zneak: i need to save the returned image to disk - Using both implementations, my save function writes the file to disk successfully. Although i dont think my save function could be screwing with my current resizing implementation, i've attached it anyways just in case:

func saveAction(image: NSImage, url: URL) {

    if let tiffdata = image.tiffRepresentation,
    let bitmaprep = NSBitmapImageRep(data: tiffdata) {

    let props = [NSImageCompressionFactor: Appearance.imageCompressionFactor]
    if let bitmapData = NSBitmapImageRep.representationOfImageReps(in: [bitmaprep], using: .JPEG, properties: props) {

        let path: NSString = "~/Desktop/out.jpg"
        let resolvedPath = path.expandingTildeInPath

        try! bitmapData.write(to: URL(fileURLWithPath: resolvedPath), options: [])

        print("Your image has been saved to \(resolvedPath)")
    }
}

Solution

  • To anyone else experiencing this problem - I ended up spending countless hours trying to find a way to do this, and ended up just getting the scaling factor of the screen (1 for normal macs, 2 for retina)... The code looks like this:

    func getScaleFactor() -> CGFloat {
        return NSScreen.main()!.backingScaleFactor
    }
    

    Then once you have the scale factor you either scale normally or half the dimensions for retina:

    if (scaleFactor == 2) {
        //halve size proportions for saving on Retina Macs
        return NSMakeSize(CGFloat(oWidth*ratio)/2, CGFloat(oHeight*ratio)/2)
    } else {
        return NSMakeSize(CGFloat(oWidth*ratio), CGFloat(oHeight*ratio))
    }