Adding gradient behind image returns empty image - swift

I have a round avatar image with a transparent background. I want to create a new round image of the same size out of the initial image, with a gradient background behind it. So it looks like standing in sky instead of having a transparent background.
Since I will use this image as tabbaritem’s image, I couldn’t use uiview and edit it’s background layer.
And to make it reusable I wanted to create a UIImage extension.
Below is what I do:
extension UIImage {
func gradientImage() -> UIImage? {
let width = self.size.width
let height = self.size.height
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let bitmapContext = CGContext(data: nil,
width: Int(width),
height: Int(height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue) else { return nil }
let locations: [CGFloat] = [0.0, 1.0]
let top = R.color.duckDimDarkGrey()?.cgColor
let bottom = R.color.duckPencilDark()?.cgColor
let colors = [top, bottom] as CFArray
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: locations) else {
return nil
}
bitmapContext.drawLinearGradient(gradient, start: CGPoint.zero, end: CGPoint(x: 0, y: size.height), options: CGGradientDrawingOptions())
guard let cgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { return nil }
UIGraphicsEndImageContext()
let img = UIImage(cgImage: cgImage)
return img
}
}
Here is how I use it:
Let image1 = UIImage(named: “test.png”)
self.tabBar.items[3].image = image1.gradientImage()
However I am getting an empty image somehow.

Related

How can I change this black shade (gradient) to some colored shade?

I want to use the actual color of the image, apply alpha effect pixel by pixel and create something like this but in colored way. How can I do it?
I tried giving it a color but it turned my image from white to pink.
In this code, I go pixel by pixel and change the pixel alpha to 0 or 1. If I make the rgb to black e.g 0, it creates the right gradient, however when I use rgb values other than 0, it turns into white shades which is not required
func processPixels(in image: UIImage) -> UIImage? {
let cgImage = convertCIImageToCGImage(inputImage: image)
guard let inputCGImage = cgImage else {
print("unable to get cgImage")
return nil
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let width = inputCGImage.width
let height = inputCGImage.height
let bytesPerPixel = 4
let bitsPerComponent = 8
let bytesPerRow = bytesPerPixel * width
let bitmapInfo = RGBA32.bitmapInfo
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
print("unable to create context")
return nil
}
context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let buffer = context.data else {
print("unable to get context data")
return nil
}
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
var rowAlpha: UInt8 = 0
for row in 0 ..< Int(height) {
rowAlpha = UInt8(row)
for column in 0 ..< Int(width) {
let offset = row * width + column
if pixelBuffer[offset].alphaComponent > 0 {
pixelBuffer[offset] = RGBA32(red: pixelBuffer[offset].redComponent, green: pixelBuffer[offset].greenComponent, blue: pixelBuffer[offset].blueComponent, alpha: rowAlpha)
}
}
}
let outputCGImage = context.makeImage()!
let outputImage = UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
return outputImage
}
func convertCIImageToCGImage(inputImage: UIImage) -> CGImage? {
guard let ciImage = inputImage.ciImage else {
print("unable to get ciImage")
return nil
}
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
return cgImage
}
return nil
}
Original Image
Gradient Image without color

Convert [UInt8] into white and transparent image

I'm trying to create a white and transparent image in swift from a [Uint8] array. The array has width * height elements and each element correspond to the transparency (alpha value).
So far, I managed to create a black and white image using this :
guard let providerRef = CGDataProvider(data: Data.init(bytes: bitmapArray) as CFData) else { return nil }
guard let cgImage = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 8,
bytesPerRow: width,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGBitmapInfo.init(rawValue: CGImageAlphaInfo.none.rawValue),
provider: providerRef,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
let image = UIImage(cgImage: cgImage)
unfortunately, as I said, this gives me a black and white image.
What I would like is to turn every black pixel (0 in my initial array) into a completely transparent pixel (my array contains only either 0 or 255). How do I do that ?
PS: I've tried to use CGImageAlphaInfo.alphaOnlybut I get "CGImageCreate: invalid image alphaInfo: 7"
Any help would be appreciated.
I found a solution that doesn't exactly satisfies me in terms of code elegance but does the job. The solution is to create that black and white full opaque image, and mask all black pixels using a CIFilter.
Here's a working code:
guard let providerRef = CGDataProvider(data: Data.init(bytes: bitmapArray) as CFData) else { return nil }
guard let cgImage = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 8,
bytesPerRow: width,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGBitmapInfo.init(rawValue: CGImageAlphaInfo.none.rawValue),
provider: providerRef,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
let context = CIContext(options: nil)
let ciimage = CIImage(cgImage: cgImage)
guard let filter = CIFilter(name: "CIMaskToAlpha") else { return nil }
filter.setDefaults()
filter.setValue(ciimage, forKey: kCIInputImageKey)
guard let result = filter.outputImage else { return nil }
guard let newCgImage = context.createCGImage(result, from: result.extent) else { return nil }
return UIImage(cgImage: newCgImage)
Feel free to provide your own (perhaps more elegant/optimal) solution !
I found a workaround: since kCGAlphaImageOnly is supported by CGBitmapContext, you can create a bitmap context from the data, then create an image from that context. This is Objective-C, but it shouldn't be too hard to translate into Swift:
CGContextRef ctx = CGBitmapContextCreate(
bitmapArray, width, height,
8, width, NULL, (CGBitmapInfo)kCGImageAlphaOnly
);
CGImageRef image = CGBitmapContextCreateImage(ctx);

