Image Manipulation in Flex

It seems that the number one request I get for development work is creating applications that do image manipulation or vector drawing or a combination of the two. This article is about my experiences in building applications in Flex to manipulate images. It will cover the basics of loading an image, saving a reference to it, adjusting color, applying pixel effects, changing its dimensions and orientation and ultimately saving these changes. The aim of this article is not to provide production ready solutions but instead to provide ideas for implementing image manipulation solutions in Flex.

Loading image data

The first thing you need to know about loading images into the Flash Player is the limits the Player has with respect to the maximum size of display objects and the maximum size of bitmap data. Currently the Flash Player has a hard coded limit as to the size of bitmap data you can create in the Flash Player (Flash Player 9 and earlier. I am hoping these limits are removed in Flash Player 10). If you create a new BitmapData object in ActionScript it cannot exceed a size of 2880 x 2880. These values are hard coded into the Flash Player and will result in an ‘Invalid BitmapData’ exception if they are exceeded.

For display objects there are no hard coded values (that I have been able to determine) but through testing I have discovered that if a display object exceeds 8191 x 8191 it won’t be rendered. This also applies to the dimensions of images being loaded. If the dimensions of the image exceed this size it will load but it will not be rendered on the display list. So theoretically you can load an image that is up to 8191 x 8191 but if you plan on accessing its bitmap data it can’t exceed 2880 x 2880. More on this below. One thing to keep in mind is that these limits (in the case of BitmapData anyways.) are related to memory allocation so the larger the image you load the more memory you will consume. Ultimately you are going to want to either limit the size of the image a user can load or scale the image down in your application so let’s look at this now.

For simple projects you could just load the image into an image tag and manipulate it. For more complex applications that require functionality like zoom, pan, multiple copies of same image ( for example a thumbnail and full size version) or non destructive editing a better strategy is to load the image with a Loader and store the bitmap data of the image in a variable. You can then use this data to create multiple bitmaps and apply different manipulations to each. This will greatly decrease the amount of memory used and create a faster more responsive application.

In the sample code below you will see how to load an image with the Loader class and store the bitmap data in a variable, scaling it down if necessary.

private var original:BitmapData;
private static const MAX_WIDTH:uint = 2880;
private static var MAX_HEIGHT:uint = 2880;

private function loadImage(url:String):void
{
 var request:URLRequest = new URLRequest(url);

 	var imageLoader:Loader = new Loader();
 imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, image_completeHandler);
 	// add other listeners here
 	imageLoader.load(request)
}

private function image_completeHandler(event:Event):void
{
 	var bmd:BitmapData = Bitmap(event.currentTarget.content).bitmapData;

 	var originalWidth:Number = bmd.width;
 	var originalHeight:Number = bmd.height;
 	var newWidth:Number = originalWidth;
 	var newHeight:Number = originalHeight;

 	var m:Matrix = new Matrix();

 	var scaleX:Number = 1;
 	var scaleY:Number = 1;

 	if (originalWidth > MAX_WIDTH || originalHeight > MAX_HEIGHT)
 	{
  		sx =  MAX_WIDTH / originalWidth;
  		sy = MAX_HEIGHT / originalHeight;
  		var scale:Number = Math.min(sx, sy);
  		newWidth = originalWidth * scale;
  		newHeight = originalHeight * scale;
  	}
 	m.scale(scale, scale);
 	original = new BitmapData( newWidth, , newHeight); 

 	original.draw(bmd, m);

}

Let’s walk through the code.
We first create a variable to store the bitmap data of the image that we are loading. We then create 2 constants for the maximum width and height this bitmap data can be.

In the loadImage method we take the url of the image as a parameter, create a new URLRequest object with it. We then create a new Loader and attach an event listener to its complete event (other listeners can be added but we are only interested in this event right now). We then call the load method of the loader with the URLRequest as its parameter.

In the event handler we need to access the content property of the loader instance that loaded the image. The content in this case is a Bitmap object. Within a Bitmap there is a property called bitmapData which contains an array of pixel data for this Bitmap. This data is an instance of the BitmapData class.

