How would I tint an image programmatically on iOS?
Asked Answered
B

13

87

I would like to tint an image with a color reference. The results should look like the Multiply blending mode in Photoshop, where whites would be replaced with tint:

alt text

I will be changing the color value continuously.

Follow up: I would put the code to do this in my ImageView's drawRect: method, right?

As always, a code snippet would greatly aid in my understanding, as opposed to a link.

Update: Subclassing a UIImageView with the code Ramin suggested.

I put this in viewDidLoad: of my view controller:

[self.lena setImage:[UIImage imageNamed:kImageName]];
[self.lena setOverlayColor:[UIColor blueColor]];
[super viewDidLoad];

I see the image, but it is not being tinted. I also tried loading other images, setting the image in IB, and calling setNeedsDisplay: in my view controller.

Update: drawRect: is not being called.

Final update: I found an old project that had an imageView set up properly so I could test Ramin's code and it works like a charm!

Final, final update:

For those of you just learning about Core Graphics, here is the simplest thing that could possibly work.

In your subclassed UIView:

- (void)drawRect:(CGRect)rect {

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetFillColor(context, CGColorGetComponents([UIColor colorWithRed:0.5 green:0.5 blue:0 alpha:1].CGColor)); // don't make color too saturated

    CGContextFillRect(context, rect); // draw base

    [[UIImage imageNamed:@"someImage.png"] drawInRect: rect blendMode:kCGBlendModeOverlay alpha:1.0]; // draw image
}
Bedside answered 12/7, 2009 at 23:38 Comment(3)
I added the snippet before posting to some old drawRect code I had sitting around and it worked fine. May want to try it without the [super viewDidLoad] call (or move it above). Also double-check to make sure whoever is allocating this object is allocating the derived version not vanilla UIImageView (i.e if it's load in a nib, the allocator, etc).Jacobba
Another suggestion if the above doesn't work: instead of subclassing UIImageView subclass a plain UIView and add both overlayColor and a UIImage property called 'image' that you can set. Then put the drawRect in there. The drawRect code doesn't care whether the 'image' value comes from UIImageView or from a property you've defined.Jacobba
Won't this fail if the image is with alpha? What if I would like to tint only drawn portion of the image? (Just like UIButton does?)Reference
J
45

First you'll want to subclass UIImageView and override the drawRect method. Your class needs a UIColor property (let's call it overlayColor) to hold the blend color and a custom setter that forces a redraw when the color changes. Something like this:

- (void) setOverlayColor:(UIColor *)newColor {
   if (overlayColor)
     [overlayColor release];

   overlayColor = [newColor retain];
   [self setNeedsDisplay]; // fires off drawRect each time color changes
}

In the drawRect method you'll want to draw the image first then overlay it with a rectangle filled with the color you want along with the proper blending mode, something like this:

- (void) drawRect:(CGRect)area
{
  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSaveGState(context);

  // Draw picture first
  //
  CGContextDrawImage(context, self.frame, self.image.CGImage);

  // Blend mode could be any of CGBlendMode values. Now draw filled rectangle
  // over top of image.
  //
  CGContextSetBlendMode (context, kCGBlendModeMultiply);
  CGContextSetFillColor(context, CGColorGetComponents(self.overlayColor.CGColor));      
  CGContextFillRect (context, self.bounds);
  CGContextRestoreGState(context);
}

Ordinarily to optimize the drawing you would restrict the actual drawing to only the area passed in to drawRect, but since the background image has to be redrawn each time the color changes it's likely the whole thing will need refreshing.

To use it create an instance of the object then set the image property (inherited from UIImageView) to the picture and overlayColor to a UIColor value (the blend levels can be adjusted by changing the alpha value of the color you pass down).

Jacobba answered 13/7, 2009 at 6:54 Comment(6)
Follow-up suggestions attached to original request (above).Jacobba
should add CGContextSaveGState(context); before changing the blend mode or you get a gstack underflow.Bedside
D'oh, thanks. Copy/paste error. I've fixed it in the code snippet.Jacobba
(As also stated in another answer here) Special Considerations The UIImageView class is optimized to draw its images to the display. UIImageView will not call drawRect: a subclass. If your subclass needs custom drawing code, it is recommended you use UIView as the base class. This means you cannot subclass UIImageView and expect drawRect to be called, at all.Northrup
Hi, unfortunately I tried the code and it didn't work :/. See answer below by @mpstx, which says drawRect won't get called from a UIImageView and instead, you should use an UIView (with a UIImage) which worked fine for me.Cooperation
@Jacobba Does this change the original image? I want a new UIImage with applied filter color. How do I do so?Terrill
S
77

