PNG Encode & Decode with Metadata

by st33d
PNG Encode & Decode with Metadata

This is a demo of saving a PNG image file with additional metadata. The demo also can read that metadata out of files it has generated.

It may also be able to read metadata from other PNGs, but I've set it up to quit scanning for metadata when it gets to the IDAT chunk. I can recommend reading the wikipedia page on PNG encoding if you wish to understand this work further.
♥2 | Line 196 | Modified 2010-09-12 01:31:32 | MIT License
play

ActionScript3 source code

/**
 * Copyright st33d ( http://wonderfl.net/user/st33d )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/oVSK
 */

// forked from st33d's PNG Encode meta data

/* PNG ENCODER WITH METADATA ENCODING AND DECODING METHODS
 *
 * Most of this class is for doing the above job. The bit
 * at the top is to prove it works.
 *
 * Press S to save a test PNG file
 *
 * Then press L to load that file back in and view the
 * metadata packed into it
 */

package {
    import flash.text.TextField;
    import flash.display.Loader;
    import flash.geom.Rectangle;
    import flash.events.KeyboardEvent;
    import flash.net.FileFilter;
    import flash.net.FileReference;
    import flash.display.Sprite;
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.utils.ByteArray;
    import flash.events.Event;
    import flash.events.KeyboardEvent;
    import flash.display.LoaderInfo;
    import flash.display.Loader;
    
    public class PNGMetaEncoder extends Sprite {
        
        public var fileReference:FileReference;
        public var bitmapData:BitmapData;
        public static var output:TextField;
        public var meta:Object = {Title: "Meta data test", Copyright: "Aaron Steed", Also: "Thank fuck I finally got this to work"};
        
        public static const FILE_FILTER:FileFilter = new FileFilter("PNGs", "*.png;");
        public static const KEY_CODE_S:int = 83;
        public static const KEY_CODE_L:int = 76;
        
        public static const CHAR_SET:String = "iso-8859-1";
        public static const IDAT_ID:uint = 0x49444154;
        public static const tEXt_ID:uint = 0x74455874;
        
        public function PNGMetaEncoder() {
            fileReference = new FileReference();
            output = new TextField();
            output.x = 60;
            output.y = 60;
            output.width = 400;
            output.height = 400;
            output.wordWrap = true;
            output.text = "Hello...";
            addChild(output);
            bitmapData = new BitmapData(35, 24, true, 0xFFFF0000);
            bitmapData.fillRect(new Rectangle(10, 10, 20, 20), 0xFF00FF00);
            stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
        }
        
        private function keyPressed(e:KeyboardEvent):void{
            if(e.keyCode == KEY_CODE_S){
                var encodedImage:ByteArray = encode(bitmapData, meta);
                
                fileReference.save(encodedImage, "myImage.png");
            } else if(e.keyCode == KEY_CODE_L){
                fileReference.addEventListener(Event.SELECT, fileSelected);
                fileReference.browse([FILE_FILTER]);
            }
        }
        
        private function fileSelected(e:Event):void{
            fileReference.removeEventListener(Event.SELECT, fileSelected);
            fileReference.addEventListener(Event.COMPLETE, fileLoadComplete);
            fileReference.load();
        }
        
        private function fileLoadComplete(e:Event):void{
            output.appendText("\nload complete");
            fileReference.removeEventListener(Event.COMPLETE, fileLoadComplete);
            var loader:Loader = new Loader();
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, contentLoaderComplete);
            
            // output meta data
            output.appendText("\n\nmeta data:");
            var metaData:Object = getMetaData(fileReference.data);
            for (var k:String in metaData) {
               output.appendText("\nkey:"+k+", value:"+metaData[k]);
            }
            
            loader.loadBytes(fileReference.data);
        }
        
        private function contentLoaderComplete(e:Event):void{
            var loaderInfo:LoaderInfo = (e.target as LoaderInfo);
            loaderInfo.removeEventListener(Event.COMPLETE, contentLoaderComplete);
            addChild(loaderInfo.content);
            if(loaderInfo.content is Bitmap) output.appendText("\n\nconfirming the datatype is a bitmap");
        }
        
        /* Utility methods from here on,
         *
         * From this point downwards comprises the PNGEncoder as it needs to be with
         * metadata saving and reading abilities */
        
        /* Reads metadata from a png image as a ByteArray, then returns an
         * object loaded with the data */
        public static function getMetaData(png:ByteArray):Object{
            var metaData:Object = {};
            var i:int, j:int, key:String, value:String;
            for(i = 0; i < png.length - 4; i++){
                png.position = i;
                // look for the tEXt chunk type
                if(png.readUnsignedInt() == tEXt_ID){
                    // chunks are broken into length, type, data and CRC
                    // we've stopped at the type, wind back to get the length
                    png.position = i - 4;
                    var totalLength:int = png.readUnsignedInt();
                    // the key/value is broken with a single 0x0 byte
                    var keyLength:int = 0;
                    for(j = 0; j < totalLength; j++){
                        png.position = i + 4 + j;
                        if(png.readByte() == 0x0){
                            keyLength = j;
                            break;
                        }
                    }
                    // capture the key
                    png.position = i + 4;
                    key = png.readMultiByte(keyLength, CHAR_SET);
                    // capture the value
                    png.position = i + 4 + keyLength + 1;
                    value = png.readMultiByte(totalLength - (keyLength + 1), CHAR_SET);
                    metaData[key] = value;
                }
                // quit searching once the IDAT chunk is encountered
                // pngs encoded by this class store the metadata before the IDAT
                if(png.readUnsignedInt() == IDAT_ID){
                    break;
                }
            }
            return metaData;
        }

        public static function encode(img:BitmapData, meta:Object = null):ByteArray {
            // Create output byte array
            var png:ByteArray = new ByteArray();
            // Write PNG signature
            png.writeUnsignedInt(0x89504e47);
            png.writeUnsignedInt(0x0D0A1A0A);
            // Build IHDR chunk
            var IHDR:ByteArray = new ByteArray();
            IHDR.writeInt(img.width);
            IHDR.writeInt(img.height);
            IHDR.writeUnsignedInt(0x08060000); // 32bit RGBA
            IHDR.writeByte(0);
            writeChunk(png,0x49484452,IHDR);
            
            // meta data insertion
            for (var k:String in meta) {
               writeChunk_tEXt(png, k, meta[k]);
            }
            
            // Build IDAT chunk
            var IDAT:ByteArray= new ByteArray();
            for(var i:int=0;i < img.height;i++) {
                // no filter
                IDAT.writeByte(0);
                var p:uint;
                var j:int;
                if ( !img.transparent ) {
                    for(j=0;j < img.width;j++) {
                        p = img.getPixel(j,i);
                        IDAT.writeUnsignedInt(
                            uint(((p&0xFFFFFF) << 8)|0xFF));
                    }
                } else {
                    for(j=0;j < img.width;j++) {
                        p = img.getPixel32(j,i);
                        IDAT.writeUnsignedInt(
                            uint(((p&0xFFFFFF) << 8)|
                            (p>>>24)));
                    }
                }
            }
            IDAT.compress();
            writeChunk(png,0x49444154,IDAT);
            // Build IEND chunk
            writeChunk(png,0x49454E44,null);
            // return PNG
            
            return png;
        }
    
        private static var crcTable:Array;
        private static var crcTableComputed:Boolean = false;
    
        private static function writeChunk(png:ByteArray, 
                type:uint, data:ByteArray):void {
            if (!crcTableComputed) {
                crcTableComputed = true;
                crcTable = [];
                var c:uint;
                for (var n:uint = 0; n < 256; n++) {
                    c = n;
                    for (var k:uint = 0; k < 8; k++) {
                        if (c & 1) {
                            c = uint(uint(0xedb88320) ^ 
                                uint(c >>> 1));
                        } else {
                            c = uint(c >>> 1);
                        }
                    }
                    crcTable[n] = c;
                }
            }
            var len:uint = 0;
            if (data != null) {
                len = data.length;
            }
            png.writeUnsignedInt(len);
            var p:uint = png.position;
            png.writeUnsignedInt(type);
            if ( data != null ) {
                png.writeBytes(data);
            }
            var e:uint = png.position;
            png.position = p;
            c = 0xffffffff;
            for (var i:int = 0; i < (e-p); i++) {
                c = uint(crcTable[
                    (c ^ png.readUnsignedByte()) & 
                    uint(0xff)] ^ uint(c >>> 8));
            }
            c = uint(c^uint(0xffffffff));
            png.position = e;
            png.writeUnsignedInt(c);
        }
        
        // from: http://blog.client9.com/2007/08/adding-metadata-to-actionscript-3-png.html
        
        // meta data can be viewed here: http://regex.info/exif.cgi
        
        /**
         * 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, CHAR_SET);
            tEXt.writeByte(0x0);
            tEXt.writeMultiByte(value, CHAR_SET);
            writeChunk(png, tEXt_ID, tEXt);
        }

    }
}