Search code examples
iosobjective-cswiftuiimageuicolor

How to tint only one part of the UIImage with alpha channel - PNG (replacing color)?


I have this transparent image:

enter image description here

My goal is to change the "ME!" parts color. Either tint only the last 3rd of the image, or replace the blue color with the new color.

Expected result after color change:

enter image description here

Unfortunately neither worked for me. To change the specific color I tried this: LINK, but as the documentation says, this works only without alpha channel!

Then I tried this one: LINK, but this actually does nothing, no tint or anything.

Is there any other way to tint only one part of the color or just replace a specific color?

I know I could slice the image in two parts, but I hope there is another way.


Solution

  • It turns out to be surprisingly complicated—you’d think you could do it in one pass with CoreGraphics blend modes, but from pretty extensive experimentation I haven’t found such a way that doesn’t mangle the alpha channel or the coloration. The solution I landed on is this:

    1. Start with a grayscale/alpha version of your image rather than a colored one: black in the areas you don’t want tinted, white in the areas you do
    2. Create an image context with your image’s dimensions
    3. Fill that context with black
    4. Draw the image into the context
    5. Get a new image (let’s call it “the-image-over-black”) from that context
    6. Clear the context (so you can use it again)
    7. Fill the context with the color you want the tinted part of your image to be
    8. Draw the-image-over-black into the context with the “multiply” blend mode
    9. Draw the original image into the context with the “destination in” blend mode
    10. Get your final image from the context

    The reason this works is because of the combination of blend modes. What you’re doing is creating a fully-opaque black-and-white image (step 5), then multiplying it by your final color (step 8), which gives you a fully opaque black-and-your-final-color image. Then, you take the original image, which still has its alpha channel, and draw it with the “destination in” blend mode which takes the color from the black-and-your-color image and the alpha channel from the original image. The result is a tinted image with the original brightness values and alpha channel.

    Objective-C

    - (UIImage *)createTintedImageFromImage:(UIImage *)originalImage color:(UIColor *)desiredColor {
        CGSize imageSize = originalImage.size;
        CGFloat imageScale = originalImage.scale;
        CGRect contextBounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
    
        UIGraphicsBeginImageContextWithOptions(imageSize, NO /* not opaque */, imageScale); // 2
        [[UIColor blackColor] setFill]; // 3a
        UIRectFill(contextBounds); // 3b
        [originalImage drawAtPoint:CGPointZero]; // 4
        UIImage *imageOverBlack = UIGraphicsGetImageFromCurrentImageContext(); // 5
        CGContextClear(UIGraphicsGetCurrentImageContext()); // 6
        [desiredColor setFill]; // 7a
        UIRectFill(contextBounds); // 7b
        [imageOverBlack drawAtPoint:CGPointZero blendMode:kCGBlendModeMultiply alpha:1]; // 8
        [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeDestinationIn alpha:1]; // 9
        finalImage = UIGraphicsGetImageFromCurrentContext(); // 10
        UIGraphicsEndImageContext();
    
        return finalImage;
    }
    

    Swift 4

    func createTintedImageFromImage(originalImage: UIImage, desiredColor: UIColor) -> UIImage {
        let imageSize = originalImage.size
        let imageScale = originalImage.scale
        let contextBounds = CGRect(origin: .zero, size: imageSize)
    
        UIGraphicsBeginImageContextWithOptions(imageSize, false /* not opaque */, imageScale) // 2
    
        defer { UIGraphicsEndImageContext() }
    
        UIColor.black.setFill() // 3a
        UIRectFill(contextBounds) // 3b
        originalImage.draw(at: .zero) // 4
        guard let imageOverBlack = UIGraphicsGetImageFromCurrentImageContext() else { return originalImage } // 5
        desiredColor.setFill() // 7a
        UIRectFill(contextBounds) // 7b
    
        imageOverBlack.draw(at: .zero, blendMode: .multiply, alpha: 1) // 8
        originalImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1) // 9
    
        guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return originalImage } // 10
    
        return finalImage
    }