Occasionally I make apps that create bitmaps and save them. To do so you need to use an encoder to turn the bitmapdata bits into a byte array that can be saved in some image format. AS3 has a PNGEncoder class, but the main problem with it is that it’s pretty slow. I’m saving a 4000×4000 bitmap and it takes sometimes well over 30 seconds, during which time, the app completely freezes up.
Some time last year I was looking around to see if anyone had created an asynchronous version, i.e. one where you could tell it to encode your bitmap and it would do a little bit each frame and tell you when it was done. I wasn’t able to find one. At the time, I took a quick look at the idea of converting the PNGEncoder to do this, but never followed through. Yesterday I started an app that really needed this functionality, and I took another look at it.
Basically the encoder writes some header stuff into a byte array, then loops through from y = 0 to y = height in an outer loop, and from x = 0 to x = width in an inner loop, where it deals with each pixel, writing it to the byte array. Finally, it sets a few more bits and ends off.
What I did was extract the inner loop into its own method, writeRow. And the stuff after the loop into a method called completeWrite. This required making a lot of local variables into class variables. Finally, I converted the outer loop into an onEnterFrame function that listens to the ENTER_FRAME event of a Sprite that I create for no other purpose than to have an ENTER_FRAME event. It’s pretty ugly, I know, but it seems the enter frame got much better performance than a timer. With a timer, whatever your delay is will be inserted between each loop, whereas the enter frame will run ASAP. You could make a really small delay, like 1 millisecond, but that seems like it’s open to some bad side effects. I felt more comfortable with the magic sprite.
Then I found that rather than doing just a single row on each frame, I got better results if I did a chunk of rows. I’m getting pretty good results at 20 rows at a time for a 4000×4000 bitmap, but I didn’t do any kind of benchmarking or testing. This could (should) probably be exposed as a settable parameter.
Anyway, each time it encodes a chunk of rows, it dispatches a progress event, and when it’s done, it dispatches a complete event. I also made a progress property that is just the current row divided by the total height. And of course a png property that lets you get at the byte array when it’s complete.
Originally, I tried extending the original PNGEncoder class and changing the parts I needed to. But everything in there is private, and I needed it to extend EventDispatcher to be able to dispatch events. So it’s a pure copy, paste, and change job.
[as3]////////////////////////////////////////////////////////////////////////////////
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2007 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE: Adobe permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package mx.graphics.codec
{
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.ProgressEvent;
import flash.utils.ByteArray;
import flashx.textLayout.formats.Float;
/**
For the PNG specification, see http://www.w3.org/TR/PNG/
.
*
//————————————————————————–
//
// Class constants
//
//————————————————————————–
/**
//————————————————————————–
//
// Constructor
//
//————————————————————————–
/**
initializeCRCTable();
}
//————————————————————————–
//
// Variables
//
//————————————————————————–
/**
//————————————————————————–
//
// Properties
//
//————————————————————————–
//———————————-
// contentType
//———————————-
/**
"image/png".//————————————————————————–
//
// Methods
//
//————————————————————————–
/**
/**
4 * width * height bytes.false, alpha channel information/**
for (var n:uint = 0; n < 256; n++) { var c:uint = 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;
}
}
/**
if (sourceByteArray)
sourceByteArray.position = 0;
// Create output byte array
_png = new ByteArray();
// Write PNG signature
_png.writeUnsignedInt(0x89504E47);
_png.writeUnsignedInt(0x0D0A1A0A);
// Build IHDR chunk
var IHDR:ByteArray = new ByteArray();
IHDR.writeInt(width);
IHDR.writeInt(height);
IHDR.writeByte(8); // bit depth per channel
IHDR.writeByte(6); // color type: RGBA
IHDR.writeByte(0); // compression method
IHDR.writeByte(0); // filter method
IHDR.writeByte(0); // interlace method
writeChunk(_png, 0x49484452, IHDR);
// Build IDAT chunk
IDAT = new ByteArray();
y = 0;
sprite = new Sprite();
sprite.addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
protected function onEnterFrame(event:Event):void
{
for(var i:int = 0; i < 20; i++) { writeRow(); y++; if(y >= height)
{
sprite.removeEventListener(Event.ENTER_FRAME, onEnterFrame);
completeWrite();
}
}
}
private function completeWrite():void
{
IDAT.compress();
writeChunk(_png, 0x49444154, IDAT);
// Build IEND chunk
writeChunk(_png, 0x49454E44, null);
// return PNG
_png.position = 0;
dispatchEvent(new Event(Event.COMPLETE));
}
private function writeRow():void
{
IDAT.writeByte(0); // no filter
var x:int;
var pixel:uint;
if (!transparent)
{
for (x = 0; x < width; x++) { if (sourceBitmapData) pixel = sourceBitmapData.getPixel(x, y); else pixel = sourceByteArray.readUnsignedInt(); IDAT.writeUnsignedInt(uint(((pixel & 0xFFFFFF) « 8) | 0xFF)); } } else { for (x = 0; x < width; x++) { if (sourceBitmapData) pixel = sourceBitmapData.getPixel32(x, y); else pixel = sourceByteArray.readUnsignedInt(); IDAT.writeUnsignedInt(uint(((pixel & 0xFFFFFF) « 8) | (pixel »> 24)));
}
}
dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS));
}
/**
// Write chunk type.
var typePos:uint = png.position;
png.writeUnsignedInt(type);
// Write data.
if (data)
png.writeBytes(data);
// Write CRC of chunk type and data.
var crcPos:uint = png.position;
png.position = typePos;
var crc:uint = 0xFFFFFFFF;
for (var i:uint = typePos; i < crcPos; i++) { crc = uint(crcTable[(crc ^ png.readUnsignedByte()) & uint(0xFF)] ^ uint(crc »> 8));
}
crc = uint(crc ^ uint(0xFFFFFFFF));
png.position = crcPos;
png.writeUnsignedInt(crc);
}
public function get png():ByteArray
{
return _png;
}
public function get progress():Number
{
return y / height;
}
}
}[/as3]
I’m not putting this out here as any kind of proof of my brilliance, as it’s not very pretty code at all. I did the bare minimum refactoring to get the thing to work in my project. But it does work, and works damn well. Well enough to call it a done for the project I need it for. I mentioned it on twitter and found out that as opposed to the last time I checked, a few other people have created similar classes. A few I am aware of now:
http://blog.inspirit.ru/?p=378
http://pastebin.sekati.com/?id=Pngencoderhack@5d892-84c96899-t
And apparently the hype framework has one built in too, that you might be able to steal.
But, there can never be enough free code out there, right? If this helps anyone, or they can use it as a launching point to create something better, great. If not, well, I wasted a few minutes posting this, so be it. 🙂