We're back after a server migration that caused effbot.org to fall over a bit harder than expected. Expect some glitches.

Creating Palette Images

February 05, 1999 | Fredrik Lundh | Previously published as “fyi #53: creating palette images

Introduction

One of the weak spots in the current release of PIL is that it’s quite difficult to create a 8-bit palette image from scratch. The obvious way to create a palette, by using the ImagePalette class, simply doesn’t behave like one would expect.

Creating the Image

To create a new palette image, use the “P” mode with the new function:

Image.new(“P”, size, fill) where size is the the size in pixels given as (width, height), and fill is the background pixel value.

If fill is omitted, it defaults to 0. To prevent PIL from filling the image at all (e.g. if you’re going to draw over the entire image anyway), use None.

Changing the Palette

PIL assigns a greyscale palette to the new image. In other words, for each colour index i, the corresponding palette entry is (i, i, i).

But how do we modify the contents of this palette? There’s not much on this in the documentation, but maybe we can use dir to see if there’s some attribute we could modify:

>>> import Image
>>> i = Image.new("P", (512, 512))
>>> dir(i)
['category', 'im', 'info', 'mode', 'palette', 'size']

Cool. There’s a palette attribute in there. If we can figure out what it is, maybe we can modify the palette via that attribute.

>>> print i.palette
None

Oops. That wasn’t really what we expected, was it?

In fact, the palette attribute is used to store the palette in some situations. But that’s not always the case, since PIL also maintains an internal palette structure (the ImagingPalette structure) which is attached to the internal image representation.

Unfortunately, the current version of PIL doesn’t do what it takes to keep the externally visible palette attribute in sync with the internal one (this will most likely change in a future version). For example, when we created a new image, PIL properly set the internal palette structure to a greyscale palette, but it didn’t set the public palette attribute.

Maybe there’s some other way to change the palette? Let’s look at the methods provided by the Image class:

>>> dir(i.__class__)
['_Image__transformer', '__doc__', '__init__', '__module__',
'__setattr__', '_dump', '_makeself', 'convert', 'copy', 'crop',
'draft', 'filter', 'format', 'format_description', 'fromstring',
'getbands', 'getbbox', 'getdata', 'getextrema', 'getpixel',
'getprojection', 'histogram', 'load', 'offset', 'paste', 'point',
'putalpha', 'putdata', 'putpalette', 'putpixel', 'quantize',
'resize', 'rotate', 'save', 'seek', 'show', 'split', 'tell',
'thumbnail', 'tobitmap', 'tostring', 'transform', 'transpose']

putpalette looks pretty promising. The only problem is that it appears to be undocumented (at least in the current release of the documentation).

Or rather, it was undocumented until now. Here’s how to use it:

putpalette(palette) where the image should have mode “P” or “L”, and palette is either a sequence of integers, or a string containing a binary representation of the palette.

In both cases, the palette contents should be ordered (r, g, b, r, g, b, …). The palette can contain up to 768 entries (3*256). If a shorter palette is given, it is padded with zeros.

And here’s a simple example. This script draws a few coloured objects on a black background.

import Image
import ImageDraw

im = Image.new("P", (400, 400), 0)

im.putpalette([
    0, 0, 0, # black background
    255, 0, 0, # index 1 is red
    255, 255, 0, # index 2 is yellow
    255, 153, 0, # index 3 is orange
])

d = ImageDraw.ImageDraw(im)
d.setfill(1)

d.setink(1)
d.polygon((0, 0, 0, 400, 400, 400))

d.setink(3)
d.rectangle((100, 100, 300, 300))

d.setink(2)
d.ellipse((120, 120, 280, 280))

im.save("out.gif")

This approach works well if you’re using only a few colours. You could for example write a Python module which contains your favourite palette definition (e.g. a standard 216-colour “web” palette), with symbolic names for the most common colour values.

Hiding Some of the Complexity

On the other hand, it’s not that hard to write a class that lets you create palettes on the fly, with the colours you happen to use in your image.

Here’s a very simple version; this keeps track of colours already used, and allocates new colour indices only when necessary:

class Palette:

    def __init__(self):
        self.palette = []

    def __call__(self, r, g, b):
        # map rgb tuple to colour index
        rgb = r, g, b
        try:
            return self.palette.index(rgb)
        except:
            i = len(self.palette)
            if i >= 256:
                raise RuntimeError, "all palette entries are used"
            self.palette.append(rgb)
            return i

    def getpalette(self):
        # return flattened palette
        palette = []
        for r, g, b in self.palette:
            palette = palette + [r, g, b]
        return palette

And here’s how to use this class:

rgb = Palette()

im = Image.new("P", (400, 400), rgb(0, 0, 0))

d = ImageDraw.ImageDraw(im)
d.setfill(1)

d.setink(rgb(255, 0, 0))
d.polygon((0, 0, 0, 400, 400, 400))

d.setink(rgb(255, 153, 0))
d.rectangle((100, 100, 300, 300))

d.setink(rgb(255, 255, 0))
d.ellipse((120, 120, 280, 280))

im.putpalette(rgb.getpalette())

im.save("out.gif")

There are many ways to improve this class. You can change it so it supports the “#rrggbb” syntax as well, and maybe even add a colour database (perhaps a subset of the one used by the X window system).

Another change would be to make the colour search a bit less strict; if two colours are very similar, they might as well be mapped to the same colour index.

In any case, extending this class is left as an exercise for the interested reader.

Copyright © 1999-2001 by Fredrik Lundh