Monday, June 22, 2009

Flex Image Resize and Upload to PHP

Hello Friends of Flex,

I spent most of last week working on a Flex Image Portfolio Project that I developed some time ago. The original design required the user to upload images and an xml file onto a server through ftp. I have a client that has been using it successfully for a while; but, she was not so computer savvy and was just sending the images to me for uploading and adding to the xml file. Both of us were looking for something that she would be able to manage on her own. Time for version 2.0. As with all good (and some not so good) second generation software, it contains several new features.

I am writing to discuss a major feature that took me much to long to research. How does a user select a file from their harddrive, have it loaded into flex, and have flex resize the image and send it to the server. My short answer is that the software writer does a lot of searching over reference material as well as the internet and piecing together learned code from lots of different places. My long answer... well, I hope to have it right here for you.

Let's get started with some basic file reference creation.

private var fileReference:FileReference = new FileReference();
private function onBrowseClicked(event:MouseEvent):void {
var imagesFilter:FileFilter = new FileFilter("Images", "*.jpg;*.gif;*.png");
fileReference.addEventListener(Event.SELECT, onFileSelected);
fileReference.browse([imagesFilter]);
}

Here, I created a FileReference object. Flex uses this to call the browse function on the user's computer. I created it outside of the function because data about the file is going to be stored here and I will need to access it after this function has terminated. When the user clicks a button, the file filter is created. This particular filter targets the three types of images that I would like to allow users to be able to select. I add a listener to the file reference that is called when the user selects a file, then I call browse() and feed it the file filter. The file reference's browse() function causes the user's operating system browse window to open.

Handling the selected file:

private function onFileSelected(event:Event):void {
fileReference.removeEventListener(Event.SELECT, onFileSelected);
fileReference.addEventListener(Event.COMPLETE, onFileLoaded);
fileReference.load();
}

Here, we are adding an event listener to the file reference to listen for when the file has been loaded into the file reference object. Don't forget to remove the previously added listener to avoid memory leaks. Now we call load(), which loads the file into the file reference object inside of flash.

Now that the file is loaded, lets do the following:

private function onFileLoaded(event:Event):void {
fileReference.removeEventListener(Event.COMPLETE, onFileLoaded);
fileName.text = fileReference.name;
fileSize.text = String(fileReference.size);
changeFileSize();
}

First things first, remove the event listener. fileName.text and fileSize.text are references to text fields that I have created to report information to the user. I populate them with information from the file reference. Finally, I call a function that I wrote for creating the bitmaps, changeFileSize().

If you thought the image was loaded, guess again.

private var imageLoader:Loader;
private var imageInfo:LoaderInfo;
private function changeFileSize():void {
imageLoader = new Loader();
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoaded);
imageLoader.loadBytes(fileReference.data);
fileReference.cancel();
}

Umm, how do you display a file? Flash is mostly a visual environment so it doesn't exactly know how to automatically create images from files. The file reference leaves the file in a holding tank somewhere so that it can send it to the server. You can't really change stuff in the file reference; so, we are going to collect the image data from the file reference and send it as a post variable. I created a Loader object here so that flex can load the image into flash.

We could use the file reference to send the file as a url request, but not if we want to modify the file first.

Next little gotcha, we need to listen to the contentLoaderInfo, not the actual image loader. The content loader info is..., well, ummm... something? You got me, I'm none too sure and didn't read up on it. I can tell you that it fires off a complete event when the image loader has finished loading. After stapping a listener onto our new loader, we call the loader's loadBytes() function and feed it data from the file reference.

I have to note here that calling cancel on the file reference may or may not free up your memory. If anyone has a better solution for this, let me know.

Hold on to your boot straps, it's gonna be a bumpy ride from here.

private var imageString:String;
private var bmpData:BitmapData;

I just created two objects that will hold the same data: one in bitmap form, the other in base64 string form. When we send a request to the server, we are going to "post" so we need it to be a string. Because we are going to be using PHP on the server, I am creating a Base64 representation of the data that PHP can decode without the need to change any server settings.

One complication here is that PHP may kick back the post variable as being too long. Be careful because this could cause problems if the end product is too big to send as a post like this. Also, I haven't quite figured out how to give feedback to the user so they know the current progress of the upload. As best as I can tell, this is currently not possible. One more drawback, yep, is that Base64 strings are about 20% larger than their decoded selves. The trade off here is that the method we are using might still be more server side efficient because we can actually scale the image down significantly before we send it.

I've broken this next function down into several parts.

private function onImageLoaded(event:Event):void {
imageLoader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onImageLoaded);
imageInfo = LoaderInfo(event.target);
imageLoader = Loader(imageInfo.loader);

First, let's free up our memory and remove the event listener. Next, we use the data from that mystical loader info to create our imageLoader. What we end up with is a reference to a loader object (referenced with imageLoader) that contains the image data we want. We could event add this image loader as a child to display it on the screen.

Now, lets work on scaling our image. I am setting up the ratio to scale the image to a height of 450 pixels:

var scale:Number = 450 / imageLoader.height;

I then create variables to hold the proper height and width of the image to be. We can take the current height and width multiplied by the scale to come up with our numbers:

var imageWidth:Number = Math.floor(imageLoader.width * scale);
var imageHeight:Number = Math.floor(imageLoader.height * scale);

Now, lets resize the image loader. The image loader is a display object so we can adjust its size just like any other display object:

imageLoader.width = imageWidth;
imageLoader.height = imageHeight;

We are going to create a flash container to hold our image data and then take a picture of that container. When we call new BitmapData(), we feed it the height and width variables. This creates an empty bitmap of the right size. Then, calling the bitmap's draw() function, and feeding it the container we created, we make our bitmap data object copy the contents of the container into itself.

