PIL Tips: Converting Between PNG and GIF

PIL supports both PNG and GIF image formats, but converting between the two while keeping transparency can be tricky. Inside is a list of tips for dealing with transparency and dithering issues when processing GIF image formats in PIL.

Saving GIF with transparency

Support for writing GIF transparency was added in PIL1.1.4. Yet the latest PIL version 1.1.6 doesn't not automatically write the transparency index for you. In order to keep the transparency, you need to explicitly specify the transparent color index as an option when saving the image in GIF format.

im = Image.open('icon.gif')
transparency = im.info['transparency'] 
im.save('icon.gif', transparency=transparency)

im.info is a dictionary that contains a set of properties defined by the Image.open method.

Converting from GIF into PNG

Luckily PNG supports palette-based images. This will save us from a lot of work. The only thing you have to do is setting the transparency option.

im = Image.open('icon.gif')
transparency = im.info['transparency'] 
im .save('icon.png', transparency=transparency)

Converting from PNG into GIF

This can be tricky for two reasons:

  • PNG gives a much wider range of color depths than GIF, truecolor up to 48-bit compared to 8-bit 256-color.
  • PNG gives a much wider range of transparency options than GIF, including alpha channel transparency.

PNG image is palette-based

If the PNG image is palette-based, i.e. P mode, then the conversion is straightforward. The only thing you have to do is setting the transparency option as before:

im = Image.open('icon.png')
assert im.mode == 'P'
transparency = im.info['transparency'] 
im .save('icon.gif', transparency=transparency)

PNG image is in RGBA mode

When converting form RGBA into P, we need to reduce the number of distinct colors used in the image to a maximum of 256 colors through a process called quantization.

PIL supports two types of palette quantizers, WEB and ADAPTIVE. The default quantizer in PIL1.1.6 is Image.WEB which reduces the color space to web safe colors and dithers the rest. While Image.ADAPTIVE quantizer is intended to keep the converted image as visually similar as possible to the original image.

The following table explains the difference in output between WEB and ADAPTIVE quantizers in PIL:

PNG: Original Image GIF: WEB quantizer GIF: ADAPTIVE quantizer
mouse mouse bad mouse

So to avoid dithering, we should use the ADAPTIVE quantizer like this:

im = Image.open('mouse.png')
im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE)
im.save('mouse.gif')

Now that we know how to solve the first issue, we need to find a solution for the transparency issue.

Solid background workaround

Sometimes you don't need to keep the transparency; all what you want is a solid background. This will do the trick for you:

im = Image.open('mouse.png')
# Create a new image with a solid color
background = Image.new('RGBA', im.size, (255, 255, 255))
# Paste the image on top of the background
background.paste(im, im)
im = background.convert('RGB').convert('P', palette=Image.ADAPTIVE)
im.save('mouse.gif')

mouse with white background

Solution for keeping transparency

GIF allows you to set one of the colors in the palette as fully transparent, whereas PNG images in RGBA mode allow you to have different levels of transparency through the alpha band. An alpha value of 255 means the pixel is fully opaque, 0 means fully transparent and Intermediate values indicate partially transparent pixels.

Let's try to set the black background to transparent:

im = Image.open('mouse.png')
im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE)
# The black index in the palette of this image is 255
im.save('mouse.gif', transparency=255)

bad transparent mouse
In addition to the background, black is also used in other places in the image, such as the eyes. That's why the result is bad. A solution to this is to fill the transparent parts with a unique color that doesn't exist in other opaque parts.

Here's a simple function that will do the job:

import random
def unique_color(image):
  """find a color that doesn't exist in the image
  """
  colors = image.getdata()
  while True:
    # Generate a random color
    color = (
      random.randint(0, 255),
      random.randint(0, 255),
      random.randint(0, 255)
    )
    if color not in colors:
      return color

We also need a method to fill the transparent parts with the unique color.
def fill_transparent(image, color, threshold=0): 
  """Fill transparent image parts with the specified color 
  """
  def quantize_and_invert(alpha):
    if alpha <= threshold:
      return 255
    return 0
  # Get the alpha band from the image
  red, green, blue, alpha = image.split()
  # Set all pixel values below the given threshold to 255,
  # and the rest to 0
  alpha = Image.eval(alpha, quantize_and_invert)
  # Paste the color into the image using alpha as a mask
  image.paste(color, alpha)

For more examples on using the paste function please refer to PIL Tutorial: How to Create a Button Generator.

Now we have what we need. Let's try them together:

im = Image.open('mouse.png')
color = unique_color(im)
# You may change the threshold value into something other than 0. 
fill_transparent(im, color, 0)
im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE)
im.save('mouse.gif')

background mouse
Now we can safely set that color to be transparent. All we need is the unique color index.
Here is a function that will get the index for you:
def color_index(image, color):
  """Find the color index
  """
  palette = image.getpalette()
  palette_colors = zip(palette[::3], palette[1::3], palette[2::3])
  return palette_colors.index(color)

The palette sequence contain 768 integer values. Each group of three values represent the red, green, and blue components of the corresponding pixel index. zip(palette[::3], palette[1::3], palette[2::3]) will group those values into tuples of (R, G, B).

And the final result is:

im = Image.open('mouse.png')
color = unique_color(im)
# You may want to change the threshold to something other than 0
fill_transparent(im, color, 0)
im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE)
im.save('mouse.gif', transparency=color_index(im, color))

background mouse

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Command line utility

Hi Nadia,

A great post, with some really good code samples. I've created a PNG2GIF command line utility that's based on your code, which makes it really simple to convert multiple PNG images to GIF. You can download it at http://www.coderholic.com/png2gif/

I really liked your other PIL post, and hope to see some more in the future!

Thanks, Ben

Thanks for sharing

Hi Ben,
I'm glad you liked the post. The command line utility you created is very simple and really helpful.
Thanks for sharing :)

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>. Beside the tag style "<foo>" it is also possible to use "[foo]".

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.