In iOS7, they've introduced tintColor property on UIImageView and renderingMode on UIImage. To tint an UIImage on iOS7, all you have to do is:

UIImageView* imageView = …
UIImage* originalImage = …
UIImage* imageForRendering = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
imageView.image = imageForRendering;
imageView.tintColor = [UIColor redColor]; // or any color you want to tint it with
Speaking answered 1/10, 2013 at 20:18 Comment(1)
This is definitely the best and easiest way to go on iOS 7 if this is the tint behavior you're looking for. But this code applies a tint as if you were using a 100% opaque "Overlay" blending mode in Photoshop, not the "Multiply" blending mode the original question was looking for.Pinchas
J
45

First you'll want to subclass UIImageView and override the drawRect method. Your class needs a UIColor property (let's call it overlayColor) to hold the blend color and a custom setter that forces a redraw when the color changes. Something like this:

- (void) setOverlayColor:(UIColor *)newColor {
   if (overlayColor)
     [overlayColor release];

   overlayColor = [newColor retain];
   [self setNeedsDisplay]; // fires off drawRect each time color changes
}

In the drawRect method you'll want to draw the image first then overlay it with a rectangle filled with the color you want along with the proper blending mode, something like this:

- (void) drawRect:(CGRect)area
{
  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSaveGState(context);

  // Draw picture first
  //
  CGContextDrawImage(context, self.frame, self.image.CGImage);

  // Blend mode could be any of CGBlendMode values. Now draw filled rectangle
  // over top of image.
  //
  CGContextSetBlendMode (context, kCGBlendModeMultiply);
  CGContextSetFillColor(context, CGColorGetComponents(self.overlayColor.CGColor));      
  CGContextFillRect (context, self.bounds);
  CGContextRestoreGState(context);
}

Ordinarily to optimize the drawing you would restrict the actual drawing to only the area passed in to drawRect, but since the background image has to be redrawn each time the color changes it's likely the whole thing will need refreshing.

To use it create an instance of the object then set the image property (inherited from UIImageView) to the picture and overlayColor to a UIColor value (the blend levels can be adjusted by changing the alpha value of the color you pass down).

Jacobba answered 13/7, 2009 at 6:54 Comment(6)
Follow-up suggestions attached to original request (above).Jacobba
should add CGContextSaveGState(context); before changing the blend mode or you get a gstack underflow.Bedside
D'oh, thanks. Copy/paste error. I've fixed it in the code snippet.Jacobba
(As also stated in another answer here) Special Considerations The UIImageView class is optimized to draw its images to the display. UIImageView will not call drawRect: a subclass. If your subclass needs custom drawing code, it is recommended you use UIView as the base class. This means you cannot subclass UIImageView and expect drawRect to be called, at all.Northrup
Hi, unfortunately I tried the code and it didn't work :/. See answer below by @mpstx, which says drawRect won't get called from a UIImageView and instead, you should use an UIView (with a UIImage) which worked fine for me.Cooperation
@Jacobba Does this change the original image? I want a new UIImage with applied filter color. How do I do so?Terrill
M
26

Just a quick clarification (after some research on this topic). The Apple doc here clearly states that:

The UIImageView class is optimized to draw its images to the display. UIImageView does not call the drawRect: method of its subclasses. If your subclass needs to include custom drawing code, you should subclass the UIView class instead.

so don't even waste any time attempting to override that method in a UIImageView subclass. Start with UIView instead.

Merell answered 27/5, 2010 at 15:51 Comment(3)
Oh, how I wish I'd know this six months ago. They should put the warning in 72 point type, red with the blink tag.Bedside
I know you mean... I lost a half day messing with it.Merell
thanks @mpstx...Also put that documentation line in the answer in quotes...Apple should have done the same in documentation...:-)Turgeon
B
26

I wanted to tint an image with alpha and I created the following class. Please let me know if you find any problems with it.

