0

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)")
    }
}
Community
  • 1
  • 1
amartin94
  • 505
  • 3
  • 19
  • 35
  • "Should" or "shouldn't"? Trivial means easy. Also, are you doing this for the purpose of showing it on screen or writing it back to disk or something else? Scaling to a bigger or a smaller size? – zneak Apr 13 '17 at 04:17
  • edited the OP :) – amartin94 Apr 13 '17 at 04:19
  • And im Scaling down to write back to the disk – amartin94 Apr 13 '17 at 04:48
  • What is the purpose of converting `CGFloat` to `Float` and then back to `CGFloat`? And you don't need parentheses around `if` expressions in Swift. – vadian Apr 13 '17 at 04:49
  • Totally forgot to change that for the post... Initially they were all floats, but i've changed so many different parts trying to make it work – amartin94 Apr 13 '17 at 04:57

1 Answers1

1

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))
}
amartin94
  • 505
  • 3
  • 19
  • 35
  • Instead of checking for a specific scale factor, you should instead divide by it: `CGFloat(oWidth*ratio)/scaleFactor`. This way your code will continue to work correctly should Apple introduce devices/modes where the backing scale factor is some other value. But *do* check for 0 first! – DarkDust Mar 02 '18 at 14:16
  • Would be a nice way of doing it - For the time being though i was happy to get it working :P – amartin94 May 01 '18 at 04:05