Archive for February, 2009
Exporting high resolution images from Flash
One of the problems I experienced while creating generative art with Flash, was that I couldn’t create BitmapData bigger then 2048 by 2048 pixels. That is in Flash 9 offcourse. In Flash 10 it is now possible to create bitmapdata instances of 4095 by 4095 pixels… but still… what if you want to go beyond the limits of Flash?
So, that’s why I wrote a class called “ChunckedBitmapRenderer”. This class allows you to render any Sprite, no matter its size, to an array of BitmapData instances. I also wrote a PNG encoder to be able to export this to a PNG (so no photoshop is required to stitch the chuncks together):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | //copyright 2009 Ward De Langhe package { import flash.display.BitmapData; import flash.display.BlendMode; import flash.display.Sprite; import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import flash.geom.Matrix; import flash.geom.Rectangle; import flash.utils.ByteArray; public class ChunckedBitmapRenderer { private var bitmapArray:Array; private var _chunckSize:Number; public function ChunckedBitmapRenderer(width:Number,height:Number,transparent:Boolean=false,fillColor:Number=0x000000,cSize:Number=2048) { _chunckSize=cSize; bitmapArray=new Array(); var numHorizontalTiles:Number=Math.ceil(width/_chunckSize) var numVerticalTiles:Number=Math.ceil(height/_chunckSize); var widthCounter:Number=0; var heightCounter:Number=0; for(var x:Number=0;x<numHorizontalTiles;x++){ bitmapArray.push(new Array()); var tileWidth:Number; tileWidth=width-widthCounter; if(tileWidth>_chunckSize) tileWidth=_chunckSize; widthCounter+=tileWidth; for(var y:Number=0;y<numVerticalTiles;y++){ var tileHeight:Number; tileHeight=height-heightCounter; if(tileHeight>_chunckSize) tileHeight=_chunckSize; heightCounter+=tileHeight; var bm:BitmapData=new BitmapData(tileWidth,tileHeight,transparent,fillColor); bitmapArray[x].push(bm); } heightCounter=0; } } public function get chunkSize():Number{ return _chunckSize; } public function render(source:Sprite,blendMode:String=BlendMode.NORMAL):void{ for(var x:Number=0;x<bitmapArray.length;x++){ for(var y:Number=0;y<bitmapArray[x].length;y++){ var bm:BitmapData=bitmapArray[x][y] as BitmapData; var region:Rectangle=new Rectangle(x*_chunckSize,y*_chunckSize,bm.width,bm.height); var m:Matrix=new Matrix(1,0,0,1, 0-region.x, 0-region.y); source.transform.matrix = m; bm.draw(source,m,null,blendMode,null,true); m=new Matrix(1,0,0,1, region.x, region.y); source.transform.matrix = m; } } } public function getPNG():ByteArray{ return ChunckedBitmapToPNGEncoder.encodeChunckedBitmap(bitmapArray,chunkSize); } } } |
Offcourse this code is useless if you don’t have a way to encode this data into a .png or a .jpg file. So that’s why I took Tinic Uro’s PNG encoder class, and changed it a bit to be able to encode an array of bitmaps, provided by the ChunckedBitmapRenderer class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | package { import flash.display.*; import flash.geom.*; import flash.utils.ByteArray; public class ChunckedBitmapToPNGEncoder { public static function encodeChunckedBitmap(bmArray:Array,chunckSize:Number=2048):ByteArray{ var imgWidth:Number=0; var imgHeight:Number=0; for(var x:Number=0;x<bmArray.length;x++){ imgWidth+=(bmArray[x][0] as BitmapData).width; if(x==0){ for(var y:Number=0;y<bmArray[x].length;y++){ imgHeight+=(bmArray[x][y] as BitmapData).height; } } } // 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(imgWidth); IHDR.writeInt(imgHeight); IHDR.writeUnsignedInt(0x08060000); // 32bit RGBA IHDR.writeByte(0); writeChunk(png,0x49484452,IHDR); // Build IDAT chunk var IDAT:ByteArray= new ByteArray(); var img:BitmapData; var transparent:Boolean=(bmArray[0][0] as BitmapData).transparent; for(var i:int=0;i < imgHeight;i++) { // no filter IDAT.writeByte(0); var p:uint; if ( !transparent ) { for(var j:int=0;j < imgWidth;j++) { img=bmArray[Math.floor(j/2048)][Math.floor(i/chunckSize)]; p = img.getPixel(j % chunckSize,i%chunckSize); IDAT.writeUnsignedInt( uint(((p&0xFFFFFF) << 8)|0xFF)); } } else { for(j=0;j < imgWidth;j++) { img=bmArray[Math.floor(j/2048)][Math.floor(i/chunckSize)]; p = img.getPixel32(j%chunckSize,i%chunckSize); 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; } public static function encode(img:BitmapData):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); // 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; if ( !img.transparent ) { for(var j:int=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 = []; 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; } } 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); } } } |
So, how do you use these classes? Here’s a quick example:
Let’s create a sprite, and draw a rectangle in there that is 3000 by 3000 pixels. Normally we wouldn’t be able to render this sprite to a bitmap (in Flash 9).
create the sprite and draw a rectangle:
1 2 3 4 5 | var testSprite:Sprite=new Sprite(); var g:Graphics=testSprite.graphics; g.beginFill(0xCCCCCC,1); g.drawRect(0,0,3000,3000); g.endFill(); |
Now we use the ChunckedBitmapRenderer to render this Sprite to BitmapData:
1 2 3 4 5 6 7 | var chunckedRenderer:ChunckedBitmapRenderer; chunckedRenderer=new ChunckedBitmapRenderer(3000, 3000, false, 0xFFFFFF, 2048); chunckedRenderer.render(testSprite); |
Offcourse once your sprite is rendered to bitmapdata, you want to convert it to an actual image file. Using the PNG encoder you can convert it to a .PNG:
1 | var imgByteArray:ByteArray = chunckedRenderer.getPNG(); |
You probably want to be able to save this ByteArray to disk. If you are creating an AIR application, you can do it this way:
1 2 3 4 5 6 7 8 9 10 | //save the image to the desktop as "snapshot.png" var fl:File = File.desktopDirectory.resolvePath("snapshot.png"); var fs:FileStream = new FileStream(); try{ fs.open(fl,FileMode.WRITE); fs.writeBytes(imgByteArray); fs.close(); }catch(e:Error){ trace(e.message); } |
I do not recommend trying to render high resolution bitmaps realtime. What I do when creating generative art for example, is record all the mouse actions while creating a low resolution “painting”, and then re-run these actions on the high resolution version.

