Jack Ha
Jack Ha

Reputation: 20961

Changing palette's of 8-bit .png images using python PIL

I'm looking for a fast way to apply a new palette to an existing 8-bit .png image. How can I do that? Is the .png re-encoded when I save the image? (Own answer: it seems so)

What I have tried (edited):

import Image, ImagePalette
output = StringIO.StringIO()
palette = (.....) #long palette of 768 items
im = Image.open('test_palette.png') #8 bit image
im.putpalette(palette) 
im.save(output, format='PNG')

With my testimage the save function takes about 65 millis. My thought: without the decoding and encoding, it can be a lot faster??

Upvotes: 4

Views: 15378

Answers (3)

Theran
Theran

Reputation: 3856

If you want to change just the palette, then PIL will just get in your way. Luckily, the PNG file format was designed to be easy to deal with when you only are interested in some of the data chunks. The format of the PLTE chunk is just an array of RGB triples, with a CRC at the end. To change the palette on a file in-place without reading or writing the whole file:

import struct
from zlib import crc32
import os

# PNG file format signature
pngsig = '\x89PNG\r\n\x1a\n'

def swap_palette(filename):
    # open in read+write mode
    with open(filename, 'r+b') as f:
        f.seek(0)
        # verify that we have a PNG file
        if f.read(len(pngsig)) != pngsig:
            raise RuntimeError('not a png file!')

        while True:
            chunkstr = f.read(8)
            if len(chunkstr) != 8:
                # end of file
                break

            # decode the chunk header
            length, chtype = struct.unpack('>L4s', chunkstr)
            # we only care about palette chunks
            if chtype == 'PLTE':
                curpos = f.tell()
                paldata = f.read(length)
                # change the 3rd palette entry to cyan
                paldata = paldata[:6] + '\x00\xff\xde' + paldata[9:]

                # go back and write the modified palette in-place
                f.seek(curpos)
                f.write(paldata)
                f.write(struct.pack('>L', crc32(chtype+paldata)&0xffffffff))
            else:
                # skip over non-palette chunks
                f.seek(length+4, os.SEEK_CUR)

if __name__ == '__main__':
    import shutil
    shutil.copyfile('redghost.png', 'blueghost.png')
    swap_palette('blueghost.png')

This code copies redghost.png over to blueghost.png and modifies the palette of blueghost.png in-place.

red ghost -> blue ghost

Upvotes: 8

Jack Ha
Jack Ha

Reputation: 20961

Changing palette's without decoding and (re)encoding does not seem possible. The method in the question seems best (for now). If performance is important, encoding to GIF seems a lot faster.

Upvotes: 0

Alex Martelli
Alex Martelli

Reputation: 881605

im.palette is not callable -- it's an instance of the ImagePalette class, in mode P, otherwise None. im.putpalette(...) is a method, so callable: the argument must be a sequence of 768 integers giving R, G and B value at each index.

Upvotes: 1

Related Questions