Reputation: 3198
I have a grayscale image which I want to use for drawing Cocoa controls. The image has various levels of gray. Where it is darkest, I want it to draw a specified tint color darkest. I want it to be transparent where the source image is white.
Basically, I want to reproduce the behavior of tintColor seen in UINavigationBar on the iPhone.
So far, I have explored several options:
Draw the tint color over the grayscale image using SourceOver composition -> This requires a non-opaque tint color -> The result comes out much darker than desired
Use a CIMultiplyCompositing CIFilter to tint the image -> I can't [CIImage drawAtPoint:fromRect:operation:fraction:] to draw only part of the image. The same works fine with NSImage -> I get occasional crashes which I cannot make sense of
Transform the grayscale image into a mask. I.e. Black should be opaque. White should be transparent. Gray should have intermediate alpha values. -> This would seem to be the best solution -> Try as I might, I cannot achieve this.
Upvotes: 32
Views: 17836
Reputation: 5566
Just a rewrite from deprecated - (void)lockFocus;
to imageWithSize:flipped:drawingHandler:
@interface NSImage(Additions)
- (NSImage *)imageTintedWithColor:(NSColor *)tint;
@end
@implementation NSImage(Additions)
- (NSImage *)imageTintedWithColor:(NSColor *)tint
{
NSImage *copy = [self copy]; // we need to break block retain cycle
NSImage *image = [NSImage imageWithSize:copy.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
[tint set];
NSRect imageRect = {NSZeroPoint, [copy size]};
[copy drawInRect:imageRect];
NSRectFillUsingOperation(imageRect, NSCompositingOperationSourceIn);
return YES;
}];
return image;
}
Upvotes: 1
Reputation: 2859
As of macOS 13.0, lockFocus
is deprecated.
I made it work like this using symbol config:
let config = NSImage.SymbolConfiguration(paletteColors: [.systemTeal, .systemGray])
let nuImage = image.withSymbolConfiguration(config)
Upvotes: 1
Reputation: 3883
Swift 5 version that also handles the alpha component of the tint color.
I use this to support dark mode with multiple icon states by converting template icons to different colors and transparency levels. For example, you could pass NSColor(white: 0, alpha: 0.5)
to get a dimmed version of an icon for light mode, and NSColor(white: 1, alpha: 0.5)
to get a dimmed version for dark mode.
func tintedImage(_ image: NSImage, color: NSColor) -> NSImage {
let newImage = NSImage(size: image.size)
newImage.lockFocus()
// Draw with specified transparency
let imageRect = NSRect(origin: .zero, size: image.size)
image.draw(in: imageRect, from: imageRect, operation: .sourceOver, fraction: color.alphaComponent)
// Tint with color
color.withAlphaComponent(1).set()
imageRect.fill(using: .sourceAtop)
newImage.unlockFocus()
return newImage
}
Upvotes: 2
Reputation: 1292
The above solution didn't work for me. But this much easier solution works great for me
- (NSImage *)imageTintedWithColor:(NSColor *)tint
{
NSImage *image = [self copy];
if (tint) {
[image lockFocus];
[tint set];
NSRect imageRect = {NSZeroPoint, [image size]};
NSRectFillUsingOperation(imageRect, NSCompositeSourceIn);
[image unlockFocus];
}
return image;
}
Upvotes: 44
Reputation: 5086
Swift version in form of Extension :
extension NSImage {
func tintedImageWithColor(color:NSColor) -> NSImage {
let size = self.size
let imageBounds = NSMakeRect(0, 0, size.width, size.height)
let copiedImage = self.copy() as! NSImage
copiedImage.lockFocus()
color.set()
NSRectFillUsingOperation(imageBounds, .CompositeSourceIn)
copiedImage.unlockFocus()
return copiedImage
}
}
Upvotes: 3
Reputation: 14935
I wanted to tint an image with a tint color that had alpha without seeing the original colors of the image show through. Here's how you can do that:
extension NSImage {
func tinting(with tintColor: NSColor) -> NSImage {
guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return self }
return NSImage(size: size, flipped: false) { bounds in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
tintColor.set()
context.clip(to: bounds, mask: cgImage)
context.fill(bounds)
return true
}
}
}
Upvotes: 8
Reputation: 2645
A Swift implementation of bluebamboo's answer:
func tintedImage(_ image: NSImage, tint: NSColor) -> NSImage {
guard let tinted = image.copy() as? NSImage else { return image }
tinted.lockFocus()
tint.set()
let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
NSRectFillUsingOperation(imageRect, .sourceAtop)
tinted.unlockFocus()
return tinted
}
Upvotes: 14
Reputation: 8117
For a more fine grained, you can use a polynomial color approach, using the approach that I suggested in the following: How can I display the spinning NSProgressIndicator in a different color?
Upvotes: 0
Reputation: 3198
- (NSImage *)imageTintedWithColor:(NSColor *)tint
{
if (tint != nil) {
NSSize size = [self size];
NSRect bounds = { NSZeroPoint, size };
NSImage *tintedImage = [[NSImage alloc] initWithSize:size];
[tintedImage lockFocus];
CIFilter *colorGenerator = [CIFilter filterWithName:@"CIConstantColorGenerator"];
CIColor *color = [[[CIColor alloc] initWithColor:tint] autorelease];
[colorGenerator setValue:color forKey:@"inputColor"];
CIFilter *monochromeFilter = [CIFilter filterWithName:@"CIColorMonochrome"];
CIImage *baseImage = [CIImage imageWithData:[self TIFFRepresentation]];
[monochromeFilter setValue:baseImage forKey:@"inputImage"];
[monochromeFilter setValue:[CIColor colorWithRed:0.75 green:0.75 blue:0.75] forKey:@"inputColor"];
[monochromeFilter setValue:[NSNumber numberWithFloat:1.0] forKey:@"inputIntensity"];
CIFilter *compositingFilter = [CIFilter filterWithName:@"CIMultiplyCompositing"];
[compositingFilter setValue:[colorGenerator valueForKey:@"outputImage"] forKey:@"inputImage"];
[compositingFilter setValue:[monochromeFilter valueForKey:@"outputImage"] forKey:@"inputBackgroundImage"];
CIImage *outputImage = [compositingFilter valueForKey:@"outputImage"];
[outputImage drawAtPoint:NSZeroPoint
fromRect:bounds
operation:NSCompositeCopy
fraction:1.0];
[tintedImage unlockFocus];
return [tintedImage autorelease];
}
else {
return [[self copy] autorelease];
}
}
- (NSImage*)imageCroppedToRect:(NSRect)rect
{
NSPoint point = { -rect.origin.x, -rect.origin.y };
NSImage *croppedImage = [[NSImage alloc] initWithSize:rect.size];
[croppedImage lockFocus];
{
[self compositeToPoint:point operation:NSCompositeCopy];
}
[croppedImage unlockFocus];
return [croppedImage autorelease];
}
Upvotes: 9
Reputation: 46020
The CIMultiplyCompositing filter is definitely the way to do this. If it's crashing, can you post a stack trace? I use CIFilters heavily and don't have crashing issues.
//assume inputImage is the greyscale CIImage you want to tint
CIImage* outputImage = nil;
//create some green
CIFilter* greenGenerator = [CIFilter filterWithName:@"CIConstantColorGenerator"];
CIColor* green = [CIColor colorWithRed:0.30 green:0.596 blue:0.172];
[greenGenerator setValue:green forKey:@"inputColor"];
CIImage* greenImage = [greenGenerator valueForKey:@"outputImage"];
//apply a multiply filter
CIFilter* filter = [CIFilter filterWithName:@"CIMultiplyCompositing"];
[filter setValue:greenImage forKey:@"inputImage"];
[filter setValue:inputImage forKey:@"inputBackgroundImage"];
outputImage = [filter valueForKey:@"outputImage"];
[outputImage drawAtPoint:NSZeroPoint fromRect:NSRectFromCGRect([outputImage extent]) operation:NSCompositeCopy fraction:1.0];
Upvotes: 4