JavaScript for Tabular Data with Flash CS3 and AIR
En route between Madrid, Spain and Paris, France, Lee Brimelow and I got into a somewhat philosophical conversation around why the HTML features are important to Flash developers. The obvious standout is in being able to render HTML content in all its glory. Is there anything beyond that though? What about the script-bridge feature? Being able to reach into the HTML DOM and manipulate it from JavaScript? What we came up with was a visually interesting, if not entirely useful example.
The basic premise for the demonstration is that HTML script bridging is important to Flash developers largely to be able to quickly and easily manage tabular data. An example of this would be quickly customizable reporting. Truth be told though, there’s not much that’s particularly exciting about reports. How else could we use tabular data in a more creative manner? Then I remembered an example I saw a few months ago, where Neil Fraser had recreated an image using only an HTML table.
This example is created using only Flash CS3 and the Adobe AIR Update for Flash CS3 Professional.
To recreate an image as an HTML table, you first get to get the image. The obvious means for this is an “Open” button, but I wanted to leverage more AIR functionality, so I went for drag and drop. I’m not sure why, but I had troubles getting drag and drop to function at the Flash level when the HTMLLoader filled the entire stage. I got around this by putting a Sprite on top of the HTMLLoader, with a transparent fill. This is pretty much what you’d do to detect drag and drop on the stage itself, regardless of HTML content.
public function Pilaxin()
{
var blank:File = File.applicationDirectory.resolvePath( BLANK_HTML );
var item:ContextMenuItem = null;
super();
item = new ContextMenuItem( VIEW_SOURCE );
item.addEventListener( ContextMenuEvent.MENU_ITEM_SELECT, doMenu );
context = new ContextMenu();
context.customItems.push( item );
browser = new HTMLLoader();
browser.width = 320;
browser.height = 320;
browser.load( new URLRequest( blank.url ) );
addChild( browser );
hit = new Sprite();
hit.graphics.lineStyle( 1, 0xFFFFFF, 0 );
hit.graphics.beginFill( 0xFFFFFF, 0 );
hit.graphics.drawRect( 0, 0, 320, 320 );
hit.graphics.endFill();
hit.contextMenu = context;
hit.addEventListener( NativeDragEvent.NATIVE_DRAG_ENTER, doEnter );
hit.addEventListener( NativeDragEvent.NATIVE_DRAG_DROP, doDrop );
addChild( hit );
img = new Loader();
img.contentLoaderInfo.addEventListener( Event.COMPLETE, doImage );
img.visible = false;
addChild( img );
}
I originally started out by just using a file from the local operating system in my drag and drop operation. This reason for this was that creating an HTML table that represents an image can take some time, especially if the image is of a high resolution. Using a file from my local system meant that I could resize to a size that would perform well. Eventually I got tired of making small images for testing, and opted to add support in for drag and drop from the browser - specifically Twitter avatars where there’s a plethora of small images.
If the file was local, I simply wanted to load the file into Flash. If the file came from a drag and drop operation from the browser, Flash gets a BitmapData object. That means that the file needs to be encoded and saved local before it can be loaded into Flash. I used the AS3 Core Library to encode as a lossless PNG. The format of the image on disk wasn’t so important so long as it could be loaded by Flash in the end.
public function doEnter( event:NativeDragEvent ):void
{
if( event.clipboard.hasFormat( ClipboardFormats.FILE_LIST_FORMAT ) ||
event.clipboard.hasFormat( ClipboardFormats.BITMAP_FORMAT ) )
{
NativeDragManager.acceptDragDrop(
event.currentTarget as InteractiveObject );
}
}
public function doDrop( event:NativeDragEvent ):void
{
var files:Array = null;
var png:ByteArray = null;
var file:File = null;
var stream:FileStream = null;
var item:URLRequest = null;
if( event.clipboard.hasFormat( ClipboardFormats.FILE_LIST_FORMAT ) )
{
files = event.clipboard.getData( ClipboardFormats.FILE_LIST_FORMAT,
ClipboardTransferMode.CLONE_PREFERRED ) as Array;
fileName = files[0].name;
item = new URLRequest( files[0].url );
}
if( event.clipboard.hasFormat( ClipboardFormats.BITMAP_FORMAT ) )
{
file = File.desktopDirectory.resolvePath( TWITTER_PNG );
fileName = TWITTER_PNG;
png = PNGEncoder.encode( event.clipboard.getData(
ClipboardFormats.BITMAP_FORMAT,
ClipboardTransferMode.CLONE_PREFERRED ) as BitmapData )
stream = new FileStream();
stream.open( file, FileMode.WRITE );
stream.writeBytes( png );
stream.close();
item = new URLRequest( file.url );
}
img.load( item );
}
Why worry about having the image loaded into Flash? Mostly because that’s the easiest way to get the pixel data for a small image. Once an image is loaded into a Loader, you can use the BitmapData.draw() method to capture the pixel data. The Loader doesn’t even have to be visible. You can also apply a Matrix during the draw operation to resize and crop. That wasn’t so important here, but nice to know for future projects involving image manipulations you might have.
var bmp:BitmapData = new BitmapData( img.content.width, img.content.height );
bmp.draw( img );
Now that you have an image source and pixel data to iterate over, you next need to reach into the browser DOM and start creating HTML elements. To recreate an image as an HTML table, you create a table cell (TD) for pixel. Once you’ve create the cell, you set the width and height to a single pixel in measurement. Finally, you can set the background color of the cell to the matching pixel color from the image. You can do this in a tight little loop to get the pixel, color, create the DOM elements, set the values and append the elements to the document.
table = browser.window.document.createElement( TABLE_ELEMENT );
table.cellPadding = 0;
table.cellSpacing = 0;
for( var y:Number = 0; y < bmp.height; y++ )
{
tr = browser.window.document.createElement( TR_ELEMENT );
for( var x:Number = 0; x < bmp.width; x++ )
{
pixel = bmp.getPixel( x, y );
red = ( pixel & 0xFF0000 ) >> 16;
green = ( pixel & 0xFF00 ) >> 8;
blue = ( pixel & 0xFF );
web = twoDigits( red.toString( 16 ) );
web += twoDigits( green.toString( 16 ) );
web += twoDigits( blue.toString( 16 ) );
td = browser.window.document.createElement( TD_ELEMENT );
td.width = 1;
td.height = 1;
td.bgColor = web;
tr.appendChild( td );
}
table.appendChild( tr );
}
browser.window.document.body.appendChild( table );
That’s all there is to it really. The magic is all in the HTMLLoader.window and HTMLLoader.window.document properties which actually map to the actual browser window and document. Any JavaScript methods normally available to you there, can be called directly from ActionScript. The one catch is that you can’t manipulate HTML documents that don’t come from your application’s home or storage directory. I wasn’t really happy with just making the table/image though, and with some extra pushing from Lee, I added a few extra features to the demonstration.

