1

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

Elye
  • 53,639
  • 54
  • 212
  • 474
  • please try to check this: https://stackoverflow.com/questions/506622/cgcontextdrawimage-draws-image-upside-down-when-passed-uiimage-cgimage?noredirect=1&lq=1 – Kishan Bhatiya Oct 26 '20 at 12:57

3 Answers3

4

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))
        }
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Nice, that's really cool. I'm actually trying to understand CGContext in more in-depth. Wonder why did we get the image upside down when we use `UIGraphicsImageRenderer`'s `ctx.cgContext.draw` ? – Elye Oct 26 '20 at 21:41
  • 1
    https://www.informit.com/articles/article.aspx?p=2149190&seqNum=11 – Leo Dabus Oct 26 '20 at 21:54
  • Thanks @LeoDabus for the further detail elaboration. I have provided my further finding between using CGContext directly and UIGraphicsImageRenderer's as below. The image resolution changes. – Elye Oct 27 '20 at 05:29
  • @Elye I am getting the same size here. It might be because of the device screen scale. Check my last edit and let me know if this solves your issue. – Leo Dabus Oct 27 '20 at 14:13
  • Hi @LeoDabus, your original solution already solve my problem. I'm just curious and try to understand more. Thanks for your sharing. As for the size, I'm actually using code playground to render the image. What device did you use? The 5x3 size image I use is https://upload.wikimedia.org/wikipedia/en/thumb/4/41/Flag_of_India.svg/5px-Flag_of_India.svg.png I've written to describe what I did here https://medium.com/p/627c1d350561 and the code here https://gist.github.com/elye/49b66fc5bd42248b8b82efba0f2b7384 – Elye Oct 27 '20 at 21:55
  • @Elye your Flag of India's link is not working for me. OPS. Never mind. It is tooooooo small – Leo Dabus Oct 27 '20 at 22:00
  • It should work.It is very tiny, and might not easily visible.... It's an image from Wikipedia flag. If change to 100 pixel width, it looks like this https://upload.wikimedia.org/wikipedia/en/thumb/4/41/Flag_of_India.svg/100px-Flag_of_India.svg.png – Elye Oct 27 '20 at 22:01
2

After exploration, just to add to the clarification on what I notice on their differences as below.

The original image of 5x3 pixels

enter image description here

When using CGContext directly

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)
    }
}

We retained the same Pixel (5x3). This is the pro of this solution, but it requires one to access the CGContext and manipulate it directly.

enter image description here

The second approach is UIGraphicsImageRenderer and accessing the internal ctx

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))
        }
    }
}

Using this approach, it

  • looks up-side down.
  • the Pixel doubled in both width and height (10x6), to smoother the color edge.

enter image description here

To solve the up-side down, we need to flip it as per the answer by @LeoDabus, using

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))
        }
    }
}

With that the image now having the correct orientation, but still double in width and height (10x6).

enter image description here

The flipped result is the same as the simplified answer provided by @LeoDabus

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

enter image description here

Elye
  • 53,639
  • 54
  • 212
  • 474
1

Here's the technique applied directly (without extension) to UIGraphicsImageRenderer, wherein CTFontDrawGlyphs() does the drawing.

        // Assume the preceding code (omitted for this example) does this:
        //
        //   - Loads a CTFont for a specified font size
        //   - calls CTFontGetGlyphsForCharacters() to cvt UniChar to CGGlyph
        //   - calls CTFontGetBoundingRectsForGlyphs() to get their bbox's 
                      .
                      .
                      .

        // This code concatenates an CGAffineTransform() to the context
        // prior to calling CTFontDrawGlyphs(), causing the image
        // to flip 180° so that it looks right in UIKit.

        let points = [CGPoint(x: 0, y: 0)]
        let rect = CGRect(x:0, y:0, width: bbox.size.width, height: bbox.size.height)
        let renderer = UIGraphicsImageRenderer(bounds: rect, format: UIGraphicsImageRendererFormat.default())
        let image = renderer.image { renderCtx in
            let ctx = renderCtx.cgContext
            ctx.concatenate(CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -rect.size.height))
            ctx.setFillColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
            CTFontDrawGlyphs(ctFont, glyphs, points, glyphs.count, ctx)
        }
clearlight
  • 12,255
  • 11
  • 57
  • 75