How to perform vertical flipping on CGImage? - ios

Currently, I have the following color wheel implemented using the following code
class ColorWheelPalette: UIView {
var cgImage: CGImage?
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
if self.cgImage == nil {
// https://stackoverflow.com/a/54819655/72437
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
"inputDither": 0,
"inputRadius": 160,
"inputSoftness": 0,
"inputValue": 1
])!
let ciImage = filter.outputImage!
let ciContext = CIContext(options: nil)
self.cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent)
}
context.draw(cgImage!, in: rect)
}
}
It looks as following
However, the output image is not what I desire. I want to green color region stay on the top. Hence, I do a quick vertical flipping using the following code
class ColorWheelPalette: UIView {
var cgImage: CGImage?
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
if self.cgImage == nil {
// https://stackoverflow.com/a/54819655/72437
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
"inputDither": 0,
"inputRadius": 160,
"inputSoftness": 0,
"inputValue": 1
])!
let ciImage = filter.outputImage!
let ciContext = CIContext(options: nil)
self.cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent)
}
context.saveGState()
// Flip vertically
context.translateBy(x: 0, y: rect.height)
context.scaleBy(x: 1.0, y: -1.0)
context.draw(cgImage!, in: rect)
context.restoreGState()
}
}
The above code achieve what I want (green region at the top)
However, for performance optimisation purpose, instead of having perform translate and scale transformation operations each time,
is there a way to perform such vertical flipping on the self.cgImage directly for just one-time?
So that the ideal code would look like the following
class ColorWheelPalette: UIView {
...
override func draw(_ rect: CGRect) {
...
if self.cgImage == nil {
...
self.cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent)
// Question: Can we perform one-time vertical flipping on self.cgImage right here?
}
context.draw(cgImage!, in: rect)
}
}

Just create an UIImage from your CGImage and draw it directly:
class ColorWheelPalette: UIView {
var image: UIImage?
override func draw(_ rect: CGRect) {
if image == nil {
guard
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
"inputDither": 0,
"inputRadius": 160,
"inputSoftness": 0,
"inputValue": 1]),
let ciImage = filter.outputImage,
let cgImage = CIContext(options: nil)
.createCGImage(ciImage, from: ciImage.extent)
else { return }
let size = ciImage.extent.size
let format = UIGraphicsImageRendererFormat.default()
format.opaque = false
image = UIGraphicsImageRenderer(size: size, format: format).image { ctx in
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -size.height)
ctx.cgContext.concatenate(transform)
ctx.cgContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
}
image?.draw(in: rect)
}
}

Related

How can i add Gradient colour for button tint in swift iOS

is there any solution to apply tint colour as a gradient in swift
my button has a simple white icon I want to change it to a gradient colour
I updated my answer according to Duncan C comment.
You can modify your image before setting it to button.
extension UIImage {
func drawLinearGradient(colors: [CGColor], startingPoint: CGPoint, endPoint: CGPoint) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: self.size)
var shouldReturnNil = false
let gradientImage = renderer.image { context in
context.cgContext.translateBy(x: 0, y: self.size.height)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
context.cgContext.setBlendMode(.normal)
let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
// Create gradient
let colors = colors as CFArray
let colorsSpace = CGColorSpaceCreateDeviceRGB()
guard let gradient = CGGradient(colorsSpace: colorsSpace, colors: colors, locations: nil) else {
shouldReturnNil = true
return
}
// Apply gradient
guard let cgImage = self.cgImage else {
shouldReturnNil = true
print("Couldn't get cgImage of UIImage.")
return
}
context.cgContext.clip(to: rect, mask: cgImage)
context.cgContext.drawLinearGradient(
gradient,
start: endPoint,
end: startingPoint,
options: .init(rawValue: 0)
)
}
return shouldReturnNil ? nil : gradientImage
}
}
You can use it like that:
guard let image = UIImage(named: "<your_image_name>") else { return }
v.image = image.drawLinearGradient(
colors: [UIColor.blue.cgColor, UIColor.red.cgColor],
startingPoint: .init(x: image.size.width / 2, y: 0),
endPoint: .init(x: image.size.width / 2, y: image.size.height)
)
button.setImage(gradientImage, for: .normal)
This code produces result like this:
I have the same problem too
but for now
if you wanna change the text color it works.
like you convert image into pattern color by this code
/*
class gradiunTitle : UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
let gradient = getGradientLayer(bounds: self.bounds)
self.setTitleColor(gradientColor(bounds: self.bounds, gradientLayer: gradient), for: .normal)
self.tintColor = gradientColor(bounds: self.frame, gradientLayer: gradient)
}
func getGradientLayer(bounds : CGRect) -> CAGradientLayer{
let gradient = CAGradientLayer()
gradient.frame = bounds
//order of gradient colors
gradient.colors = [UIColor.red.cgColor,UIColor.blue.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
return gradient
}
func gradientColor(bounds: CGRect, gradientLayer :CAGradientLayer) -> UIColor? {
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIColor(patternImage: image!)
}
}
*/

clampedToExtent() not work when rotation angle

