Showing posts with label PNG. Show all posts
Showing posts with label PNG. Show all posts

Wednesday, August 29, 2007

PNG metadata from the command line, again

You may have noticed that ImageMagick's identify -verbose output is huge, and if the key or data part is a bit long, the formating is all wacky. Here's a quick and dirty python script that uses PIL to print PNG metadata to the command line. It's simple enough that even if you don't know python you should be able to hack it to do what you want. Save this file as pngmeta, and then do a chmod a+x pngmeta. Then it should work just like any other shell command, e.g. ./pngmeta file1 file2 file3.... This might work with other image types as well.

#!/usr/bin/env python                                                                                                                   

# public domain, nick galbreath
# http://blog.modp.com/2007/08/png-metadata-from-command-line-again.html

import sys
from PIL import Image

# These are not user-added meta data, skip                                                                                              
reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect')

# sys.argv[0] is the name of the program.. skip it                                                                                      
# for each file on the command line
for file in sys.argv[1:]:
    print file
    im = Image.open(file)
    for k,v in im.info.iteritems():
        # if auto-generated metadata, skip it
        if k in reserved: continue
        print k + " = " + v

Oh great, I'm becoming a PNG metadata expert. Just what I always wanted to be.

Tuesday, August 28, 2007

Python, PIL and PNG metadata, take 2

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.

Sunday, August 19, 2007

Adding metadata to Actionscript 3 PNG encoders

You may have noticed that my pet-peeve du jour is the sorry state of PNG metadata. Right now I'm working in Actionscript 3, and the current crop of PNG encoders doesn't allow metadata.

These all require that the keys and values are in Latin1 (ISO-8859-1) as according to the PNG spec. The spec also requires that the key is 1-79 characters.

AS3CoreLib

The AS3CoreLibs project has a nice PNGEncoder. Here's the drop in replacement that supports metadata.

To use, you do something like this

var meta:Object = {Title: "A big cow", Copyright: "Oh yes"};
var enc:PngEncoder = new PngEncoder();
var bytes:ByteArray = enc.encode(data, meta);

Line by Line

For this patch I'll give a brief explanation on how it works. The other patches are very similar.

This writes out a tEXt chunk

/**
 * write out metadata using Latin1, uncompressed
 *
 * @param png The output bytearray
 * @param key the metadata key.  Must be in latin1, between 1-79 characters
 * @param value the metadata value.  Must be in latin1.
 *
 * the key or value is null or violates some contraints, the metadata
 *  is silently not added
 */
private static function writeChunk_tEXt(png:ByteArray,
                                         key:String, value:String):void
{
    if (key == null || key.length == 0 || key.length > 79) {
        return;
    }
    if (value == null) {
        value = "";
    }

    // the spec says this should be latin1,
    // but UTF8 is probably ok, but be care of overflows
    var tEXt:ByteArray = new ByteArray();
    tEXt.writeMultiByte(key, "iso-8859-1");
    tEXt.writeByte(0x0);
    tEXt.writeMultiByte(value, "iso-8859-1");
    writeChunk(png, 0x74455874, tEXt);
}

Change the encode function to take an optional metadata object (a hash table of key, values)

public function encode(img:BitmapData, meta:Object = null):ByteArray {

Write the tEXt chunks between the IHDR and IDAT chunks

writeChunk(png,0x49484452,IHDR);

// should be before IDAT so ImageMagick can read it
for (var k:String in meta) {
   writeChunk_tEXt(png, k, meta[k]);
}

// Build IDAT chunk
var IDAT:ByteArray= new ByteArray();

Ta - Da!

Better, Faster, PNGEncoder

Hey, it's a better, faster implementation. Besides being faster, it supports RBG or RGBA PNG formats which can save space as well. There is a rumour the improvements will be merged into AS3CoreLib.

The drop in replacement that supports metadata is here. It also cleaned up some warnings/errors spotted by the Flex3-Beta1 compiler and some other tidbits.

AsPngEncoder

There is one other PNG encoder, AsPngEncoder. It's notable since it support palete based PNG files, which can result in dramatic space savings. I got lazy and didn't hack this one. I'll leave it as an exercise for you to add meta data.

License

Hey kids, go nuts. These changes are in the public domain, so the original authors can integrate this without hassle. It would be swell if you gave me some credit (e.g. Thanks to: Nick Galbreath or so), but not required.

WARNING

Always save the best for last. I'm not a flash expert, really. Always check your application before deploying this to production. It's possible quite a few "check for nulls" can be rid of. I'm coming from C++ background and I'm paranoid. ha! Any tips are welcome.

Compiling pngcrush 1.6.4 on Mac OS X 10.4.10

Just in case you are trying to compile pngcrush 1.6.4 on Mac OS 10.4.10. Apple's version of as (the assembler, based on GAS 1.3.8) is retarded. Anyways, it's easy to patch up. Edit the pngcrush Makefile and replace the GAS_VERSION macro as such:
# OLD
#GAS_VERSION := $(shell as --version | grep "GNU assembler" | sed -e 's/GNU assembler //' -e 's/ .*//')

# MAC OS X
GAS_VERSION := $(shell echo "" | as -v 2>&1 | grep "GNU assembler" | sed -e 's/.*GNU assembler version //')

then do make, etc.

And if that doesn't work, change it to GAS_VERSION:= "Apple"

I'm sure this applies to other versions of pngcrush and other versions of OS X

Update 29-Aug-2007: You can use the most excellent macports to install this instead.

sudo  ports -v install pngcrush

Editing PNG Metadata from the Command Line

You would think in our Web 2.0 universe with it's tagoholics, that PNG image metadata would be used and easy to use. Oddly it's not. I know everyone is trying to make their PNG files smaller, but a few bytes describing the owner and copyright information isn't going to kill anyone. Very few (if any) image APIs and GUI applications let you add or edit the metadata and many just erase any that exist.

Here's some tips on read and write PNG metadata from the command line.

PNG Metadata and ImageMagick

Here's how to manipulate metadata to PNG files using Image Magick.

How to read metadata
identify -verbose FILE.png

This spits out all metadata, user defined or otherwise.

How to write metadata
convert INFILE.png -set Title "foobar the great" OUTFILE.png

INFILE and OUTFILE can be the same.

Ugly Details

ImageMagick can read tTXt and zTXt but I was unable to get iTXt to work. It could be that I was not writing the iTXt correctly and I'm too lazy to look at their source code. I'm also told that the libpng does support iTXt yet.

While the PNG spec says you can add chunks in just about any order. In practice, you should put the tEXt chunks before the image data. If you don't, ImageMagick appears to ignore them.

Reading PNG Metadata and PIL

The Python Imaging Library allows you to read but not write metadata(see below). In fact, if you save the image, all metadata will be erased.

http://www.blogger.com/img/gl.link.gif
$ python
Python 2.5 (r25:51908, Oct  7 2006, 01:04:15) 
Type "help", "copyright", "credits" or "license" for more information.
>>> import Image
>>> img = Image.open("junk.png")
>>> img.info
{'foo': "bar"}

Also, PIL cannot read zTXt or iTXt chunks. Boo.

UPDATE 28-Aug-2007: PIL 1.6 secretly allows you to add metadata. See this post for details.

PNG Metadata and PNGCrush

PNGCrush is a nifty little program for optimizing the size of PNG files. It also let's you write metadata. But sadly, I think it has a little bug.

A sister program pngmeta dumps metadata. It's old but seems to work. It does not let you add metadata.