Reputation: 60321
Given an image, when I want to convert to standard sRGB, I can CGContext to help draw it as below.
Given the original image
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?
Upvotes: 2
Views: 3242
Reputation: 12625
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)
}
Upvotes: 1
Reputation: 236548
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))
}
}
}
Upvotes: 6
Reputation: 60321
After exploration, just to add to the clarification on what I notice on their differences as below.
The original image of 5x3 pixels
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.
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
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).
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))
}
}
}
Upvotes: 2