var container:Sprite = new Sprite();
container.addChild(imageLoader);
bmpData = new BitmapData(imageWidth, imageHeight);
bmpData.draw(container);

If you want to display your bitmap to the user, you can do it with the next line of code (below), where previewImage is a canvas or other similar display object container. There's at least one other way to display sprites in a Flex DisplayObject. There appears to be somewhat of a debate on best practices here. You might check google if you want a "better" solution. Here is the quick and brutal way.

previewImage.rawChildren.addChild(container);

NOTICE: I could not figure this next part out so I improvised. I don't believe the bitmap data is stored imediately, so, starting with a the user interface upload button disabled, I created a timer and counted to three before allowing the user to start the upload. If someone knows of a good listener for solving this in a more appropraiate manner, please let me know.

timer = new Timer(3000);
timer.addEventListener(TimerEvent.TIMER, onTimerComplete);
timer.start();
}

private var timer:Timer;
private function onTimerComplete(event:TimerEvent):void {
// handle enabling of button here with
}

So when the user clicks the upload button:

private function onUploadClicked(event:MouseEvent):void {
this.parentApplication.currentState = "blocked";

Last time I tested my internet upload speed it was .65 Mb/s ~ It took about 7 seconds to upload one megabyte worth of data ( an 800 x 450 image as a base64 png string). We need to inform the user that they have to wait for this upload to happen. This will take more time depending on their internet speed and how big of an image you want them to upload. I created a state for the main app and set it to disabled ( a big canvas over everything with some textual information). The user, unfortunately, is just going to have to blindly wait.


Here's where stuff starts getting code sexy. We're going to convert our bitmap data into png data and then convert our png data into a Base64 string. We need two encoders for this:

var pngEncoder:PNGEncoder = new PNGEncoder();
var base64Encoder:Base64Encoder = new Base64Encoder();

The big catch here is that our two encoders don't really behave the same.

Let's start with our png encoder. This encoder has an encode function that you feed bitmap data and it returns an array of bytes, a ByteArray. So we create a variable and store the data in a single line of code.

var byteArray:ByteArray = pngEncoder.encode(bmpData);

Next up, our base64 encoder takes a byte array and stores the data within itself. So we give it the byte array, then we ask it to feed us the data into a string.

base64Encoder.encodeBytes(byteArray);
var encodedImageString:String = encoder.flush();

Congratulations, we have finally created our elusive post variable that represents our PNG image.

I handle server functions via a controller so I am going to pass our created variables to this controller's function. I am including a reference to the calling object within the request so that the controller can inform us when the server has completed it's job.

controller.uploadImage(encodedImageString, this);

In the controller class, let's take a look at the function:

private var uploadCallingObject:Object;
public function uploadImage(imageString:String, object:Object):void {
uploadCallingObject = object;

We set up our reference to the calling object reference. Now we are going to set up our post variables. We are going to send our data to the server within an HTTPService. We need to create a generic object to store the post variables (a little bit of programming magic). I am also going to dynamically create a filename (this saves the worry of validating file names and checking for duplicates). The file name is the universal time number representation of exact current time, with the user's name attached. Our controller already has the user's name stored within it, so I just collecting from the controller. We are also going to use an action variable which tells the server side code what type of action to take.

var variables:Object = new Object();
variables.imageString = imageString;
variables.filename = getUserName() + (new Date().time) + ".png";
variables.action = "upload";
updateData(serverControllerURL, variables, object);
}

I have created a function that handles all of my HTTP service server calls; so, let's use that. We need to establish our http service object outside of the function because we need it to survive and tell us when it’s done it’s job.

private var httpService:HTTPService;
private function updateData(url:String, variables:Object):void {

Now we create our http service object and set it up. We need it to post, return results as xml (e4x), and send the data to our specified url. We also need to feed the service our post variables.

httpService = new HTTPService();
httpService.method = "POST";
httpService.resultFormat = "e4x";
httpService.url = “./imageHandler.php”;
httpService.request = variables;

Set it to listen for a response and send our data to the server.

httpService.addEventListener(ResultEvent.RESULT, onServerCallCompleted);
httpService.addEventListener(FaultEvent.FAULT, onServerCallError); httpService.send();
}

From here you can catch the results from the server and send them to the calling object:

private function onServerCallCompleted(event:Event):void {

uploadCallingObject.onServerSuccess(event);

}

private function onServerCallError(event:FaultEvent):void {

uploadCallingObject.onServerError(event);

}

The results are set to come back as XML so make sure you have php return a string that looks like XML. You could also include more functionality here to handle the results from the service but I prefer the results to be handled within the called object.

If you are still awake and got through this, congratulations. I fell asleep about an hour ago. It was a good nap. Now, just a few steps on the PHP server side and we are done for the day.

Let’s collect our post data with PHP. I am not going to get involved with best practices in PHP so this is going to be short and sweet. Keep in mind that you should probably be testing this stuff better: like making sure the file opens well, making sure the decoder responds well. This will keep the PHP from returning an errors.

Anyways:

if ($_POST["action"] == "upload") {

$filename = $_POST["filename"];

$encodedString = $_POST["imageString"];

Then decode the image string. PHP has a build in function for decoding base64. If you remember, that's why we are using base64 in the first place.

$data = base64_decode($encodedString);

Now we can create a file on the server, write the data to the file, and close the file. Because the decoded data is in the proper format, creating the PNG file is simple, just fill the file with the data.

$fp = fopen( $filename, 'wb');
fwrite( $fp, $data );
fclose( $fp );
}

... and I can get back to sleep.

I hope this stuff comes in handy for someone out there. Feel free to comment if you have questions or if you think there is a better way to do something. I am not against making changes so please don’t hesitate to suggest any.


Your friendly flexer,

Randall