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

13,121

Solution 1

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

Solution 2

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.

Share:
13,121
Jack Ha
Author by

Jack Ha

Musician and software engineer

Updated on June 04, 2022

Comments

  • Jack Ha
    Jack Ha almost 2 years

    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??