19

I'm facing an image orientation issue when cropping a square portion of an image out of a rectangular original image. When image is in landscape, it's fine. But when it is in portrait, it seems that the image orientation is not preserved, which result in an image with wrong orientation AND bad crop:

 func cropImage(cropRectangleCoordinates: CGRect) {

        let croppedImage = originalImage

        let finalCroppedImage : CGImageRef = CGImageCreateWithImageInRect(croppedImage.CGImage, cropRectangleCoordinates)

        finalImage =  UIImage(CGImage: finalCroppedImage)!


    }

I think the problem is with croppedImage.CGImage. Here the image gets converted to CGImage, but it seems not to preserve the orientation. It's easy to preserve orientation by using UIImage only, but to make the crop, image needs to be temporarily CGImage and this is the problem. Even if I reorient the image when converting back to UIImage, it might be in the correct orientation but the damage is already done when cropping CGImage.

This is a swift question, so please answer in swift, as the solution can be different in Objective-C.

Neeku
  • 3,646
  • 8
  • 33
  • 43
Robert Brax
  • 6,508
  • 12
  • 40
  • 69

8 Answers8

22

SWIFT 3:

convert rotated cgImage to UIImage by this method

UIImage(cgImage:croppedCGImage, scale:originalImage.scale, orientation:originalImage.imageOrientation)

Source @David Berry answer

Adnan Yusuf
  • 409
  • 5
  • 6
17

Here's a UIImage extension I wrote after looking after looking at several older pieces of code written by others. It's written in Swift 3 and uses the iOS orientation property plus CGAffineTransform to re-draw the image in proper orientation.

SWIFT 3:

public extension UIImage {

    /// Extension to fix orientation of an UIImage without EXIF
    func fixOrientation() -> UIImage {

        guard let cgImage = cgImage else { return self }

        if imageOrientation == .up { return self }

        var transform = CGAffineTransform.identity

        switch imageOrientation {

        case .down, .downMirrored:
            transform = transform.translatedBy(x: size.width, y: size.height)
            transform = transform.rotated(by: CGFloat(M_PI))

        case .left, .leftMirrored:
            transform = transform.translatedBy(x: size.width, y: 0)
            transform = transform.rotated(by: CGFloat(M_PI_2))

        case .right, .rightMirrored:
            transform = transform.translatedBy(x: 0, y: size.height)
            transform = transform.rotated(by: CGFloat(-M_PI_2))

        case .up, .upMirrored:
            break
        }

        switch imageOrientation {

        case .upMirrored, .downMirrored:
            transform.translatedBy(x: size.width, y: 0)
            transform.scaledBy(x: -1, y: 1)

        case .leftMirrored, .rightMirrored:
            transform.translatedBy(x: size.height, y: 0)
            transform.scaledBy(x: -1, y: 1)

        case .up, .down, .left, .right:
            break
        }

        if let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: cgImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) {

            ctx.concatenate(transform)

            switch imageOrientation {

            case .left, .leftMirrored, .right, .rightMirrored:
                ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))

            default:
                ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
            }

            if let finalImage = ctx.makeImage() {
                return (UIImage(cgImage: finalImage))
            }
        }

        // something failed -- return original
        return self
    }
}
ziligy
  • 1,447
  • 16
  • 10
  • 2
    calling this before converting to calling image.cgImage worked – Cjay Mar 01 '18 at 14:08
  • I know this is old, but the `case .upMirrored, .downMirrored:` and `case .leftMirrored, .rightMirrored` are incorrect. You are doing nothing with the results of `translatedBy` and `scaledBy`. You need a pattern like you have above with `.down`, `.left` and `.right` (saving the result back in the `transform` property. – Rob Sep 13 '22 at 05:06
14

I found a solution.... time will tell if it's robust enough, but it seems to work in all situations. That was a vicious bug to fix.

So the problem is that UIImage, in some case only, lose it's orientation when converted to CGImage. It affects portraits image, that are automatically put in landscape mode. So image that are landscape by default are not affected. But where the bug is vicious is that it doesn't affect ALL portrait images !! Also imageorientation value won't help for some image. Those problematic images are images that user has in it's library that he got from email, messages, or saved from the web, so not taken with a camera. These images possibly don't have orientation information, and thus in some case, an image in portrait.... REMAINS in portrait when converted to CGImage. I really got stuck on that until I realized that some of my image in my device library were saved from messages or emails.

So the only reliable way I found to guess which image will be reoriented, is to create both version of a given image: UIImage, and CGImage, and compare their height value. If they are equal, then the CGImage version will not be rotated and you could work with it as expected. But if they height are different, you can be sure that the CGImage conversion from CGImageCreateWithImageInRect will landscapize the image. In this case only, I swap the x/y coordinate of origin, that I pass as rectangle coordinate to crop and it treats those special images correctly.

That was a long post, but the main idea is to compare CGImage height to UIImage width, and if they are different, expect origin point to be inverted.

Robert Brax
  • 6,508
  • 12
  • 40
  • 69
  • 2
    But if the image was squared that will be a big problem for ur solution :[] – Husam Dec 14 '15 at 14:06
  • Just put a image path like following view and upload image UIImageView *imgViewTemp = [[UIImageView alloc]init]; [imgViewTemp setImage:[UIImage imageWithCGImage:[rep fullScreenImage]]]; UIImage *compressedImage = [mYGlobal funResizing:imgViewTemp.image]; i have same problem but fix like above. – Chetu Jan 24 '17 at 10:20
  • So many bad answers, that's the one, Thanks! – las Feb 23 '23 at 16:36
11

This is THE answer, credit to @awolf (Cropping an UIImage). Handles scale and orientation perfectly. Just call this method on the image you want to crop, and pass in the cropping CGRect without worrying about scale or orientation. Feel free to check whether cgImage is nil instead of force unwrapping it like I did here.

extension UIImage {
    func croppedInRect(rect: CGRect) -> UIImage {
        func rad(_ degree: Double) -> CGFloat {
            return CGFloat(degree / 180.0 * .pi)
        }

        var rectTransform: CGAffineTransform
        switch imageOrientation {
        case .left:
            rectTransform = CGAffineTransform(rotationAngle: rad(90)).translatedBy(x: 0, y: -self.size.height)
        case .right:
            rectTransform = CGAffineTransform(rotationAngle: rad(-90)).translatedBy(x: -self.size.width, y: 0)
        case .down:
            rectTransform = CGAffineTransform(rotationAngle: rad(-180)).translatedBy(x: -self.size.width, y: -self.size.height)
        default:
            rectTransform = .identity
        }
        rectTransform = rectTransform.scaledBy(x: self.scale, y: self.scale)

        let imageRef = self.cgImage!.cropping(to: rect.applying(rectTransform))
        let result = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation)
        return result
    }
}

