Search code examples
iosswiftcgcontextcgcontextdrawimageuigraphicsimagerenderer

Using CGContext of UIGraphicImageRenderer to redraw an image, flipped the image. How to prevent the flip?


Given an image, when I want to convert to standard sRGB, I can CGContext to help draw it as below.

Given the original image

enter image description here

When I use the CGContext directly, it redraws correctly

extension UIImage {
    func toSRGB() -> UIImage {        
        guard let cgImage = cgImage,
            let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
            let cgContext = CGContext(
                data: nil,
                width: Int(size.width),
                height: Int(size.height),
                bitsPerComponent: 8,
                bytesPerRow: 0,
                space: colorSpace,
                bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
            else { return self }
        cgContext.draw(cgImage, in: CGRect(origin: .zero, size: size))

        guard let newCGImage = cgContext.makeImage()
            else { return self }

        return UIImage.init(cgImage: newCGImage)
    }
}

However, if I use the CGContext from UIGraphicImageRenderer

extension UIImage {
    func toSRGB() -> UIImage {
        guard let cgImage = cgImage else { return self }

        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { ctx in
            ctx.cgContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
        }
    }
}

The image got flipped upside down as below. Why did it get flipped, and how can I avoid the flip?

enter image description here


Solution

  • No need to get the cgImage. you can simply draw the UIImage:

    extension UIImage {
        func toSRGB() -> UIImage {
            UIGraphicsImageRenderer(size: size).image { _ in
                draw(in: CGRect(origin: .zero, size: size))
            }
        }
    }
    

    The reason why your image got flipped is the difference between coordinate system in UIKit and Quartz. UIKit vertical origin is at the top while Quartz vertical origin is at the bottom. The easiest way to fix this is to apply a CGAffineTransform scaling it by minus one and then translating the height distance backwards:

    extension CGAffineTransform {
        static func flippingVerticaly(_ height: CGFloat) -> CGAffineTransform {
            var transform = CGAffineTransform(scaleX: 1, y: -1)
            transform = transform.translatedBy(x: 0, y: -height)
            return transform
        }
    }
    

    extension UIImage {
        func toSRGB() -> UIImage {
            guard let cgImage = cgImage else { return self }
            return UIGraphicsImageRenderer(size: size).image { ctx in
                ctx.cgContext.concatenate(.flippingVerticaly(size.height))
                ctx.cgContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
            }
        }
    }
    

    edit/update:

    To keep the scale at 1x you can pass an image renderer format:

    extension UIImage {
        func toSRGB() -> UIImage {
            guard let cgImage = cgImage else { return self }
            let format = imageRendererFormat
            format.scale = 1
            return UIGraphicsImageRenderer(size: size, format: format).image { ctx in
                ctx.cgContext.concatenate(.flippingVerticaly(size.height))
                ctx.cgContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
            }
        }
    }