Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 674
Jack Ha Avatar asked Jan 24 '23 09:01

Jack Ha


1 Answers

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

like image 111
Theran Avatar answered Jan 31 '23 02:01

Theran