Once we have a reference to the bitmapData we can check its dimensions and if they exceed our maximum size we can draw it to a new bitmapData object at a new size. To accomplish this we get the amount we need to scale and create a new BitmapData object at this new size. We then use the draw method of the BitmapData class to draw the loaded image to the new BitmapData object and use a matrix to provide the new scale. This BitmapData can now be stored in a variable and used to create as many bitmaps you need each with its own transformations. The transformations applied to you bitmaps don’t affect the original BitmapData so you can always use it as a reference. Now that we have a bitmap data instance that we can use let’s do something with it.

Adjusting Color

One of the things that you are probably going to want to do when manipulating an image is adjust its color. Within the Flash Player API’s there are a number of ways to do this but no matter which you choose what you are basically doing is changing the red, green and blue (and possibly alpha) values of the image. The two main ways to change the values is to either use a ColorTransform or a ColorMatrixFilter. Let’s look at each and how they differ and when to use each.

Using ColorTransform

ColorTransform universally adjust the values of the red, green and blue channels of a display object . Depending on whether you are manipulating a bitmap or a display object the color transform is applied differently. For a display object it is a property of the transform object. For bitmaps it is passed as a parameter to the colorTransform method. To adjust the color you either create a new instance of ColorTransform or get a reference to an existing one and change the red, green and blue values and/or offsets.

Adjusting brightness with a ColorTransform:

*to adjust brightness you change the offsets of the red, green, and blue channels equally

 

var ct:ColorTransform = new  ColorTransform();
ct.redOffset = value;
ct.blueOffset = value;
 ct.greenOffset = value;
image.transform.colorTransform = ct; // apply the transform to a display object

Using ColorMatrixFilter

The ColorMatrix filter uses a 4 x 5 matirx to adjust the values. Unlike the ColorTransform which universally adjusts the red, green and blue channels, the ColorMatrixFilter adjusts each pixel. The speed of the ColorMatrixFilter is proportional to the size of the image (the number of pixels to change).

Adjusting brightness with a ColorMatrixFilter:

 

var cmf:ColorMatrixFilter = new ColorMatrixFilter( [ red, green, blue,0, 0, red, green, blue, 0, 0, red,green, blue, 0, 0, 0, 0, 0, 1, 0 ]);

var filtersArray:Array = new Array();
filtersArray.push(cmf);

image.filters = filtersArray;

or

 

var matrix:Array = new Array();
matrix = matrix.concat([1, 0, 0, 0, value]); // red
matrix = matrix.concat([0, 1, 0, 0, value]); // green
matrix = matrix.concat([0, 0, 1, 0, value]); // blue
matrix = matrix.concat([0, 0, 0, 1, 0]); 	// alpha
var cmf:ColorMatrixFilter = new ColorMatrixFilter(matrix); 

var filtersArray:Array = new Array();
filtersArray.push(cmf);

image.filters = filtersArray;

No matter which you choose you have to remember that each pixel is broken down into its red, green, blue and alpha channels and the range for each channel is 0 to 255. Any values less than 0 will be set to 0 (0x00) and any values greater than 255 will be set to 255 (0xFF). With each of them you can apply multiple effects simultaneously but it is difficult to undo incremental changes made by concatenating multiple ColorTransform objects together. If you use the filter approach undoing one of your effects is as easy as removing it from the filters array and reapplying the array to the display object. With the filter approach you can also apply multiple filters by pushing each of them into the filters array and turning them off by removing them but the filter approach is slower if you have a larger image. More information on the differences can be found in the Flex documentation.

More examples of adjusting color
*Note – there are different formulas that can be used to calculate the values and offsets for these. The ones used here were taken from the Actionscript 3 Cookbook by Joey Lott, Darron Schall and Keith Peters.

Contrast
You adjust brightness by either scaling or offsetting the color values. You change contrast by changing both with all the values being equal and all of the offsets being equal. *value is a number between 0 and 1

 

var a:Number = value * 11;
var b:Number = 63.5 - (value * 698.5);
redValue = greenValue = blueValue = a;
redOffset = greenOffset = blueOffset = b;
var cmf:ColorMatrixFilter = new ColorMatrixFilter(a, 0, 0, 0, b, 0, a, 0, 0, b, 0, 0, a, 0, b, 0, 0, 0, 1, 0);

Saturation

These are the constants for the luminance contrasts for the red, green and blue channels

 

