Search code examples
iosswiftuiimageuigraphicscontext

Tint is inverting colors in swift


I'm trying out this code to tint color a UIImage but it seems to be inverting the colors - coloring the background and turning the black stroke to white.

How do I have it color the black stroke lines of an image and leave the white background alone? Here's a playground sample where I try one technique as well as another one suggested by the first answer.

Playground Code

 extension UIImage {

    func tint(color: UIColor, blendMode: CGBlendMode = CGBlendMode.Normal) -> UIImage
    {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        let context = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(context, 0, self.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        CGContextSetBlendMode(context, blendMode);
        let rect = CGRectMake(0, 0, self.size.width, self.size.height);
        CGContextClipToMask(context, rect, self.CGImage);
        color.setFill()
        CGContextFillRect(context, rect);
        let newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return newImage;
    }

    func tint2(color: UIColor, blendMode: CGBlendMode) -> UIImage
    {
        let drawRect = CGRectMake(0.0, 0.0, size.width, size.height)
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        let context = UIGraphicsGetCurrentContext()
        CGContextClipToMask(context, drawRect, CGImage)
        color.setFill()
        UIRectFill(drawRect)
        drawInRect(drawRect, blendMode: blendMode, alpha: 1.0)
        let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return tintedImage
    }

}

let stringUrl = "https://s3.amazonaws.com/neighbor-chat-assets/icons/avatar1000.png"

let url = NSURL(string: stringUrl)

let data = NSData(contentsOfURL: url!)

var originalimage = UIImage(data: data!)

var image = originalimage!.imageWithRenderingMode(.AlwaysTemplate).tint(UIColor.blueColor(), blendMode:.Normal)
var image2 = originalimage!.imageWithRenderingMode(.AlwaysTemplate).tint2(UIColor.blueColor(), blendMode:.Normal)

Solution

  • This should work, I just tested it with a png and transparent background

    func tint(color: UIColor, blendMode: CGBlendMode = CGBlendMode.Normal) -> UIImage
    {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        let context = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(context, 0, self.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        CGContextSetBlendMode(context, blendMode);
        let rect = CGRectMake(0, 0, self.size.width, self.size.height);
        CGContextClipToMask(context, rect, self.CGImage);
        color.setFill()
        CGContextFillRect(context, rect);
        let newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return newImage;
    }
    

    Also see this post for further information. The alternative would be to wrap your image in a UIImageView with UIImageRenderingMode.AlwaysTemplate rendering mode, and then set the tintColor property.

    EDIT

    So this is the function you can use to first mask out certain colors if you don't have an alpha channel in an image

    func tint(color: UIColor, blendMode: CGBlendMode = CGBlendMode.Normal, colorToMask: UIColor = UIColor.blackColor()) -> UIImage
    {
        var fRed : CGFloat = 0
        var fGreen : CGFloat = 0
        var fBlue : CGFloat = 0
        var fAlpha: CGFloat = 0
        colorToMask.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha)
    
        let rect = CGRectMake(0, 0, self.size.width, self.size.height);
        var colorMasking : [CGFloat] = [fRed,fRed,fGreen,fGreen,fBlue,fBlue]
        let newImageRef = CGImageCreateWithMaskingColors(self.CGImage!, &colorMasking)
    
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        let context = UIGraphicsGetCurrentContext();
    
        CGContextTranslateCTM(context, 0, self.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        CGContextSetBlendMode(context, blendMode);
        CGContextClipToMask(context, rect, newImageRef!);
        color.setFill()
        CGContextFillRect(context, rect);
    
        let newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    
        return newImage;
    }
    

    This function first creates a mask depending on the specified color, and clips the context to this mask.

    EDIT #2

    As stated in the comments, I also didn't manage to mask out the white color, I tried UInt8 values (i.e. 255) and float values (i.e. 1.0), but it still didn't work. So the only solution I could come up with was to invert the image. Here is the function:

    func negativeImage() -> UIImage {
    
        let coreImage = MYImage(CGImage: self.CGImage!)
        let filter = CIFilter(name: "CIColorInvert", withInputParameters: ["inputImage" : coreImage])!
        let result = filter.outputImage!
        let context = CIContext(options:nil)
        let cgimg : CGImageRef = context.createCGImage(result, fromRect: result.extent)
    
        let bmpcontext = CGBitmapContextCreate(nil, Int(self.size.width), Int(self.size.height), 8, Int(self.size.width)*4, CGColorSpaceCreateDeviceRGB(),CGImageAlphaInfo.NoneSkipLast.rawValue)
        CGContextDrawImage(bmpcontext, CGRectMake(0, 0, self.size.width, self.size.height), cgimg)
        let newImgRef = CGBitmapContextCreateImage(bmpcontext)!
        return UIImage(CGImage: newImgRef)
    }
    

    You can use it like this

    var image : UIImage = ...
    image = image.negativeImage()
    image = image.tint(UIColor.redColor())
    

    Here are some general notes

    • CGImageCreateWithMaskingColors requires a CGImage with NO alpha channel, that's why I use the flag CGImageAlphaInfo.NoneSkipLast in the negativeImage function in order to remove the alpha channel again
    • There is absolutely no error checking included in this, you should at least check whether there is an alpha channel included within the tint function using CGImageGetAlphaInfo
    • You can either have the masked out parts (white in our example) transparent (by setting false in the UIGraphicsBeginImageContextWithOptions command within the tint function), or a specific color by setting true, which is black by default (you would need to paint before clipping to the mask).