How to tint a transparent PNG image in iPhone?

41,859

Solution 1

Update: Here is a Gist for a Swift UIColor extension using the code below.


If you have a greyscale image and want white become the tinting color, kCGBlendModeMultiply is the way to go. With this method, you cannot have highlights lighter than your tinting color.

On the contrary, if you have either a non-greyscale image, OR you have highlights and shadows that should be preserved, the blend mode kCGBlendModeColor is the way to go. White will stay white and black will stay black as the lightness of the image is preserved. This mode is just made for tinting - it is the same as Photoshop's Color layer blend mode (disclaimer: slightly differing results may happen).

Note that tinting alpha-pixels does not work correctly neither in iOS nor in Photoshop - half-transparent black pixels would not stay black. I updated the answer below to work around that issue, it took quite a long time to find out.

You can also use one of the blend modes kCGBlendModeSourceIn/DestinationIn instead of CGContextClipToMask.

If you want to create a UIImage, each of the following code sections can be surrounded by the following code:

UIGraphicsBeginImageContextWithOptions (myIconImage.size, NO, myIconImage.scale); // for correct resolution on retina, thanks @MobileVet
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextTranslateCTM(context, 0, myIconImage.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

CGRect rect = CGRectMake(0, 0, myIconImage.size.width, myIconImage.size.height);

// image drawing code here

UIImage *coloredImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

So here's the code for tinting a transparent image with kCGBlendModeColor:

// draw black background to preserve color of transparent pixels
CGContextSetBlendMode(context, kCGBlendModeNormal);
[[UIColor blackColor] setFill];
CGContextFillRect(context, rect);

// draw original image
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextDrawImage(context, rect, myIconImage.CGImage);

// tint image (loosing alpha) - the luminosity of the original image is preserved
CGContextSetBlendMode(context, kCGBlendModeColor);
[tintColor setFill];
CGContextFillRect(context, rect);

// mask by alpha values of original image
CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
CGContextDrawImage(context, rect, myIconImage.CGImage);

If your image has no half-transparent pixels, you could also do it the other way around with kCGBlendModeLuminosity:

// draw tint color
CGContextSetBlendMode(context, kCGBlendModeNormal);
[tintColor setFill];
CGContextFillRect(context, rect);

// replace luminosity of background (ignoring alpha)
CGContextSetBlendMode(context, kCGBlendModeLuminosity);
CGContextDrawImage(context, rect, myIconImage.CGImage);

// mask by alpha values of original image
CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
CGContextDrawImage(context, rect, myIconImage.CGImage);

If you don't care for luminosity, as you just have got an image with an alpha channel that should be tinted with a color, you can do it in a more efficient way:

// draw tint color
CGContextSetBlendMode(context, kCGBlendModeNormal);
[tintColor setFill];
CGContextFillRect(context, rect);

// mask by alpha values of original image
CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
CGContextDrawImage(context, rect, myIconImage.CGImage);

or the other way around:

// draw alpha-mask
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextDrawImage(context, rect, myIconImage.CGImage);

// draw tint color, preserving alpha values of original image
CGContextSetBlendMode(context, kCGBlendModeSourceIn);
[tintColor setFill];
CGContextFillRect(context, rect);

Have fun!

Solution 2

I had most success with this method, because the others I tried caused distorted colors for semi-transparent pixels for certain color-combinations. This should also be a bit better on the performance side.

+ (UIImage *) imageNamed:(NSString *) name withTintColor: (UIColor *) tintColor {

    UIImage *baseImage = [UIImage imageNamed:name];

    CGRect drawRect = CGRectMake(0, 0, baseImage.size.width, baseImage.size.height);

    UIGraphicsBeginImageContextWithOptions(baseImage.size, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextTranslateCTM(context, 0, baseImage.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    // draw original image
    CGContextSetBlendMode(context, kCGBlendModeNormal);
    CGContextDrawImage(context, drawRect, baseImage.CGImage);

    // draw color atop
    CGContextSetFillColorWithColor(context, tintColor.CGColor);
    CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
    CGContextFillRect(context, drawRect);

    UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return tintedImage;
}

Solution 3

After searching around, the best solution I've come to thus far is to use a combination of blend mode and the clipping mask to achieve colorizing/tinting a transparent PNG:

CGContextSetBlendMode (context, kCGBlendModeMultiply);

CGContextDrawImage(context, rect, myIconImage.CGImage);

CGContextClipToMask(context, rect, myIconImage.CGImage);

CGContextSetFillColorWithColor(context, tintColor);

CGContextFillRect(context, rect);

Solution 4

I can get results very close to the tint in the Apple navigation bar by using kCGBlendModeOverlay. Taking excelent @fabb answer and combining @omz approach in this post https://stackoverflow.com/a/4684876/229019 I came with this solution that helds the results I was expecting:

- (UIImage *)tintedImageUsingColor:(UIColor *)tintColor;
{
    UIGraphicsBeginImageContextWithOptions (self.size, NO, [[UIScreen mainScreen] scale]);

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);

    // draw original image
    [self drawInRect:rect blendMode:kCGBlendModeNormal alpha:1.0f];

    // tint image (loosing alpha). 
    // kCGBlendModeOverlay is the closest I was able to match the 
    // actual process used by apple in navigation bar 
    CGContextSetBlendMode(context, kCGBlendModeOverlay);
    [tintColor setFill];
    CGContextFillRect(context, rect);

    // mask by alpha values of original image
    [self drawInRect:rect blendMode:kCGBlendModeDestinationIn alpha:1.0f];

    UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return tintedImage;
}

Here's an example tinting several grayscale images with transparency:

Rendered Example:

  • The first line is the apple toolbar tinted [UIColor orangeColor].
  • The second line is the same gradient tinted in several colors starting with clear color (= the actual gradient) and ending with the same orange.
  • The third is a simple circle with transparency (the linen is the background color)
  • The forth line is a complex dark noisy texture

Solution 5

In iOS7, they've introduced tintColor property on UIImageView and renderingMode on UIImage. See my example at https://stackoverflow.com/a/19125120/1570970

Share:
41,859
anna
Author by

anna

I'm a native iOS developer with a focus on UI/UX.

Updated on July 05, 2022

Comments

  • anna
    anna almost 2 years

    I know it's possible to tint a rectangular image by drawing a CGContextFillRect over it and setting the blend mode. However, I can't figure out how to do a tint on a transparent image such as an icon. It must be possible since the SDK does it itself on tab-bars in such. Would anyone be able to provide a snippet?

    UPDATE:

    Lots of great suggestions have been given for this problem since I originally asked. Be sure to read through all the answers to figure out what suits you best.

    UPDATE (Apr 30, 2015):

    With iOS 7.0, I can now just do the following, which would satisfy the needs of my original question. But if you have more complicated cases, check out all the answers.

    UIImage *iconImage = [[UIImage imageNamed:@"myImageName"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];    
    UIImageView *icon = [[UIImageView alloc] initWithImage:iconImage];
    icon.tintColor = [UIColor redColor];
    
  • Ziconic
    Ziconic about 12 years
    This is fantastic and saved me tons of time! I love the technique of using kCGBlendModeDestinationIn to restore the original alpha of the image that you are manipulating. Way easier than dealing with clipping masks.
  • Todd Lehman
    Todd Lehman almost 12 years
    I tried everything else I could think of before finding this, and this was the only thing that worked. Thank you, @anna!
  • C. Lee
    C. Lee over 11 years
    This worked but flipped my image, so I used this answer from Cliff Viegas to fix that: stackoverflow.com/questions/506622/…
  • Gottfried
    Gottfried almost 11 years
    worked great. the above solution left a 1 pixel border for me for some strange reason.
  • jwarrent
    jwarrent over 10 years
    This worked great for me. I would just add that changing the first line to: UIGraphicsBeginImageContextWithOptions (self.size, NO, [[UIScreen mainScreen] scale]); allowed the image to retain a retina formatting, if applicable.
  • MobileVet
    MobileVet over 10 years
    This is brilliant and thorough... however it lacks one tiny option that ensures quality output on Retina displays---- UIGraphicsBeginImageContextWithOptions (myIconImage.size, NO, [[UIScreen mainScreen] scale]);
  • HotJard
    HotJard over 10 years
    Or set retina scale like so UIGraphicsBeginImageContextWithOptions(self.size, 0, self.scale) in category
  • malhal
    malhal about 10 years
    This would be more appropriate: UIGraphicsBeginImageContextWithOptions (self.size, NO, self.scale);
  • Zev Eisenberg
    Zev Eisenberg almost 9 years
    This does not seem to work with colors whose alpha component is less than 1. Do you know a way to fix that?
  • Zev Eisenberg
    Zev Eisenberg almost 9 years
    I got it working with this code from github.com/vilanovi/UIImage-Additions/blob/master/… : CGContextSetBlendMode(context, kCGBlendModeNormal); CGContextDrawImage(context, rect, self.CGImage); CGContextSetBlendMode(context, kCGBlendModeSourceIn); [color setFill]; CGContextFillRect(context, rect);