var red:Number = 0.3086; // luminance contrast value for red
var green:Number = 0.694; // luminance contrast value for green
var blue:Number = 0.0820; // luminance contrast value for blue
var a:Number = (1-value) * red + value;
var b:Number = (1-value) * green;
var c:Number = (1-value) * blue;
var d:Number = (1-value) * red;
var e:Number = (1-value) * green + value;
var f:Number = (1-value) * blue;
var g:Number = (1-value) * red ;
var h:Number = (1-value) * green;
var i:Number = (1-value) * blue + value;
var cmf:ColorMatrixFilter = new ColorMatrixFilter(a, b, c, 0, 0, d, e, f, 0, 0, g, h, i, 0 ,0, 0, 0, 0, 1, 0);

Grey Scale

Apply a grey scale effect by red, green and blue values to their luminance contrast values.

 

var red:Number = 0.3086; // luminance contrast value for red
var green:Number = 0.694; // luminance contrast value for green
var blue:Number = 0.0820; // luminance contrast value for blue
var cmf:ColorMatrixFilter = new ColorMatrixFilter(red, green, blue, 0, 0, red, green, blue, 0, 0, red, green, blue, 0, 0, 0, 0, 0, 1, 0);

Negative

Apply a negative effect by reversing the values of the matrix

 

var cmf:ColorMatrixFilter = new ColorMatrixFilter(-1, 0, 0, 0, 255, 0, -1, 0, 0, 255, 0, 0, -1, 0, 255, 0, 0, 0, 1, 0);

Applying Effects

Unlike other aspects of image manipulation there is really only one way to apply effects to your images, a ConvolutionFilter. Like the ColorMatrixFilter, the ConvolutionFilter also uses a matrix to change the image but in this case the matrix can be any size. There are a number of factors that affect performance and they are outlined in the Flex documentation. The most common matrix you will use is a 3×3 matrix. Applying effects is basically the same as applying color effects – you create a filter and add it to the filters array of the display object. Here are some common filters (the first two parameters are the dimensions of the matrix and the third is the matrix).
*the values I am using here are also taken from the Actionscript 3 cookbook

Embossing

Use a negative value in the center and opposite values on each side.

 

var emboss:ConvolutionFilter = new ConvolutionFilter(3, 3, [-2, -1, 0, -1, 1, 1, 0, 1, 2])

Edge Detection

Use a negative value in the center with symmetrical surrounding values

 

var edge:ConvolutionFilter = new ConvolutionFilter(3, 3, [0, 1, 0, 1, -3, 1, ,0, 1, 0])

You can change the center value (-3) to apply more or less of an effect. The smaller the number the more of an effect is applied.

Sharpening

This is the opposite of the Edge Detection matrix – a positive value in the center with symmetrical negative values

 

var sharpen:ConvolutionFilter = new ConvolutionFilter(3, 3, [0, -1, 0, -1, 5, -1, 0, -1, 0]);

The larger the center value the less drastic the effect.
There are other parameters and properties of the ConvolutionFilter that can give some very interesting effects so I encourage you to experiment with them.

Rotating and Flipping

Like color adjustments there are different ways to rotate and/ or flip (mirror) an image in the Flash Player. The first way would be to simply change the rotation, scaleX, and scaleY properties of the display object. This may be fine for simple applications but for more complex applications you will want to use a matrix to manipulate these properties. As noted in the in the Adjusting Color section, each display object has a property called ‘transform’ which contains all of the transformations applied to the display object. The ‘matrix’ property is used to move, rotate, scale and skew the display object. As an example let’s scale the image to twice its original size.
To use the matrix to make changes you get a reference to the matrix (you can’t change the matrix directly)

 

var m:Matrix = image.transform.matrix;

//make your changes (scale it in the x and y direction by a factor of 2)

m.scale(2, 2);

//and reapply the matrix to the display object

image.transform.matrix = m;

One thing of note – If you get a reference to the matrix and then make changes all of your changes will be applied onto any changes that have already been applied. In other words they are applied incrementally. As an example if the matrix for the display object already has a rotation of 30 and you apply a rotation of 10 the result will be a rotation of 40.If this is not your desired effect you can simply create a new Matrix and apply it instead of referencing the matrix that already exists.
The other reason a matrix is the better choice is that you can pass it as a parameter in bitmapData.draw. This allows you to make multiple changes and then using you original bitmap data you can draw a new image with all of these transformations.
If you look at the example under ‘Loading image data’ you will see we used a matrix to scale the image down after it was loaded.

Rotating

To use a matrix to rotate an image you either create a new Matrix with appropriate parameters

 