In addition to creating a crazy table in the browser, I also wrote the source code to a file on disk. The idea was to establish a “View Source” mechanism to show that yes, what looks like an image, is actually an HTML table (they match just that close). I didn’t want to view just plain old source either, so I used a service that Mike Chambers told me about prior to the start of the tour. The service is designed to provide pretty formatting for provided source code. Once the HTML table has been rendered, I send the HTML source code to this service for formatting.
public function doRead( event:Event ):void
{
var args:URLVariables = new URLVariables();
var format:URLLoader = new URLLoader();
var req:URLRequest = new URLRequest( FORMATTER );
args.language = HTML_CODE;
args.source = stream.readMultiByte(
stream.bytesAvailable, File.systemCharset );
req.method = URLRequestMethod.POST;
req.data = args;
format.addEventListener( Event.COMPLETE, doFormat );
format.load( req );
}
public function doFormat( event:Event ):void
{
var file:File = File.desktopDirectory.resolvePath(
fileName + "." + FORMATTED + HTML_EXT );
var stream:FileStream = new FileStream();
var res:XML = new XML( event.currentTarget.data as String );
stream.open( file, FileMode.WRITE );
stream.writeMultiByte( res..code[0], File.systemCharset );
stream.close();
}
With Adobe AIR, Flash developers also have the ability to create custom context menus that don’t sport that fancy “Settings…” item. As part of the constructor of the class (above), I create an add a context menu to the transparent Sprite that’s on top of the HTMLLoader control. Selecting the option to view the source uses navigateToUrl() to open a browser the shows the pretty formatted source code. The one improvement I’d like to make, but probably never will, is to use a NativeWindow for the viewing of the source in place of opening a browser window.
Most of the source of this application is available for download. The one part that I’ve removed is the endpoint to Mike Chambers’ pretty code formatting service. Mike has requested that I keep that private for now, and I’ll defer to him on his plans to make it available (or not). That’s all for Paris, France and the AIR Tour, now onto the next project, to be ready with a new demo for the Amsterdam, Netherlands stop. I’m thinking that I’d like to explore data push, visualizations and HTML.