Tinting a grayscale NSImage (or CIImage)

Viewed 16891

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.

8 Answers

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
}
Related