Python, PIL and PNG metadata, take 2

28 Aug 2007

Contrary to my original post, the Python PIL library does have support for reading and writing PNG metadata. This is based on the 1.6 and devel snapshot, as of 28-Aug-2007.

The Short Story

The short story is that Image.load reads most PNG metadata into the Image.info dict. But, Image.save ignores Image.info and will erase all metadata!. Use this wrapper function instead:

#                                                                                                                                      
# wrapper around PIL 1.1.6 Image.save to preserve PNG metadata
#
# public domain, Nick Galbreath                                                                                                        
# http://blog.modp.com/2007/08/python-pil-and-png-metadata-take-2.html                                                                 
#                                                                                                                                       
def pngsave(im, file):
    # these can be automatically added to Image.info dict                                                                              
    # they are not user-added metadata
    reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect')

    # undocumented class
    from PIL import PngImagePlugin
    meta = PngImagePlugin.PngInfo()

    # copy metadata into new object
    for k,v in im.info.iteritems():
        if k in reserved: continue
        meta.add_text(k, v, 0)

    # and save
    im.save(file, "PNG", pnginfo=meta)

Just edit the Image.info as you like and it will get written out.

from PIL import Image
im = Image.new("RGB", (128,128), "Black")
im.info["foo"] = "bar"
pngsave(im, "foo.png")

You can see that it worked by either doing strings foo.png or if you use ImageMagick, identify -verbose foo.png

The Long Story

The next section is mostly for PNG nerds and developers of PIL.

Reading

It dumps the metadata key/value pairs into the standard Image.info field. So far so good.

What's not so good is that is also puts transparency, gamma, aspect, and dpi into the same array. While I guess this is metadata, it is rendering metadata which is treated differently in the PNG file than user-added metadata. I'm not sure what PIL does for other image types -- there may be other keywords. This is really only a problem when it comes to writing metadata, in the next section.

Another issue is that PIL only reads only one of the three different type of metadata chunks that PNG supports. (tEXt: yes, zTXt: no, iTXt: no). This post provides a patch for zTXt.

Writing

By default PIL will erase any user-metadata with Image.save. I would think this is a bug. Editing the Image.info dictionary does not result in changes either. It is completely ignored on write.

Oddly PIL has support for writing metadata, as either uncompressed tEXt or compressed zTXt data (which it isn't able to read!). Here's what you do:

>>> from PIL import Image
>>> from PIL import PngImagePlugin

>>> # let's make an image
>>> im = Image.new("RGB", (128,128), "Black")
>>> 
>>> # HERE'S THE SECRET
>>> meta = PngImagePlugin.PngInfo()
>>> meta.add_text("foo", "bar")
>>> im.save("foo.png", "png", pnginfo=meta)
>>>
>>> 
>>> # But im.info is not modified
>>> im.info
{}
>>> # but if we re-open the image, we get out
>>> # metadata back
>>> im2 = Image.open("foo2.png")
>>> im2.info
{'foo': 'bar'}
>>> #
>>> # but remember if we save it without
>>> # explicitly adding the metadata, we lose it
>>> im2.save("foo3.png")
>>> im3 = Image.open("foo3.png")
>>> im3.info
{}
>>> # whoops

The secret is making a PngImagePlugin.PngInfo() object, and then adding key/value pairs using the add_text method. It has an optional third argument whether to compress the value text or not (true/false).

Technically, the PNG spec says that tEXt and zTXt should only contain latin-1 characters. I don't see the writer code enforcing this rule, but it's doubtful it matters at all. There is also no support for the iTXt block, which is for UTF-8 data. This doesn't seem to be a big deal since few (if any) image programs support it.

Ideas

Adding support for zTXt seems like a no-brainer. Especially since the writer exists.

Adding support for iTXt would be nice, but it appears nobody really uses it.

Adding support for the tIME (last modified time) seems like another no-brainer. It is currently not read or written.

Lumping together rendering metadata and user metadata in the same dict is not great. In a ideal world it would be nice to store the metadata in a special dict, that said what type of chunk it was in. Loading and saving a file would result in a near identical file. You then could also specify if a metadatum needed compressing or not. This is bonus. I'd be happy with any interface that allowed one to write plain 'ol tEXt chunks.

The hard part is making a uniform system of metadata that can work between different image types.


Comment 2007-09-12 by None

Thanks for this one. Any idea on how to carry out the same trick for jpegs and tiffs? The JpegImagePlugin module doesn't have an equivalent JpegInfo function, and there is no TiffImagePlugin module at all ...


Comment 2007-09-14 by None

Hi, thanks for the comment. I have not explored metadata for other image types very much. For JPEG images, I know http://www.adobe.com/products/xmp/ and http://www.disc-info.org/ are widely used so some tool must exist for them.

I'll take a look at what PIL offers here and report back.


Comment 2007-09-26 by None

Hey there...

I'm actually trying to save out a PNG at a new bitdepth using PIL and Python with no success so far. Is this at all related to what you're exploring here? Have any suggestions?


Comment 2007-09-30 by None

Hi Taylor,

Saving the PNG with a new bit depth is a little different than what I'm talking about in this article. But, if you just need to convert to grayscale, look at PIL.Image.convert.

Beyond that I would recommend using ImageMagick's convert function. Changing bit depth gets tricky since you might need to quantize colors.


Comment 2008-05-19 by None

1.1.6 as opposed to 1.6, I take it.


Comment 2008-10-27 by None

Good for people to know.


Comment 2009-02-11 by None

Thanks for the bit of code, it was very helpful considering the lack of documentation.

I hope you don't mind I used your snippet of code (with credit) here.

PS: Great blog.


Comment 2009-06-02 by None

Thanks a lot for this snippet, it saved me a lot of time!