29 September 2008
Prior experience with server-side scripting is required. Basic familiarity with the Flash authoring environment is also helpful.
Intermediate
The sample Snapshot Explorer application relies on a very simple PHP script. If you wish, you may translate it into another server-side scripting language such as ColdFusion or .NET.
Have you ever wanted to dynamically convert graphics into static image files at runtime, either for rapid display on your website or to allow your site visitors to download and save images to their computers? This tutorial describes the process of creating the Snapshot class, a custom ActionScript 3.0 class that accomplishes this goal.
For example, imagine that you're developing a drawing program that visitors to your web page can use to compose their own sketches online. Finished drawings are published in a gallery on your website, displayed as JPEG images embedded in HTML.
This project came to life when I developed the website Pixton, which features user-generated comics. Using our Flash editor, you can compose scenes with characters, props, and dialog. But Flash is needed only during the creation of a scene. Once the scene has been created, it doesn't make sense to reconstruct it in Flash every time it's viewed. We realized the more elegant solution would involve taking a snapshot of the scene and then saving it on the server as a JPEG file.
Note: After testing, we decided to save images as JPEGs rather than GIFs because we encountered color-shifting and dithering issues when saving Flash graphics to the GIF file format.
The Snapshot Explorer converts graphics generated at runtime into either JPEG or PNG image files (see screen shot in Figure 1). To test the live example, click the image to open the Snapshot Explorer. Then type some text in the box on the left, choose your options, and click Capture.
Figure 1. Snapshot Explorer (click to launch)
To see the Snapshot Explorer in action, choose PNG as the image format and Prompt to save as the action. When you click Capture, you're prompted to open or save a copy of the PNG file, which is a snapshot of the rounded rectangular area in the SWF.
If desired, try using the other options to load the snapshot image file into the Flash movie or display it in a new browser window. Now that you have a good understanding of what the Snapshot Explorer does, let's take a look under the hood and see how it works.
In this section we'll analyze the project files and look at how the Snapshot Explorer is structured. If you haven't already, be sure to download the provided sample files and uncompress the ZIP so that you can review the contents.
The sample files consist of:
The Snapshot class depends on two public-domain classes that do not ship with Flash:
First, let's examine snapshot-example.fla and Main.as. The user interface includes the bare essentials:
Open the Actions panel to review the code of Main.as. The script begins by importing the required classes. The code automatically declares the Stage instances, so we just need to declare the two RadioButtonGroup instances we'll use to organize the radio buttons interface options into two groups (grpAction and grpFormat):
package
{
import com.goodinson.snapshot.*;
import flash.display.*;
import flash.events.*;
import fl.controls.*;
public class Main extends Sprite
{
private var grpAction:RadioButtonGroup;
private var grpFormat:RadioButtonGroup;
In the Main() constructor function, you'll see that we start by hiding the interface. We use the ADDED_TO_STAGE event to trigger initialization of the interface because the radio buttons won't be ready to initialize during the constructor. When onReady() is triggered, we can be sure that all of the interface components have been fully loaded and then we can make the interface visible. As far as the user is concerned, this all happens instantaneously; but it's very important to always check that the data is loaded and ready before attempting to display it to ensure that everything will work as expected. Here's the constructor code:
function Main()
{
// hide UI while initializing
visible = false;
addEventListener(Event.ADDED_TO_STAGE, onReady);
}
In the onReady() method, we initialize the radio button groups, set a CLICK action for the Capture button, and set the gateway URL for the Snapshot class. The gateway URL is the path to your server-side script that processes the image data sent from Flash. I'll discuss the gateway URL in more detail further.
private function onReady(evt:Event):void
{
// UI has been initialized
visible = true;
removeEventListener(Event.ADDED_TO_STAGE, onReady);
// create radio button groups
grpAction = new RadioButtonGroup("grpAction");
grpFormat = new RadioButtonGroup("grpFormat");
// initialize radio buttons
optJPG.group = grpFormat; optJPG.value = Snapshot.JPG;
optPNG.group = grpFormat; optPNG.value = Snapshot.PNG;
grpFormat.selection = optJPG;
optDisplay.group = grpAction; optDisplay.value = Snapshot.DISPLAY;
optPrompt.group = grpAction; optPrompt.value = Snapshot.PROMPT;
optLoad.group = grpAction; optLoad.value = Snapshot.LOAD;
grpAction.selection = optLoad;
// set the required Snapshot URL
Snapshot.gateway = "http://localhost/_e/snapshot/snapshot.php";
btnCapture.addEventListener(MouseEvent.CLICK, onCapture);
}
Once the interface is initialized, the user can type text into the input text field, choose their options and, when they're ready, click Capture. This triggers the onCapture() method of Main, which subsequently calls the capture method of the Snapshot class. That's basically all there is to snapshot-example.fla and Main.as.
Now let's turn our attention to the Snapshot class, which contains the logic and functionality that makes the Snapshot Explorer run. Inside the sample files, navigate to the snapshot folder and then open Snapshot.as in Flash CS3. The first bit of code in the Snapshot class imports all the dependent classes needed for this project:
package com.goodinson.snapshot
{
import com.goodinson.snapshot.*;
import com.adobe.images.*;
import com.dynamicflash.util.Base64;
import flash.display.*;
import flash.geom.*;
import flash.net.*;
import flash.events.*;
import flash.utils.ByteArray;
After importing the dependent files, the next step is to declare some constants. The public constants correspond to the options accessed in Main.as; the private constants define values that you might want to change in order to tweak Snapshot's output. The public static variable gateway contains the path to the server-side script, which receives and processes the image data sent from Flash. The variable is assigned a value in Main.as rather than declared as a hard-coded constant in Snapshot.as, because you might want to reuse the Snapshot class later with different server-side contexts. Here's the part of the code that declares the constants:
public class Snapshot
{
// supported image file types
public static const JPG:String = "jpg";
public static const PNG:String = "png";
// supported server-side actions
public static const DISPLAY:String = "display";
public static const PROMPT:String = "prompt";
public static const LOAD:String = "load";
// default parameters
private static const JPG_QUALITY_DEFAULT:uint = 80;
private static const PIXEL_BUFFER:uint = 1;
private static const DEFAULT_FILE_NAME:String = 'snapshot';
// path to server-side script
public static var gateway:String;
public static function capture(target:DisplayObject, options:Object):void
{
The entry point into the Snapshot class is the capture method. This method is called with two arguments: a target DisplayObject (the display object to capture as a static image) and an options Object which tells the class which image format to generate, what to do with it and, if applicable, indicates the Loader instance to load the resulting JPEG or PNG. Here's the beginning of the capture method:
public static function capture(target:DisplayObject, options:Object):void
{
The first step is to invoke the bounding rectangle of the DisplayObject to capture. We calculate the rectangle's coordinates relative to the target's parent in order to capture the rotation, scaling, and offset of the target.
Note: If you try to get the rectangle's coordinates with respect to the target itself (by setting the variable relative equal to target), you'll find that any transformations applied to the target are not captured.
So we use the following code to determine the coordinates of the area to be captured:
var relative:DisplayObject = target.parent;
// get target bounding rectangle
var rect:Rectangle = target.getBounds(relative);
Given the target's bounding coordinates, the next step is to create a BitmapData instance, where we'll store the captured pixels. A call to draw() renders the target into the BitmapData instance. Although we know the width and height of the area to capture, we also need to know its x- and y-offsets, otherwise we'd capture the graphics starting at (0,0). To see what would happen without setting these offsets, temporarily comment out the second argument to draw(), the Matrix instance. You'll find that the captured graphic depicts the target starting at (0,0) relative to the target's parent, and the lower and right-hand edges of the target get cut off.
While getBounds() returns a display object's bounds including any strokes, it doesn't always capture all of the pixels associated with the antialiasing of strokes. To compensate for this, we'll add a one-pixel buffer all the way around the target area to be captured. The Matrix instance, then, defines the correct transformation to capture the target DisplayObject in its entirety:
// capture within bounding rectangle; add a 1-pixel buffer around the perimeter to ensure that all anti-aliasing is included
var bitmapData:BitmapData = new BitmapData(rect.width + PIXEL_BUFFER * 2, rect.height + PIXEL_BUFFER * 2);
// capture the target into bitmapData
bitmapData.draw(relative, new Matrix(1, 0, 0, 1, -rect.x + PIXEL_BUFFER, -rect.y + PIXEL_BUFFER));
The next step involves turning the BitmapData instance into a ByteArray. This is akin to converting a two-dimensional array into a one-dimensional array. It's the intermediary variable type we need in order to send the data to the server. Depending on the image format selected by the user, we use JPGEncoder or PNGEncoder. The implementation of these classes is slightly different. For JPGEncoder we need to first construct an instance of the class, passing the JPEG quality as an argument. Since compression isn't an option with the PNG format, the encode() method of PNGEncoder is static and so we call it without creating an instance.
Here's the code:
// encode image to ByteArray
var byteArray:ByteArray;
switch (options.format)
{
case JPG:
// encode as JPG
var jpgEncoder:JPGEncoder = new JPGEncoder(JPG_QUALITY_DEFAULT);
byteArray = jpgEncoder.encode(bitmapData);
break;
case PNG:
default:
// encode as PNG
byteArray = PNGEncoder.encode(bitmapData);
break;
}
The final step in preparing our captured image for server-side processing is to turn the ByteArray into a String. This is where the Base64 class I mentioned earlier comes in handy. A call to its static encodeByteArray() method turns our ByteArray into a single String, which can be sent along with any other data to the server as part of a set of POST variables:
data
var byteArrayAsString:String = Base64.encodeByteArray(byteArray);
In the next part we'll prepare a URLRequest instance, which will send our data to the server. In order to preclude client-side caching, we'll add a random GET query string to the gateway URL. Along with the image data we'll send three other variables: the name of the image file to save (if applicable), the format of the image (JPEG or PNG), and a constant mapping to the action to be performed (Load into Flash, Prompt to save, or Open in browser).
What we do with the URLRequest instance also depends on the action selected by the user. If the Snapshot Explorer is opening the image file in a browser window, we have to call navigateToURL; otherwise, we use the load() method of the Loader instance that was placed in the user interface, like this:
// construct server-side URL to use to send image data
var url:String = gateway + '?' + Math.random();
// determine name of file to be saved / displayed
var fileName:String = DEFAULT_FILE_NAME + '.' + options.format;
// create URL request
var request:URLRequest = new URLRequest(url);
// send data via POST method
request.method = URLRequestMethod.POST;
// set data to send
var variables:URLVariables = new URLVariables();
variables.format = options.format;
variables.action = options.action;
variables.fileName = fileName;
variables.image = byteArrayAsString;
request.data = variables;
if (options.action == LOAD)
{
// load image back into loadContainer
options.loader.load(request);
} else
{
navigateToURL(request, "_blank");
}
The final step happens on the server. The script that processes the image data is quite simple. In this project we used PHP, but the script could just as easily be implemented using ColdFusion or any similar server-side scripting language.
First the script verifies the image format to determine what sort of HTTP header to output. Then, if the user selected the option to save the image to their computer, we add another header that instructs the browser to treat the script's output as a downloadable file. Otherwise, the browser loads the script directly as an image file. PHP's base64_decode() function is the inverse of Base64.encodeByteArray(), so all we need to do is decode the image data and send it directly to output:
<?php
switch ($_POST["format"])
{
case 'jpg':
header('Content-Type: image/jpeg');
break;
case 'png':
default:
header('Content-Type: image/png');
break;
}
if ($_POST['action'] == 'prompt')
{
header("Content-Disposition: attachment; filename=".$_POST['fileName']);
}
echo base64_decode($_POST["image"]);
?>
The Snapshot class performs the basic function of capturing graphics generated in Flash at runtime, and generating a static image file that can be displayed in a browser window, saved to the user's machine or loaded into the SWF.
You can see this technique in action by creating a comic at pixton.com. Try it out. As you edit a scene, the visual data is constructed as Flash graphics. But as soon as you've saved a scene, the data is loaded back into Flash as a static JPEG file. Similarly, all of the comics published on the website consist of JPEGs, which makes for rapid retrieval and hopefully more chuckles per minute!

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License
| 04/23/2012 | Auto-Save and Auto-Recovery |
|---|---|
| 04/23/2012 | Open hyperlinks in new window/tab/pop-up ? |
| 04/21/2012 | PNG transparencies glitched |
| 04/01/2010 | Workaround for JSFL shape selection bug? |
| 02/13/2012 | Randomize an array |
|---|---|
| 02/11/2012 | How to create a Facebook fan page with Flash |
| 02/08/2012 | Digital Clock |
| 01/18/2012 | Recording webcam video & audio in a flv file on local drive |