4

Is there an easy way to rotate a NSImage in a Mac OSX app? Or just set the orientation from portrait to landscape using Swift?

I am playing around with CATransform3DMakeAffineTransform but I can't get it to work.

CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI) * 90/180))

It's the first time for me to work with transformations. So please be patient with me :) Maybe I'm working on a wrong approach...

Can anybody help me please?

Thanks!

stefOCDP
  • 803
  • 2
  • 12
  • 20

6 Answers6

12
public extension NSImage {
public func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {

    var imageBounds = NSZeroRect ; imageBounds.size = self.size
    let pathBounds = NSBezierPath(rect: imageBounds)
    var transform = NSAffineTransform()
    transform.rotateByDegrees(degrees)
    pathBounds.transformUsingAffineTransform(transform)
    let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
    let rotatedImage = NSImage(size: rotatedBounds.size)

    //Center the image within the rotated bounds
    imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
    imageBounds.origin.y  = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)

    // Start a new transform
    transform = NSAffineTransform()
    // Move coordinate system to the center (since we want to rotate around the center)
    transform.translateXBy(+(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
    transform.rotateByDegrees(degrees)
    // Move the coordinate system bak to normal 
    transform.translateXBy(-(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
    // Draw the original image, rotated, into the new image
    rotatedImage.lockFocus()
    transform.concat()
    self.drawInRect(imageBounds, fromRect: NSZeroRect, operation: NSCompositingOperation.CompositeCopy, fraction: 1.0)
    rotatedImage.unlockFocus()

    return rotatedImage
}


var image = NSImage(named:"test.png")!.imageRotatedByDegreess(CGFloat(90))  //use only this values 90, 180, or 270
}

Updated for Swift 3:

public extension NSImage {
public func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {

    var imageBounds = NSZeroRect ; imageBounds.size = self.size
    let pathBounds = NSBezierPath(rect: imageBounds)
    var transform = NSAffineTransform()
    transform.rotate(byDegrees: degrees)
    pathBounds.transform(using: transform as AffineTransform)
    let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
    let rotatedImage = NSImage(size: rotatedBounds.size)

    //Center the image within the rotated bounds
    imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
    imageBounds.origin.y  = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)

    // Start a new transform
    transform = NSAffineTransform()
    // Move coordinate system to the center (since we want to rotate around the center)
    transform.translateX(by: +(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
    transform.rotate(byDegrees: degrees)
    // Move the coordinate system bak to normal
    transform.translateX(by: -(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
    // Draw the original image, rotated, into the new image
    rotatedImage.lockFocus()
    transform.concat()
    self.draw(in: imageBounds, from: NSZeroRect, operation: NSCompositingOperation.copy, fraction: 1.0)
    rotatedImage.unlockFocus()

    return rotatedImage
    }
}

class SomeClass: NSViewController {
       var image = NSImage(named:"test.png")!.imageRotatedByDegreess(degrees: CGFloat(90))  //use only this values 90, 180, or 270
}
Mark
  • 6,647
  • 1
  • 45
  • 88
C-Viorel
  • 1,801
  • 3
  • 22
  • 41
5

Thank for this solution, however it did not worked perfectly for me. As you may have noticed that pathBounds is not used anywhere. In my opinion is has to be used like so:

let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y , pathBounds.bounds.size.width, pathBounds.bounds.size.height )

Otherwise the image will be rotated but cropped to a square bounds.

tinbert
  • 59
  • 1
  • 1
4

Letting IKImageView do the heavy lifting:

import Quartz

extension NSImage {
    func imageRotated(by degrees: CGFloat) -> NSImage {
        let imageRotator = IKImageView()
        var imageRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
        let cgImage = self.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
        imageRotator.setImage(cgImage, imageProperties: [:])
        imageRotator.rotationAngle = CGFloat(-(degrees / 180) * CGFloat(M_PI))
        let rotatedCGImage = imageRotator.image().takeUnretainedValue()
        return NSImage(cgImage: rotatedCGImage, size: NSSize.zero)
    }
}
jbaraga
  • 656
  • 5
  • 16
  • Any idea how to make it work with `rotateImageLeft` / `rotateImageRight`? I guess those two don't do any real rotation but rather update some orientation tag instead. – silverdr Apr 18 '19 at 12:27
4

Here's a simple Swift (4+) solution to drawing an image that is rotated around the center:

extension NSImage {
    /// Rotates the image by the specified degrees around the center.
    /// Note that if the angle is not a multiple of 90°, parts of the rotated image may be drawn outside the image bounds.
    func rotated(by angle: CGFloat) -> NSImage {
        let img = NSImage(size: self.size, flipped: false, drawingHandler: { (rect) -> Bool in
            let (width, height) = (rect.size.width, rect.size.height)
            let transform = NSAffineTransform()
            transform.translateX(by: width / 2, yBy: height / 2)
            transform.rotate(byDegrees: angle)
            transform.translateX(by: -width / 2, yBy: -height / 2)
            transform.concat()
            self.draw(in: rect)
            return true
        })
        img.isTemplate = self.isTemplate // preserve the underlying image's template setting
        return img
    }
}
marcprux
  • 9,845
  • 3
  • 55
  • 72
4

This one works also for non-square images, Swift 5.

extension NSImage {
    func rotated(by degrees : CGFloat) -> NSImage {
        var imageBounds = NSRect(x: 0, y: 0, width: size.width, height: size.height)
        let rotatedSize = AffineTransform(rotationByDegrees: degrees).transform(size)
        let newSize = CGSize(width: abs(rotatedSize.width), height: abs(rotatedSize.height))
        let rotatedImage = NSImage(size: newSize)

        imageBounds.origin = CGPoint(x: newSize.width / 2 - imageBounds.width / 2, y: newSize.height / 2 - imageBounds.height / 2)

        let otherTransform = NSAffineTransform()
        otherTransform.translateX(by: newSize.width / 2, yBy: newSize.height / 2)
        otherTransform.rotate(byDegrees: degrees)
        otherTransform.translateX(by: -newSize.width / 2, yBy: -newSize.height / 2)

        rotatedImage.lockFocus()
        otherTransform.concat()
        draw(in: imageBounds, from: CGRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0)
        rotatedImage.unlockFocus()

        return rotatedImage
    }
}
OwlOCR
  • 1,127
  • 11
  • 22
  • Good code, but it actually crops in one direction while extends correctly in the other direction. The problem is the AffineTransform() rotation. – LGP May 21 '20 at 06:52
3

Building on @FrankByte.com's code, this version should extend correctly in both x and y on any image and any rotation.

extension NSImage {
    func rotated(by degrees: CGFloat) -> NSImage {
        let sinDegrees = abs(sin(degrees * CGFloat.pi / 180.0))
        let cosDegrees = abs(cos(degrees * CGFloat.pi / 180.0))
        let newSize = CGSize(width: size.height * sinDegrees + size.width * cosDegrees,
                             height: size.width * sinDegrees + size.height * cosDegrees)

        let imageBounds = NSRect(x: (newSize.width - size.width) / 2,
                                 y: (newSize.height - size.height) / 2,
                                 width: size.width, height: size.height)

        let otherTransform = NSAffineTransform()
        otherTransform.translateX(by: newSize.width / 2, yBy: newSize.height / 2)
        otherTransform.rotate(byDegrees: degrees)
        otherTransform.translateX(by: -newSize.width / 2, yBy: -newSize.height / 2)

        let rotatedImage = NSImage(size: newSize)
        rotatedImage.lockFocus()
        otherTransform.concat()
        draw(in: imageBounds, from: CGRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0)
        rotatedImage.unlockFocus()

        return rotatedImage
    }
}
LGP
  • 4,135
  • 1
  • 22
  • 34