PNGDecoder.as

This class can transform a PNG image (loaded as a ByteArray) directly into a BitmapData. It's useful only when you want to load a lot of tiny png images and don't want to use the Loader.loadBytes(); method.

UPDATED

The PNGDecoder class has been updated. Its now a singleton, no instantiation needed. Also, you can now retrieve the textual information as a xml that a png file might have.

How to use:
import ion.utils.png.PNGDecoder;
 
var bd:BitmapData = PNGDecoder.decodeImage(myByteArray);
var xml:XML = PNGDecoder.decodeInfo(myByteArray);
Source:
/*
 * Copyright (C) 2011 Ionel Rodriguez
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 */
 
 
 
/* * * * * * * * * * * * * * * * * * * * *
 * IMPORTANT!!
 * 
 * Only works for png files with bit depth
 * of 8 (4 bytes per pixel), colour type 6
 * (true colour with alpha) and non inter-
 * laced.
 * 
 * * * * * * * * * * * * * * * * * * * * */
 
package ion.utils.png {
	import flash.display.BitmapData;
	import flash.utils.ByteArray;
 
	public class PNGDecoder {
		private static const IHDR:int = 0x49484452;
		private static const IDAT:int = 0x49444154;
		private static const tEXt:int = 0x74455874;
		private static const iTXt:int = 0x69545874;
		private static const zTXt:int = 0x7A545874;
		private static const IEND:int = 0x49454E44;
 
		private static var infoWidth:uint;
		private static var infoHeight:uint;
		private static var infoBitDepth:int;
		private static var infoColourType:int;
		private static var infoCompressionMethod:int;
		private static var infoFilterMethod:int;
		private static var infoInterlaceMethod:int;
 
		private static var fileIn:ByteArray;
		private static var buffer:ByteArray;
		private static var scanline:int;
		private static var samples:int;
 
		//**************************************************************************************************************
 
		/* Decodes a png image stream to a BitmapData. If the ByteArray is not a png, returns null. */
		public static function decodeImage(bytes:ByteArray):BitmapData {
			if (bytes == null) return null;
 
			fileIn = bytes;
			buffer = new ByteArray();
			samples = 4;
 
			fileIn.position = 0;
 
			// signature check
			if (fileIn.readUnsignedInt() != 0x89504e47) return invalidPNG();
			if (fileIn.readUnsignedInt() != 0x0D0A1A0A) return invalidPNG();
 
			var chunks:Array = getChunks();
 
			var countIHDR:int = 0;
			var validChunk:Boolean;
			var i:int;
 
			for (i = 0; i < chunks.length; ++i) {
				fileIn.position = chunks[i].position;
				validChunk = true;
 
				if (chunks[i].type == IHDR) {
					++countIHDR;
 
					if (i == 0) validChunk = processIHDR(chunks[i].length);
					else validChunk = false;
				}
 
				if (chunks[i].type == IDAT) {
					buffer.writeBytes(fileIn, fileIn.position, chunks[i].length);
				}
 
				if (!validChunk || countIHDR > 1) return invalidPNG();
			}
 
			var bd:BitmapData = processIDAT();
 
			fileIn = null;
			buffer = null;
 
			return bd;
		}
 
		/* Retrieves the textual information from a png image stream. If the ByteArray is not a png, returns null. */
		public static function decodeInfo(bytes:ByteArray):XML {
			if (bytes == null) return null;
 
			fileIn = bytes;
			fileIn.position = 0;
 
			// signature check
			if (fileIn.readUnsignedInt() != 0x89504e47) { fileIn = null; return null; }
			if (fileIn.readUnsignedInt() != 0x0D0A1A0A) { fileIn = null; return null; }
 
			var chunks:Array = getChunks();
			var xml:XML = <information />;
			var i:int;
 
			for (i = 0; i < chunks.length; ++i) {
				if (chunks[i].type == tEXt) xml.appendChild(gettEXt(chunks[i].position, chunks[i].length));
				if (chunks[i].type == iTXt) xml.appendChild(getiTXt(chunks[i].position, chunks[i].length));
				if (chunks[i].type == zTXt) xml.appendChild(getzTXt(chunks[i].position, chunks[i].length));
			}
 
			fileIn = null;
 
			return xml;
		}
 
		//**************************************************************************************************************
		// Textual data decoding
 
		private static function gettEXt(init:int, len:int):XML {
			var node:XML = <tEXt />;
			var keyword:String = "";
			var text:String = "";
			var char:int = -1;
 
			fileIn.position = init;
 
			while (fileIn.position < init + len) {
				char = fileIn.readUnsignedByte();
 
				if (char > 0) keyword += String.fromCharCode(char);
				else break;
			}
 
			text = fileIn.readUTFBytes(init + len - fileIn.position);
 
			node.appendChild(<keyword>{keyword}</keyword>);
			node.appendChild(<text>{text}</text>);
 
			return node;
		}
 
		private static function getzTXt(init:int, len:int):XML {
			var node:XML = <zTXt />;
			var keyword:String = "";
			var text:String = "";
			var char:int = -1;
 
			fileIn.position = init;
 
			while (fileIn.position < init + len) {
				char = fileIn.readUnsignedByte();
 
				if (char > 0) keyword += String.fromCharCode(char);
				else break;
			}
 
			var compressionMethod:int = fileIn.readUnsignedByte();
 
			if (compressionMethod == 0) {
				var ba:ByteArray = new ByteArray();
 
				ba.writeBytes(fileIn, fileIn.position, init + len - fileIn.position);
				ba.uncompress();
 
				text = ba.readUTFBytes(ba.length);
			}
 
			node.appendChild(<keyword>{keyword}</keyword>);
			node.appendChild(<text>{text}</text>);
 
			return node;
		}
 
		private static function getiTXt(init:int, len:int):XML {
			var node:XML = <iTXt />;
			var keyword:String = "";
			var langTag:String = "";
			var transKeyword:String = "";
			var text:String = "";
			var char:int = -1;
 
			fileIn.position = init;
 
			while (fileIn.position < init + len) {
				char = fileIn.readUnsignedByte();
 
				if (char > 0) keyword += String.fromCharCode(char);
				else break;
			}
 
			var isCompressed:Boolean = fileIn.readUnsignedByte() == 1;
			var compressionMethod:int = fileIn.readUnsignedByte();
 
			while (fileIn.position < init + len) {
				char = fileIn.readUnsignedByte();
 
				if (char > 0) langTag += String.fromCharCode(char);
				else break;
			}
 
			while (fileIn.position < init + len) {
				char = fileIn.readUnsignedByte();
 
				if (char > 0) transKeyword += String.fromCharCode(char);
				else break;
			}
 
			if (isCompressed) {
				if (compressionMethod == 0) {
					var ba:ByteArray = new ByteArray();
 
					ba.writeBytes(fileIn, fileIn.position, init + len - fileIn.position);
					ba.uncompress();
 
					text = ba.readUTFBytes(ba.length);
				}
			}
			else text = fileIn.readUTFBytes(init + len - fileIn.position);
 
			node.appendChild(<keyword>{keyword}</keyword>);
			node.appendChild(<text>{text}</text>);
			node.appendChild(<languageTag>{langTag}</languageTag>);
			node.appendChild(<translatedKeyword>{transKeyword}</translatedKeyword>);
 
			return node;
		}
 
		//**************************************************************************************************************
		// Misc functions
 
		private static function invalidPNG():BitmapData {
			fileIn = null;
			buffer = null;
 
			return null;
		}
 
		private static function getChunks():Array {
			var chunks:Array = [];
			var length:uint;
			var type:int;
 
			do {
				length = fileIn.readUnsignedInt();
				type = fileIn.readInt();
 
				chunks.push({type: type, position: fileIn.position, length: length});
				fileIn.position += length + 4; //data length + CRC (4 bytes)
			}
			while (type != IEND && fileIn.bytesAvailable > 0);
 
			return chunks;
		}
 
		//**************************************************************************************************************
		// Header & image chunks processing
 
		private static function processIHDR(cLength:int):Boolean {
			if (cLength != 13) return false;
 
			infoWidth 				= fileIn.readUnsignedInt();
			infoHeight 				= fileIn.readUnsignedInt();
			infoBitDepth 			= fileIn.readUnsignedByte();
			infoColourType 			= fileIn.readUnsignedByte();
			infoCompressionMethod 	= fileIn.readUnsignedByte();
			infoFilterMethod 		= fileIn.readUnsignedByte();
			infoInterlaceMethod 	= fileIn.readUnsignedByte();
 
			if (infoWidth <= 0 || infoHeight <= 0) return false;
 
			switch (infoBitDepth) {
				case 1:
				case 2:
				case 4:
				case 8:
				case 16: break;
				default: return false;
			}
 
			switch (infoColourType) {
				case 0:
					if (
						infoBitDepth != 1 &&
						infoBitDepth != 2 &&
						infoBitDepth != 4 &&
						infoBitDepth != 8 &&
						infoBitDepth != 16
					) return false;
					break;
 
				case 2:
				case 4:
				case 6:
					if (
						infoBitDepth != 8 &&
						infoBitDepth != 16
					) return false;
					break;
 
				case 3:
					if (
						infoBitDepth != 1 &&
						infoBitDepth != 2 &&
						infoBitDepth != 4 &&
						infoBitDepth != 8
					) return false;
					break;
 
				default: return false;
			}
 
			if (infoCompressionMethod != 0 || infoFilterMethod != 0) return false;
			if (infoInterlaceMethod != 0 && infoInterlaceMethod != 1) return false;
 
			return true;
		}
 
		private static function processIDAT():BitmapData {
			var bd:BitmapData = new BitmapData(infoWidth, infoHeight);
 
			var bufferLen:uint;
			var filter:int;
			var i:int;
 
			var r:uint;
			var g:uint;
			var b:uint;
			var a:uint;
 
			try { buffer.uncompress(); }
			catch (err:*) { return null }
 
			scanline = 0;
			bufferLen = buffer.length;
 
			while (buffer.position < bufferLen) {
				filter = buffer.readUnsignedByte();
 
				// check each scanline filter
				if (filter == 0) {
					for (i = 0; i < infoWidth; ++i ) {
						r = noSample() << 16;
						g = noSample() << 8;
						b = noSample();
						a = noSample() << 24;
 
						bd.setPixel32(i, scanline, a + r + g + b);
					}
				}
				else
				if (filter == 1) {
					for (i = 0; i < infoWidth; ++i ) {
						r = subSample() << 16;
						g = subSample() << 8;
						b = subSample();
						a = subSample() << 24;
 
						bd.setPixel32(i, scanline, a + r + g + b);
					}
				}
				else
				if (filter == 2) {
					for (i = 0; i < infoWidth; ++i ) {
						r = upSample() << 16;
						g = upSample() << 8;
						b = upSample();
						a = upSample() << 24;
 
						bd.setPixel32(i, scanline, a + r + g + b);
					}
				}
				else
				if (filter == 3) {
					for (i = 0; i < infoWidth; ++i ) {
						r = averageSample() << 16;
						g = averageSample() << 8;
						b = averageSample();
						a = averageSample() << 24;
 
						bd.setPixel32(i, scanline, a + r + g + b);
					}
				}
				else
				if (filter == 4) {
					for (i = 0; i < infoWidth; ++i ) {
						r = paethSample() << 16;
						g = paethSample() << 8;
						b = paethSample();
						a = paethSample() << 24;
 
						bd.setPixel32(i, scanline, a + r + g + b);
					}
				}
				else {
					buffer.position += samples * infoWidth;
				}
 
				++scanline;
			}
 
			return bd;
		}
 
		//**************************************************************************************************************
		// Scanline filters
 
		private static function noSample():uint {
			return buffer[buffer.position++];
		}
 
		private static function subSample():uint {
			var ret:uint = buffer[buffer.position] + byteA();
 
			ret &= 0xff;
			buffer[buffer.position++] = ret;
 
			return ret;
		}
 
		private static function upSample():uint {
			var ret:uint = buffer[buffer.position] + byteB();
 
			ret &= 0xff;
			buffer[buffer.position++] = ret;
 
			return ret;
		}
 
		private static function averageSample():uint {
			var ret:uint = buffer[buffer.position] + Math.floor((byteA() + byteB())/2);
 
			ret &= 0xff;
			buffer[buffer.position++] = ret;
 
			return ret;
		}
 
		private static function paethSample():uint {
			var ret:uint = buffer[buffer.position] + paethPredictor();
 
			ret &= 0xff;
			buffer[buffer.position++] = ret;
 
			return ret;
		}
 
		//**************************************************************************************************************
		// Misc functions
 
		private static function paethPredictor():uint {
			var a:uint = byteA();
			var b:uint = byteB();
			var c:uint = byteC();
 
			var p:int = 0;
			var pa:int = 0;
			var pb:int = 0;
			var pc:int = 0;
			var Pr:int = 0;
 
			p = a + b - c;
 
			pa = Math.abs(p - a);
			pb = Math.abs(p - b);
			pc = Math.abs(p - c);
 
			if (pa <= pb && pa <= pc) Pr = a;
			else
			if (pb <= pc) Pr = b;
			else
			Pr = c;
 
			return Pr;
		}
 
		/* The byte corresponding to the one being filtered from the pixel to the left. */
		private static function byteA():uint {
			var init:int = scanline * (infoWidth * samples + 1);
			var priorIndex:int = buffer.position - samples;
 
			if (priorIndex <= init) return 0;
 
			return buffer[priorIndex];
		}
 
		/* The byte corresponding to the one being filtered from the pixel at the top. */
		private static function byteB():uint {
			var priorIndex:int = buffer.position - (infoWidth * samples + 1);
 
			if (priorIndex < 0) return 0;
 
			return buffer[priorIndex];
		}
 
		/* The byte corresponding to the one being filtered from the pixel to the left from the pixel at the top. */
		private static function byteC():uint {
			var priorIndex:int = buffer.position - (infoWidth * samples + 1);
 
			if (priorIndex < 0) return 0;
 
			var init:int = (scanline - 1) * (infoWidth * samples + 1);
			priorIndex = priorIndex - samples;
 
			if (priorIndex <= init) return 0;
 
			return buffer[priorIndex];
		}
	}
}