Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rotate a UIImage without memory spikes

I am currently rotating images using CGContextDrawImage, but there is a giant memory spike whenever it is called. Newer devices can handle it, but devices with lower ram such as the iPhone 4s can not. The image is a large file taken by the user at AVCaptureSessionPresetHigh

I'm currently rotating the image using this extension to UIImage

func rotate(orientation: UIImageOrientation) -> UIImage{
    if(orientation == UIImageOrientation.Up){return self}

    var transform: CGAffineTransform = CGAffineTransformIdentity

    if(orientation == UIImageOrientation.Down || orientation == UIImageOrientation.DownMirrored){
        transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height)
        transform = CGAffineTransformRotate(transform, CGFloat(M_PI))
    }
    else if(orientation == UIImageOrientation.Left || orientation == UIImageOrientation.LeftMirrored){
        transform = CGAffineTransformTranslate(transform, self.size.width, 0)
        transform = CGAffineTransformRotate(transform, CGFloat(M_PI_2))
    }
    else if(orientation == UIImageOrientation.Right || orientation == UIImageOrientation.RightMirrored){
        transform = CGAffineTransformTranslate(transform, 0, self.size.height)
        transform = CGAffineTransformRotate(transform,  CGFloat(-M_PI_2))
    }


    if(orientation == UIImageOrientation.UpMirrored || orientation == UIImageOrientation.DownMirrored){
        transform = CGAffineTransformTranslate(transform, self.size.width, 0)
        transform = CGAffineTransformScale(transform, -1, 1)
    }
    else if(orientation == UIImageOrientation.LeftMirrored || orientation == UIImageOrientation.RightMirrored){
        transform = CGAffineTransformTranslate(transform, self.size.height, 0)
        transform = CGAffineTransformScale(transform, -1, 1);
    }

    let ref: CGContextRef = CGBitmapContextCreate(nil, Int(self.size.width), Int(self.size.height), CGImageGetBitsPerComponent(self.CGImage), 0, CGImageGetColorSpace(self.CGImage), CGImageGetBitmapInfo(self.CGImage))
    CGContextConcatCTM(ref, transform)

    if(orientation == UIImageOrientation.Left || orientation == UIImageOrientation.LeftMirrored || orientation == UIImageOrientation.Right || orientation == UIImageOrientation.RightMirrored){
        CGContextDrawImage(ref, CGRectMake(0, 0, self.size.height, self.size.width), self.CGImage)
    }
    else{
        CGContextDrawImage(ref, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage)
    }


    let cgImg: CGImageRef = CGBitmapContextCreateImage(ref)
    let rotated: UIImage = UIImage(CGImage: cgImg)!

    return rotated
}

This code works perfectly on devices such as the iPhone 5 and 6, but almost always causes the iPhone 4s to crash due to low memory.

I am aware that I could just upload the image with EXIF data specifying the orientation, but I feel like just rotating the image before uploading it would prevent further problems down the line.

How does iOS rotate the image for display using the EXIF data? For example, although using

UIImage(image.CGImage, scale: 1.0, orientation: UIImageOrientation.Right)

changes the EXIF data, the image is still displayed in the correct orientation on the screen, with a lot less of a memory impact. Is there some really low-level code that can be used to achieve this by using the GPU like the above code does?

I would also like to avoid scaling the image down before uploading it.

Is there any way that I can reduce the impact of rotating the image, such as rotating it in pieces, or will I have to find an alternative?

like image 712
Jojodmo Avatar asked Oct 31 '22 19:10

Jojodmo


1 Answers

I was able to (mostly) solve this by calling UIGraphicsEndImageContext() at the end of the above code, as well as not rotating the image until just before it is uploaded (users can rotate their photos before uploading them, so not rotating them until just before upload greatly decreases the memory usage).

So, whenever the user rotates a photo, I call this rotateExif(orientation) function that I created

extension UIImage{
    func rotateExif(orientation: UIImageOrientation) -> UIImage{

        if(orientation == UIImageOrientation.Up){return self}

        let current = self.imageOrientation
        let currentDegrees: Int = (
            current == UIImageOrientation.Down || current == UIImageOrientation.DownMirrored ? 180 : (
                current == UIImageOrientation.Left || current == UIImageOrientation.LeftMirrored ? 270 : (
                    current == UIImageOrientation.Right || current == UIImageOrientation.RightMirrored ? 90 : 0
                )
            )
        )
        let changeDegrees: Int = (
            orientation == UIImageOrientation.Down || orientation == UIImageOrientation.DownMirrored ? 180 : (
                orientation == UIImageOrientation.Left || orientation == UIImageOrientation.LeftMirrored ? 270 : (
                    orientation == UIImageOrientation.Right || orientation == UIImageOrientation.RightMirrored ? 90 : 0
                )
            )
        )


        let mirrored: Bool = (
            current == UIImageOrientation.DownMirrored || current == UIImageOrientation.UpMirrored ||
            current == UIImageOrientation.LeftMirrored || current == UIImageOrientation.RightMirrored ||
            orientation == UIImageOrientation.DownMirrored || orientation == UIImageOrientation.UpMirrored ||
            orientation == UIImageOrientation.LeftMirrored || orientation == UIImageOrientation.RightMirrored
        )

        let degrees: Int = currentDegrees + changeDegrees

        let newOrientation: UIImageOrientation = (
            degrees == 270 || degrees == 630 ? (mirrored ? UIImageOrientation.LeftMirrored : UIImageOrientation.Left) : (
                degrees == 180 || degrees == 540 ? (mirrored ? UIImageOrientation.DownMirrored : UIImageOrientation.Down) : (
                    degrees == 90 || degrees == 450 ? (mirrored ? UIImageOrientation.RightMirrored : UIImageOrientation.Right) : (
                        mirrored ? UIImageOrientation.UpMirrored : UIImageOrientation.Up
                    )
                )
            )
        )

        return UIImage(CGImage: self.CGImage!, scale: 1.0, orientation: newOrientation)
    }
}

This rotates the image's EXIF by the amount that is inputted in orientation. So, for example, if the original orientation was Right, and it was rotated Right, the new orientation would be Down. If the original was RightMirrored, and it was rotated Left, the new orientation would be UpMirrored.

Then, before uploading the photo, the actual pixels are rotated by calling the code in the question as an extension on UIImage, image.rotate(image.imageOrientation).

like image 90
Jojodmo Avatar answered Nov 15 '22 06:11

Jojodmo