var q:Number  = 30 * Math.PI / 180 // 30 degrees in radians

var m:Matrix = new Matrix(Math.cos(q), Math.sin(q), -1 * Math.sin(q), Math.cos(q));

//or as a shortcut use the rotate method

var m:Matrix = new Matrix();

m.rotate(q) ;

When you rotate something in the Flash Player it will rotate around its registration point. This by default is the top left corner. If you want to rotate it around a different point you will need to offset it in the negative direction, do the rotation and then put it back where it was.

 

var m:Matrix = new Matrix();

// rotate around the center of the image

var centerX:Number = image.width / 2;

var centerY:Number = image.height /2;

m.translate(-1 * centerX, -1 * centerY);

m.rotate(q);

m.translate(centerX, centrerY);

Flipping

To flip and image is a 2 step process. The first step is to multiply the current scaleX and/or scaleY by -1 and the second is to adjust the x and y position. When you flip and image the registration point does not change and its drawn in the opposite direction. To compensate you will need to change the x position by its width and its y position by its height.

 

var m:Matrix = new Matrix();

m.scale(-1, 0); // flip horizontal assuming that scaleX is currently 1

m.translate(image.width, 0); // move its x position by its width to put it in the upper left corner again

Cropping, Panning and Zooming

Like everything else we have looked at, there are different ways to change the area of an image that you see. You could change the x, y, width and height properties on the display object or a Rectangle to mark out the pixels you want to see.

There are a few reasons that changing the size of the display object is not the best solution. If the user zooms in enough to make the display object larger than 8191 x 8191 it simply will no longer be rendered. Also if you are applying filters with a matrix the new value of each pixel needs to be calculated which can have dire consequences on performance. You only want to apply you filters to the pixels that the user actually sees. The better approach is to redraw the area of the image that the user wants to see using bitmapData.draw and a matrix.

Since you are storing a reference to the original bitmap data of the image you can use a Rectangle to draw any area of that data onto a new bitmap and then use a matrix to scale it to fit the area of the screen it is displayed in.

When you first load the image you set the rectangle to the dimensions of the original bitmap data and store it in an instance variable

 

var zoomArea:Rectangle

zoomArea = original.rect // rect is a property of bitmapData containing the rectangle of the data

The properties of zoomArea can be updating if you want to zoom in or out or pan the image around. The rectangle can then be used to draw an area of the original bitmapData onto a new bitmap.
To pan the image around you can adjust the left and top position of the rectangle through its offset property.

 

zoomArea.offset(panX, panY);

To zoom in or out you adjust the size of the rectangle

 

zoomArea.inflate(newWidth, newHeight);

Once you have made the adjustments you can then draw a new bitmapData with the dimensions of zoomArea using the copyPixels method of the bitmapData class. The copyPixels method takes a rectangle as the second parameter.

 

var newBitmapData:BitmapData = new BitmapData(zoomArea.width, zoomArea.height)
newBitmapData.copyPixels(original, zoomArea, new Point(0, 0));

The copyPixles method is a fast way to copy bitmap data but it will reset any transformations you have made so these all need to be replied. You could also use the rectangle as the clipRect parameter of the bitmapData.draw method.

Printing and Encoding

Once your user has made all of these changes they are probably going to want to print or save them. The Flex 3 SDK now contains a great way to capture these changes. In the mx.graphics package there is a class called ImageSnapshot and this can be used to capture the bitmapData of you image to print or create a jpeg or png to save. You can event use it to encode a byte stream to send to the server. I am not going to go too deeply into this but will mention that if you used a matrix to draw your bitmap then the resolution will now be at screen resolution regardless of what the dpi was before it was loaded. If you simply send the display object to the printer from the stage it will probably appear pixelatted. You will want to capture the display object with the ImageSnapshot.captureImage and scale it up to about 300 dpi to get a crisper image.

Where to go from here

Image manipulation is a broad subject and there are a lot of different approaches you can take. It really comes down to the project requirements. I hope this article gave you some insight into the different approaches you can take. For maximum speed and performance in your application you should familiarize yourself with the Flash Player APIs mentioned in this article. They include Loader, Bitmap, BitmapData, Matrix, Rectangle, Math, ImageSnapshot, JPEGEncoder, PNGEncoder, PrintJob, FlexPrintJob and all of the bitmap filters.