I have named my class CSTintedImageView and it inherits from UIView since UIImageView does not call the drawRect: method, like mentioned in previous replies. I have set a designated initializer similar to the one found in the UIImageView class.

Usage:

CSTintedImageView * imageView = [[CSTintedImageView alloc] initWithImage:[UIImage imageNamed:@"image"]];
imageView.tintColor = [UIColor redColor];

CSTintedImageView.h

@interface CSTintedImageView : UIView

@property (strong, nonatomic) UIImage * image;
@property (strong, nonatomic) UIColor * tintColor;

- (id)initWithImage:(UIImage *)image;

@end

CSTintedImageView.m

#import "CSTintedImageView.h"

@implementation CSTintedImageView

@synthesize image=_image;
@synthesize tintColor=_tintColor;

- (id)initWithImage:(UIImage *)image
{
    self = [super initWithFrame:CGRectMake(0, 0, image.size.width, image.size.height)];

    if(self)
    {
        self.image = image;

        //set the view to opaque
        self.opaque = NO;
    }

    return self;
}

- (void)setTintColor:(UIColor *)color
{
    _tintColor = color;

    //update every time the tint color is set
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();    

    //resolve CG/iOS coordinate mismatch
    CGContextScaleCTM(context, 1, -1);
    CGContextTranslateCTM(context, 0, -rect.size.height);

    //set the clipping area to the image
    CGContextClipToMask(context, rect, _image.CGImage);

    //set the fill color
    CGContextSetFillColor(context, CGColorGetComponents(_tintColor.CGColor));
    CGContextFillRect(context, rect);    

    //blend mode overlay
    CGContextSetBlendMode(context, kCGBlendModeOverlay);

    //draw the image
    CGContextDrawImage(context, rect, _image.CGImage);    
}

@end
Blenny answered 2/12, 2011 at 4:30 Comment(5)
Thank you. This was the first solution in this question that actually supported image transparency properly.Instinctive
When I try to use this answer, my background (the area that should be transparent) is black instead. Do you know what I could be doing wrong here?Paoting
Nevermind! (I had a background color set in the storyboard! D'oh!) This solution is awesome!!!Paoting
This solution works well, although I got better results by rendering the image first, then filling the color on top with kCGBlendModeColor.Lesalesak
the drawRect: method is really the "meat" of this solution. The rest is a matter of taste/style. Thanks! Worked for me!Accentor
A
5

This could be very useful: PhotoshopFramework is one powerful library to manipulate images on Objective-C. This was developed to bring the same functionalities that Adobe Photoshop users are familiar. Examples: Set colors using RGB 0-255, apply blend filers, transformations...

Is open source, here is the project link: https://sourceforge.net/projects/photoshopframew/

Athome answered 22/10, 2009 at 20:44 Comment(3)
Looks interesting, but what are you going to change the name to when Adobe's lawyers find out about it?Tush
We'll figure out later. Is not a commercial product anyway, is some kind of "codename". Also is an internal library.Athome
+1 for suggesting a library (even if it is a little self promotion), and not giving steps to reinvent the wheel.Brooksbrookshire
I
4
UIImage * image = mySourceImage;
UIColor * color = [UIColor yellowColor];  
UIGraphicsBeginImageContext(image.size);
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height) blendMode:kCGBlendModeNormal alpha:1];
UIBezierPath * path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, image.size.width, image.size.height)];
[color setFill];
[path fillWithBlendMode:kCGBlendModeMultiply alpha:1]; //look up blending modes for your needs
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//use newImage for something
Itinerate answered 7/2, 2015 at 4:25 Comment(1)
I needed to tint an UIImage, but not to use it inside of an UIImageView, but as a background image for an UINavigationBar, and your code did the trick. Thanks.Upstairs
S
2

For those of you who try to subclass an UIImageView class and get stuck at "drawRect: is not being called", note that you should subclass an UIView class instead, because for UIImageView classes, the "drawRect:" method is not called. Read more here: drawRect not being called in my subclass of UIImageView

Stunning answered 25/11, 2010 at 20:58 Comment(0)
R
2

Here is another way to implement image tinting, especially if you are already using QuartzCore for something else. This was my answer for a similar question.

Import QuartzCore:

