Thursday, October 17, 2013

How to replace a color in a PNG with python preserving transparency

Here is a small bit of code I thought I'd share with you, since I couldn't find a good solution on the web quickly...

I was coding up a web site with a horizontal navigation menu that used a downwards pointing arrow to denote a menu entry that has a drop down menu. The graphics designer had given me the image for this arrow. Here it is enlarged in a GIMP window:

https://dl.dropboxusercontent.com/u/8112069/darenatwork/2010.10.17_arrow_white.png

Which is all fine and dandy, except, when the mouse hovers above the menu item, the arrow should turn cyan. #00aeef.

I was just about to ask the designer for the cyan version of the arrow, after failing to find a quick and easy way to change all the white pixels to cyan preserving transparency, when I decided to see how hard that would be to do with the Python Imaging Library (PIL).

This is the throwaway script I came up with:

OLD_PATH = r'c:\path\to\images\arrow_white.png'
NEW_PATH = r'c:\path\to\images\arrow_cyan.png'

R_OLD, G_OLD, B_OLD = (255, 255, 255)
R_NEW, G_NEW, B_NEW = (0, 174, 239)

import Image
im = Image.open(OLD_PATH)
pixels = im.load()

width, height = im.size
for x in range(width):
    for y in range(height):
        r, g, b, a = pixels[x, y]
        if (r, g, b) == (R_OLD, G_OLD, B_OLD):
            pixels[x, y] = (R_NEW, G_NEW, B_NEW, a)
im.save(NEW_PATH)

As you can see, that was not so difficult at all... And produced this result:

https://dl.dropboxusercontent.com/u/8112069/darenatwork/2010.10.17_arrow_cyan.png

Let's just go through this quickly... OLD_PATH and NEW_PATH should be self explaining.

Next, I configure the R(ed), G(reen) and B(lue) values of the color I'm looking for and the color I want to change that to. White is easy, you should be able to do that in your head, but for the cyan bit, the python interpreter can help us:

>>> int('00', 16)
0
>>> int('ae', 16)
174
>>> int('ef', 16)
239
>>>

Converting bytes from hex to decimal is equivalent to parsing the hex string as an int with base 16. The other way round is even easier:

>>> hex(0)
'0x0'
>>> hex(174)
'0xae'
>>> hex(239)
'0xef'
>>>

Next, I import the imaging library and open the image. The load() method returns a map of the image, which each x,y-coordinate denoting a tuple (r, g, b, a) with a being the alpha channel or transparency.

The rest of the script is therefor just a simple iteration over the width and height of the image, pixel by pixel, testing for the old color and replacing that with the new color if found, preserving the original transparency.