Another note: if you are working with imageView embedded in a scrollView, there is one additional step, you have to take the zoom factor into account. Assuming your imageView spans the entire content view of the scrollView, and you use the bounds of the scrollView as the cropping frame, the cropped image can be obtained as

let ratio = imageView.image!.size.height / scrollView.contentSize.height
let origin = CGPoint(x: scrollView.contentOffset.x * ratio, y: scrollView.contentOffset.y * ratio)
let size = CGSize(width: scrollView.bounds.size.width * ratio, let height: scrollView.bounds.size.height * ratio)
let cropFrame = CGRect(origin: origin, size: size)
let croppedImage = imageView.image!.croppedInRect(rect: cropFrame)
Jack Guo
  • 3,959
  • 8
  • 39
  • 60
5

Change your UIImage creation call to:

finalImage = UIImage(CGImage:finalCroppedImage, scale:originalImage.scale, orientation:originalImage.orientation)

to maintain the original orientation (and scale) of the image.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • I tried that, but the problem is more vicious: finalimage is already cropped, the loss of orientation happens at an earlier stage: croppedImage.CGImage. What you suggest can correctly rotate the image but after damage is done. – Robert Brax Feb 12 '15 at 15:26
  • Thanks a lot. That help me. – Eugene Trapeznikov Aug 09 '17 at 19:58
5

SWIFT 5. I added the following as an extension to UIImage. Idea is to force the image inside your UIImage to match that of the UIImage orientation (which only plays a role in how it's displayed). Redrawing the actual image data inside the UIImage "container" will make the corresponding CGImage to have the same orientation

func forceSameOrientation() -> UIImage {
    UIGraphicsBeginImageContext(self.size)
    self.draw(in: CGRect(origin: CGPoint.zero, size: self.size))
    guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
        UIGraphicsEndImageContext()
        return self
    }
    UIGraphicsEndImageContext()
    return image
}
Kaccie Li
  • 126
  • 1
  • 5
2

@JGuo has the only answer that has actually worked. I've updated only a little bit to return an optional UIImage? and for swift-er syntax. I prefer to never implicitly unwrap.

extension UIImage {

    func crop(to rect: CGRect) -> UIImage? {
        func rad(_ degree: Double) -> CGFloat {
            return CGFloat(degree / 180.0 * .pi)
        }

        var rectTransform: CGAffineTransform
        switch imageOrientation {
        case .left:
            rectTransform = CGAffineTransform(rotationAngle: rad(90)).translatedBy(x: 0, y: -self.size.height)
        case .right:
            rectTransform = CGAffineTransform(rotationAngle: rad(-90)).translatedBy(x: -self.size.width, y: 0)
        case .down:
            rectTransform = CGAffineTransform(rotationAngle: rad(-180)).translatedBy(x: -self.size.width, y: -self.size.height)
        default:
            rectTransform = .identity
        }
        rectTransform = rectTransform.scaledBy(x: self.scale, y: self.scale)

        guard let imageRef = self.cgImage?.cropping(to: rect.applying(rectTransform)) else { return nil }
        let result = UIImage(cgImage: imageRef, scale: self.scale, orientation: self.imageOrientation)
        return result
    }
}

Here's its implementation as a computed property in my ViewController.

var croppedImage: UIImage? {
    guard let image = self.image else { return nil }

    let ratio = image.size.height / self.contentSize.height
    let origin = CGPoint(x: self.contentOffset.x * ratio, y: self.contentOffset.y * ratio)
    let size = CGSize(width: self.bounds.size.width * ratio, height: self.bounds.size.height * ratio)
    let cropFrame = CGRect(origin: origin, size: size)
    let croppedImage = image.crop(to: cropFrame)

    return croppedImage
}
Alex Wall
  • 101
  • 2
  • 5
1

For Swift 5 you can use:

public extension UIImage {
    func cropped(rect: CGRect) -> UIImage? {
        if let image = self.cgImage?.cropping(to: rect) {
            return UIImage(cgImage: image, scale:self.scale, orientation:self.imageOrientation)
        }
        return nil

} }

SupaFil
  • 21
  • 4