Swift CGContext with retina

I have a UIImage extension that can change the color of it's image that I pulled off somewhere. The problem is that it downgrades it's resolution after it colors the image. I've seen other answers based on this, but I'm not sure how to adapt this to rendering a retina image in this instance:
extension UIImage {
func maskWithColor(color: UIColor) -> UIImage? {
let maskImage = cgImage!
let width = size.width
let height = size.height
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)!
context.clip(to: bounds, mask: maskImage)
context.setFillColor(color.cgColor)
context.fill(bounds)
if let cgImage = context.makeImage() {
let coloredImage = UIImage(cgImage: cgImage)
return coloredImage
} else {
return nil
}
}
}
I've seen people using UIGraphicsBeginImageContextWithOptions and setting it's scale to the main screen, but I don't think it works if I'm using the CGContext function.
I think you want:
let width = size.width * scale
let height = size.height * scale
and:
let coloredImage = UIImage(cgImage: cgImage, scale:scale, orientation:.up)
(You may need to use imageOrientation instead of .up.)

Merging two Images & text on a final image

What I'm trying to do in Swift is merge two images together, the images I'm merging together is me dragging one image on top of another image with touch gestures. I get the images to composite, BUT the image I dragged on top of the main image does not keep it's position. How do I get it to stay where I placed it for the composite?
Here's my code:
extension CIHHatSelectionViewController{
// Return composite image of image2 overlayed on image1
//
func compositeImage(_ image1: UIImage, image2: UIImage, drawText: String, imageView1: CGPoint, imageView2:CGRect) -> UIImage {
let catPos = (image1.size.height - imageView2.origin.y) - imageView2.height
let bounds1 = CGRect(x: 0, y: 0, width: image1.size.width, height: image1.size.height)
let bounds2 = CGRect(x:imageView2.origin.x, y:catPos, width:imageView2.size.width, height:imageView2.size.height)
_ = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
let ctx = CGContext(data: nil,
width: image1.cgImage!.width,
height: image1.cgImage!.height,
bitsPerComponent: image1.cgImage!.bitsPerComponent,
bytesPerRow: image1.cgImage!.bytesPerRow,
space: image1.cgImage!.colorSpace!,
bitmapInfo: bitmapInfo.rawValue)!
ctx.draw(image1.cgImage!, in: bounds1, byTiling: false)
ctx.setBlendMode(.normal) // one image over the other
ctx.draw(image2.cgImage!, in: bounds2, byTiling: false)
let finalImage = textToImage(drawText, inImage: UIImage(cgImage: ctx.makeImage()!), atPoint: imageView1)
return finalImage
}

Take a snapshot of current screen with Metal in swift

I tried:
let scale = UIScreen.mainScreen().scale
UIGraphicsBeginImageContextWithOptions(metalLayer.bounds.size, false, scale)
// metalLayer.renderInContext(UIGraphicsGetCurrentContext()!)
// self.view.layer ...
metalLayer.presentationLayer()!.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
But the result is an empty screenshot. Any help would be nice!
Please keep in mind that I want to take a snapshot of a CAMetalLayer.
To make a screenshot, you need to get MTLTexture of the frame buffer.
1. If you use MTKView:
let texture = view.currentDrawable!.texture
2. If you don't use MTKView
Here's what I would do - I would have a property which holds last drawable presented to the screen:
let lastDrawableDisplayed: CAMetalDrawable?
And then when you present drawable to the screen, I would update it:
let commandBuffer = commandQueue.commandBuffer()
commandBuffer.addCompletedHandler { buffer in
self.lastDrawableDisplayed = drawable
}
Now you whenever you need to take a screenshot, you can get a texture like this:
let texture = lastDrawableDisplayed.texture
Ok, now when you have MTLTexture you can convert it to CGImage and then to UIImage or NSImage.
Here's the code for OS X playground (MetalKit.MTLTextureLoader is not available for iOS playgrounds), in which I convert MTLTexture to CGImage
I made a small extension over MTLTexture for this.
import Metal
import MetalKit
import Cocoa
let device = MTLCreateSystemDefaultDevice()!
let textureLoader = MTKTextureLoader(device: device)
let path = "path/to/your/image.jpg"
let data = NSData(contentsOfFile: path)!
let texture = try! textureLoader.newTextureWithData(data, options: nil)
extension MTLTexture {
func bytes() -> UnsafeMutablePointer<Void> {
let width = self.width
let height = self.height
let rowBytes = self.width * 4
let p = malloc(width * height * 4)
self.getBytes(p, bytesPerRow: rowBytes, fromRegion: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)
return p
}
func toImage() -> CGImage? {
let p = bytes()
let pColorSpace = CGColorSpaceCreateDeviceRGB()
let rawBitmapInfo = CGImageAlphaInfo.NoneSkipFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue
let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
let selftureSize = self.width * self.height * 4
let rowBytes = self.width * 4
let provider = CGDataProviderCreateWithData(nil, p, selftureSize, nil)
let cgImageRef = CGImageCreate(self.width, self.height, 8, 32, rowBytes, pColorSpace, bitmapInfo, provider, nil, true, CGColorRenderingIntent.RenderingIntentDefault)!
return cgImageRef
}
}
if let imageRef = texture.toImage() {
let image = NSImage(CGImage: imageRef, size: NSSize(width: texture.width, height: texture.height))
}
For swift 4.0,
Just converting code provided by haawa
let lastDrawableDisplayed = metalView?.currentDrawable?.texture
if let imageRef = lastDrawableDisplayed?.toImage() {
let uiImage:UIImage = UIImage.init(cgImage: imageRef)
}
extension MTLTexture {
func bytes() -> UnsafeMutableRawPointer {
let width = self.width
let height = self.height
let rowBytes = self.width * 4
let p = malloc(width * height * 4)
self.getBytes(p!, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)
return p!
}
func toImage() -> CGImage? {
let p = bytes()
let pColorSpace = CGColorSpaceCreateDeviceRGB()
let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
let selftureSize = self.width * self.height * 4
let rowBytes = self.width * 4
let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
return
}
let provider = CGDataProvider(dataInfo: nil, data: p, size: selftureSize, releaseData: releaseMaskImagePixelData)
let cgImageRef = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: pColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: true, intent: CGColorRenderingIntent.defaultIntent)!
return cgImageRef
}
}
I didn't manage to get the accepted answer to work in Swift 4 / Metal 2 with XCode 9.1 on an iPhone 6s. Therefore I used a slightly different approach assuming lastDrawableDisplayed is saved as described in the accepted answer. Quick and dirty and without any exception handling:
let context = CIContext()
let texture = self.lastDrawableDisplayed!.texture
let cImg = CIImage(mtlTexture: texture, options: nil)!
let cgImg = context.createCGImage(cImg, from: cImg.extent)!
let uiImg = UIImage(cgImage: cgImg)
This is based on the documentation on the used CIImage Initializer:
init(mtlTexture:options:) Initializes an image object with data supplied by a Metal texture.
and CIImage Processing which describes how to create a CGImage with the use of CIContext:
CIContext() Create[s] a CIContext object (with default options) [...] context.createCGImage Render[s] the output image to a Core Graphics image that you can display or save to a file.
Hope that helps for anyone using Swift 4.
Edit: Additionally, I have multiple overlaying CAMetalLayer in my project and want to combine them into one single UIImage. Therefore it is needed to have references to the last CAMetalDrawable object of each layer. Before a new layer is added (and therefore used as the provider of nextDrawable()) I simply add the lastDrawableDisplayed to an array [CAMetalDrawable]. When "exporting" the layers I simply write all UIImages subsequently into a bitmap-based graphics context and get the final image with UIGraphicsGetImageFromCurrentImageContext().
Edit: If you are having trouble with orientation, try the following:
let uiImg = UIImage(cgImage: cgImg, scale: 1.0, orientation: UIImageOrientation.downMirrored)
MTLTexture's method toImage needs to release data's memory in release data callback:
let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
data.dealloc()
return
}
swift 4.2
extension MTLTexture {
func bytes() -> UnsafeMutableRawPointer {
let width = self.width
let height = self.height
let rowBytes = self.width * 4
let p = malloc(width * height * 4)
self.getBytes(p!, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)
return p!
}
func toImage() -> CGImage? {
let p = bytes()
let pColorSpace = CGColorSpaceCreateDeviceRGB()
let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
let selftureSize = self.width * self.height * 4
let rowBytes = self.width * 4
let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
return
}
let provider = CGDataProvider(dataInfo: nil, data: p, size: selftureSize, releaseData: releaseMaskImagePixelData)
let cgImageRef = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: pColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: true, intent: CGColorRenderingIntent.defaultIntent)!
return cgImageRef
}
}