#import <QuartzCore/QuartzCore.h>

Create transparent CALayer and add it as a sublayer for the image you want to tint:

CALayer *sublayer = [CALayer layer];
[sublayer setBackgroundColor:[UIColor whiteColor].CGColor];
[sublayer setOpacity:0.3];
[sublayer setFrame:toBeTintedImage.frame];
[toBeTintedImage.layer addSublayer:sublayer];

Add QuartzCore to your projects Framework list (if it isn't already there), otherwise you'll get compiler errors like this:

Undefined symbols for architecture i386: "_OBJC_CLASS_$_CALayer"
Rebak answered 14/9, 2013 at 18:48 Comment(0)
C
0

The only thing I can think of would be to create a rectangular mostly transparent view with the desired color and lay it over your image view by adding it as a subview. I'm not sure if this will really tint the image in the way you imagine though, I'm not sure how you would hack into an image and selectively replace certain colors with others... sounds pretty ambitious to me.

For example:

UIImageView *yourPicture = (however you grab the image);
UIView *colorBlock = [[UIView alloc] initWithFrame:yourPicture.frame];
//Replace R G B and A with values from 0 - 1 based on your color and transparency
colorBlock.backgroundColor = [UIColor colorWithRed:R green:G blue:B alpha:A];
[yourPicture addSubView:colorBlock];

Documentation for UIColor:

colorWithRed:green:blue:alpha:

Creates and returns a color object using the specified opacity and RGB component values.

+ (UIColor *)colorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha

Parameters

red    - The red component of the color object, specified as a value from 0.0 to 1.0.

green  - The green component of the color object, specified as a value from 0.0 to 1.0.

blue   - The blue component of the color object, specified as a value from 0.0 to 1.0.



alpha  - The opacity value of the color object, specified as a value from 0.0 to 1.0.

Return Value

The color object. The color information represented by this object is in the device RGB colorspace. 
Coloratura answered 13/7, 2009 at 2:26 Comment(1)
Core Graphics seems to be able to apply bending modes. How, is the question.Bedside
P
0

Also you might want to consider caching the composited image for performance and just rendering it in drawRect:, then updated it if a dirty flag is indeed dirty. While you might be changing it often, there may be cases where draws are coming in and you're not dirty, so you can simply refresh from the cache. If memory is more of an issue than performance, you can ignore this :)

Pitchdark answered 13/7, 2009 at 16:21 Comment(1)
That's what the underlying CALayer does for you automatically. No need to cache the view drawing manually.Thracian
V
0

I have a library I open-sourced for this: ios-image-filters

Veach answered 27/8, 2011 at 7:42 Comment(0)
R
0

For Swift 2.0,

    let image: UIImage! = UIGraphicsGetImageFromCurrentImageContext()

    imgView.image = imgView.image!.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)

    imgView.tintColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 

51/255.0, alpha: 1.0)
Rudolphrudwik answered 1/3, 2016 at 11:3 Comment(0)
W
0

I made macros for this purpose:

#define removeTint(view) \
if ([((NSNumber *)[view.layer valueForKey:@"__hasTint"]) boolValue]) {\
for (CALayer *layer in [view.layer sublayers]) {\
if ([((NSNumber *)[layer valueForKey:@"__isTintLayer"]) boolValue]) {\
[layer removeFromSuperlayer];\
break;\
}\
}\
}

#define setTint(view, tintColor) \
{\
if ([((NSNumber *)[view.layer valueForKey:@"__hasTint"]) boolValue]) {\
removeTint(view);\
}\
[view.layer setValue:@(YES) forKey:@"__hasTint"];\
CALayer *tintLayer = [CALayer new];\
tintLayer.frame = view.bounds;\
tintLayer.backgroundColor = [tintColor CGColor];\
[tintLayer setValue:@(YES) forKey:@"__isTintLayer"];\
[view.layer addSublayer:tintLayer];\
}

To use, simply just call:

setTint(yourView, yourUIColor);
//Note: include opacity of tint in your UIColor using the alpha channel (RGBA), e.g. [UIColor colorWithRed:0.5f green:0.0 blue:0.0 alpha:0.25f];

When removing the tint simply call:

removeTint(yourView);
Wheelchair answered 21/6, 2018 at 4:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.