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.

2 comments:

flexmongo said...

Hi Nick,

thank you very much for providing this usefull peace of code. Hopefully Mike Chambers will integrate this in the next as3corelib version.
Btw: Could you provide an example how to read the metadata which was written before?

Best Regards
Jan Viehweger

nickg said...

Hi Jan, thanks for the comment.

As for reading PNG metadata.. it's a bit harder. Note that the library doesn't provide a PNG reader, since normally PNG rendering and rendering is automatically handled by the Flash core.

I haven't touched Flash/Flex in about 9 months, and it's doubtful I'll have time to jump back into it.
A bit simpler would be code to extract only the metadata from a PNG file. Turn the PNGEncoder "writes" to "reads" and with a bit of hacking you'll get it.

Depending on your app it might be easier to use a server-side app to extract the metadata and pass that upstream to your app.

Good luck!

--nickg