I wanted to achieve a CIAffineClamp effect where the image could be rotated, scale, and translation and still fill the area. But after trying, I still have some problems.
Show the code in question:
func translateAndClamp(image: UIImage) -> UIImage {
guard let ciImage = CIImage(image: image) else {
return image
}
var transform = CGAffineTransform.identity
transform = transform.concatenating(CGAffineTransform(translationX: -(ciImage.extent.origin.x + ciImage.extent.width/2), y: -(ciImage.extent.origin.y + ciImage.extent.height/2)))
transform = transform.concatenating(CGAffineTransform(translationX: 100, y: 0))
transform = transform.concatenating(CGAffineTransform(scaleX: 0.6, y: 0.6))
// if you open this code, everything effect was gone. Whether the rotation angle will affect the effect?
// transform = transform.concatenating(CGAffineTransform(rotationAngle: 0.2))
transform = transform.concatenating(CGAffineTransform(translationX: (ciImage.extent.origin.x + ciImage.extent.width/2), y: (ciImage.extent.origin.y + ciImage.extent.height/2)))
let outputImage = ciImage.transformed(by: transform).clampedToExtent()
let context = CIContext(options: [CIContextOption.useSoftwareRenderer: true])
let extent = ciImage.extent
guard let result = context.createCGImage(outputImage, from: extent) else {
return image
}
return UIImage(cgImage: result, scale: image.scale, orientation: image.imageOrientation)
}

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
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?
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))
}
}
}
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
looks up-side down.
the Pixel doubled in both width and height (10x6), to smoother the color edge.
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))
}
}
}
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)
}

Cropping UIImage - change background color

I have a function that crops a UIImage to the dimensions of a given rectangle. Sometimes part of the rectangle falls outside the image. After cropping, the default color of this extra part is white. Is it possible to change this color?
func crop(mainImage:UIImage, rectangle:CGRect) -> UIImage?{
let rectSize = CGSize(width: floor(rectangle.width), height: floor(rectangle.height))
UIGraphicsBeginImageContextWithOptions(rectSize, false, mainImage.scale)
let context:CGContext = UIGraphicsGetCurrentContext()!;
mainImage.draw(at: CGPoint(x: -rectangle.origin.x, y: -rectangle.origin.y))
let croppedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return croppedImage
}
You can use extension
extension UIImage {
func crop( rect: CGRect) -> UIImage {
var rect = rect
rect.origin.x*=self.scale
rect.origin.y*=self.scale
rect.size.width*=self.scale
rect.size.height*=self.scale
let imageRef = self.cgImage!.cropping(to: rect)
let image = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation)
return image
}
}
either use method like below
/**
Extracts image at a given rect from an image
- parameter rect : rect for which UIImage should be extracted within
- returns : new UIImage created from rect value supplied
*/
func imageByCroppingAtRect(_ rect: CGRect) -> UIImage? {
let imageRef: CGImage? = cgImage?.cropping(to: rect)
if imageRef == nil {
return nil
} else {
let subImage: UIImage = UIImage(cgImage: imageRef!)
return subImage
}
}

Create a universal vector image programmatically

We can now create universal vector images in Xcode by adding them into image assets folder and then changing the scale factors to single vector. I would like to know how to do this programmatically. I've tried the following but no luck :(
func resizeImage(image: UIImage, newSize: CGSize) -> (UIImage) {
let newRect = CGRectIntegral(CGRectMake(0,0, newSize.width, newSize.height))
let imageRef = image.CGImage
let scaleFactor = UIScreen.mainScreen().scale
UIGraphicsBeginImageContextWithOptions(newSize, false, scaleFactor)
// UIGraphicsBeginImageContextWithOptions(newSize, false, 0.5)
let context = UIGraphicsGetCurrentContext()
// Set the quality level to use when rescaling
CGContextSetInterpolationQuality(context, CGInterpolationQuality.High)
let flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height)
CGContextConcatCTM(context, flipVertical)
// Draw into the context; this scales the image
CGContextDrawImage(context, newRect, imageRef)
let newImageRef = CGBitmapContextCreateImage(context)! as CGImage
let newImage = UIImage(CGImage: newImageRef)
// Get the resized image from the context and a UIImage
UIGraphicsEndImageContext()
return newImage
}
This should do it. Yea...Apple kind of gears this towards setting it in storyboard by ticking the preserve vector data. I'm not sure how to preserve vector data programmatically.
public class var add: UIImage {
let path = bundle.url(forResource: "plusReal", withExtension: "pdf")
return drawPDFFromURL(url: path!)!
}
class func drawPDFFromURL(url: URL) -> UIImage? {
guard let document = CGPDFDocument(url as CFURL),
let page = document.page(at: 1) else {return nil }
let pageRect = page.getBoxRect(.mediaBox)
let renderer = UIGraphicsImageRenderer(size: pageRect.size)
let img = renderer.image { ctx in
UIColor.white.set()
ctx.fill(pageRect)
ctx.cgContext.translateBy(x: 0.0, y: pageRect.size.height)
ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
ctx.cgContext.drawPDFPage(page)
}
return img
}

Resources