Earlier today I saw a pretty darn good tutorial over at ZURB (I have no idea why they are, but with a name like ZURB they are either Web 2.0 experts or an alien race hell bent on enslaving us. Either way - cool). The article, Image Uploads with 100% Less Suck. Guaranteed, detailed how you can let a user select an image and create a preview from that selection. I'm not going to talk a lot about how the code works - the ZURB folks did a real good job in their blog entry. So be sure to read that before going any further. I assume you did that (programmers always follow written directions) and have asked - can we do this with ColdFusion handling the server-side image processing? Of course we can. Here is a quick mock up I came up with.
I began with a slightly modified version of ZURB's front end code. I set up my div/form like so:
<div id="preview"><span id="status"></span><img id="thumb"></div>
<form action="test.cfm" method="post">
<b>Upload a Picture</b><br/>
<input type="field" id="imageUpload" name="imageUpload"><br/>
<input type="submit" value="Save">
</form>
Notice I've got a preview div with a status span and a blank image. The form itself just consists of the upload field. Now for the JavaScript, and again, credit for this goes to ZURB, and I should also point out it makes use of the AJAX Upload jquery plugin.
$(document).ready(function(){ var thumb = $('img#thumb'); new AjaxUpload('imageUpload', {
action: 'thumbnailupload.cfm',
name: 'image',
onSubmit: function(file, extension) {
$('span.status').text("Loading preview");
},
onComplete: function(file, response) {
thumb.load(function(){
$('span.status').text("");
thumb.unbind();
});
response = $.trim(response)
console.log('thumbnailpreview.cfm?f='+escape(response))
thumb.attr('src', 'thumbnailpreview.cfm?f='+escape(response));
}
});
}); </script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="ajaxupload.js"></script>
<script>
For the most part this is no different from the ZURB blog entry, but I decided to use simple text for the status field during loading. (And yes, I left a console.log in there - IE folks please delete!) You will notice that I point the upload to thumbnailupload.cfm. The result is then loaded via another ColdFusion file, thumbnailpreview.cfm. Let's first look at the upload.
<cffile action="upload" filefield="image" destination="#destdir#" nameconflict="makeunique" result="result">
<cfset newFilePath = destdir & "/" & result.serverfile> <cfif isImageFile(newFilePath)>
<cfset jpgVersion = destdir & "/" & replace(createUUID(), "-", "_", "all") & ".jpg">
<cfimage action="resize" width="100" height="100" source="#newFilePath#" destination="#jpgVersion#" overwrite="true">
<cfoutput>#getFileFromPath(jpgVersion)#</cfoutput>
</cfif>
<cfset destdir = "ram:////thumbnails">
<cfif not directoryExists(destdir)>
<cfdirectory action="create" directory="#destdir#">
</cfif>
I begin by making use of the Virtual File System - a ColdFusion 9 feature. If my root folder doesn't exist, I create it. I then handle the upload. So far so good. The last part of the template simply checks to see if the upload was an image. If it is, I both resize and rename to a JPG. I'll explain the rename in a bit. The resize is hard coded to be 100x100. If I had left height to blank I could have gotten a resize that respected the aspect ratio. Normally you do want to respect that - but in my case I just thought a box worked better. The final action is to return the file name of the JPG file I created.
So if you look back into the JavaScript, you see I take that result, trim it, and pass it to a new url, the preview script. Let's now take a look at that:
<cfif fileExists("ram:////thumbnails/#url.f#") and isImageFile("ram:////thumbnails/#url.f#")>
<cfimage action="read" source="ram:////thumbnails/#url.f#" name="image">
<cfcontent type="image/jpg" variable="#imageGetBlob(image)#">
</cfif>
<cfparam name="url.f" default="">
Woot. Lots of a code there, eh? As you can see, I simply validate that the requested file exists in the VFS (and is a valid image - which means I'm duplicating my check here - a bit of overkill but it can't hurt). I read in the bits and serve it up via cfcontent. The result is spectacular:
Ok, so maybe that's not exactly spectacular, but it does work rather well. However, there's a few things we need to keep in mind before using this code:
- When you make use of the AJAX Upload jquery plugin, it "takes over" the file upload field. What that means is if you submit the form you don't get the file. This means we are right back to the non trivial problem I blogged about last week. If I had to solve it for this particular example, I'd probably store the uploaded image as is, after renaming to a UUID, and then create a thumbnail by adding a "_t" to the file name. I'd return the UUID key and my preview script would know how to find the thumbnail. I'd store that UUID in a hidden form field. When the form is submitted, I'd then know how to fetch the real image out of RAM. Who else thinks it would be cool to have a presentation on "Non-Trivial AJAX Problems"?
- I'm not sure why I used cfimage to read in the bits in the preview script. I should have just read the bits in via fileRead. It would - I assume - run a bit faster.
- This code should be pretty sure - it's checking to see if the upload is a valid image, but you probably tighten it up even further. Also note I forgot to delete the upload if it wasn't an image. That's a two second modification though.
Anyway, I hope this implementation example is useful!
Archived Comments
I saw that to but really it's nothing new since it has to upload the photo first and process it and then show what's already been done. This is nice with a small image but not a larger one and the plugin is nice but no progress bar which is why I have never used it.
When I do this with a photo or video I set it to when they select the item it does this and shows it before submitting the form just like this but for other reasons. Say when a user uploads a photo I upload it to a separate hhd and rename it to a uuid and convert it to a png and move it to a temp folder(actually I use ram:/// with railo) and return the uuid to the form which then via ajax calls an image tag and adds the uuid to show the image. Then when the user submits the photo (say it's for their profile pic) I process the form and take bits of it like their names and build a new image name like ray-camden-coldfusion-jedi-03072010 and then grab the image in temp folder and move, resize and convert in one swoop to the final folders (large & thumbs) and then run a script to delete any old images in any of the folders or in ram.
@Ray,
You may also want to look into trying this using Flex in Flash Player 10.1 I'm sure you know this, but it allows you to read the file in locally from the user's file system, manipulate everything (resize, resample) to display to the user, then allow the user to send or cancel before sending everything to the server. This gets around having to send anything back and forth between client and server, and also allows you to manage the filesize before sending.
@Gareth: Actually, I did not know that. I knew you could do form posts w/ file uploads in Flex, but I didn't know you could read the file w/o doing the upload. Isn't that a security risk? Got any online examples?
@Ray,
Yeah, they added the feature to Flash Player 10.1 Here's a video demo of it
http://www.gotoandlearn.com...
along with a nice, long discussion about why certain changes were made to the Flash sandbox in order to accommodate this feature here
http://theflashblog.com/?p=423
It takes away programmatically opening the browse dialog (for security reasons), but allows the local opening of files. Similar functionality for save...it needs user interaction from a save dialog box to save files locally.
And the FileReference class documentation
http://livedocs.adobe.com/f...
Thanks for sharing that video. I just watched it and it seems like a cool addition to Flash.
My problem is that if a non image file is uploaded I'm having a hard time displaying back to the user that they uploaded an invalid file type. The onComplete event never seems to fire with, for example, a pdf file.
Well you wouldn't be able to use my code w/o modification. It assumes the response is image data - notice how it updates the src attribute of the image. You would need to do something like:
if response is 0, point the src to a 'badimage.gif'
Ray, I can't seem to get this to work all the way. The upload and re-size happens but that's the end of it. Not sure what's fowling the works. The only difference in our code is the destination directory isn't in RAM. I'm using expandPath("./uploads"). For some reason I can't harvest the response. It's not showing any activity in Firebug either.
Also, I'm using the latest version of ajaxupload and linked to the most recent jquery library at googleapis
If you do a dump on the list of files in ram after the upload do you see anything?
<cfdirectory action="list" directory="ram://" name="listDir" recurse="yes" > <cfdump var="#listDir#"> ?
No activity in Firebug at all? How about logging in the CF pages to see they are being accessed?
I dropped a cflog onto all three pages. The main upload page is, of course accessed and so is the thumbnailupload.cfm. But, no activity on the thumbnailpreview.cfm is reported.
One minor caveat, I'm using CF8. But, I'm also not making use of the 'ram:////'
I've tried a lot of things. For some reason I'm not getting a response back. The 'onComplete' isn't firing for some reason. I'll keep hacking away until I get to the bone.
Try adding an alert('poo') as the first line in the onComplete. See if that fires.
Great script got everything to work but it's broken in IE anyone have any problems with IE, safari, firefox, ipad works awesome?
Do you still have the console.log messages from my code? They make IE sad. Remove them and cast them beneath your feet.
Yes I did Duh!, Cool looks good now.
I'm a bit of a console.log freak. I try to remove them when I post because it always tends to trip people up.
Good Stuff, thanks for sharing. Quick question, so I follow the link to AJAX Upload and download the plugin. I don't see the file ajaxupload.js, I see fileuploader.js. Am I correct to assume they one in the same?
thanks
Yeah, looks like they updated the plugin.
Camden:
Thanks for your post, the example is really well-made and explain by itself; thanks to you I was able to fix a problem I had with this library since 3 days ago. But I think it have a little error, the code
<cfif isImageFile(newFilePath)>
<cfset jpgVersion = destdir & "/" & replace(createUUID(), "-", "_", "all") & ".jpg">
<cfimage action="resize" width="100" height="100" source="#newFilePath#" destination="#jpgVersion#" overwrite="true">
<cfoutput>#getFileFromPath(jpgVersion)#</cfoutput>
</cfif>
is making the file be upload twice. In your case it does not matter because you are uploading it to a memory zone, but in may solution I need to store the image in the server, so I improve it by just naming the file instead of resize it and create a new copy.
I hope it work for someone else.
Technically it isn't uploading it twice. It is keeping two copies. :) A small nit - but important if you are worried about network speed.
Hi, making few Changes I made it to work, All events work but only when i upload multiple files and cancel 1 file, it still upload that file, so any update the way i can handle the onCancel multiple uploads cancellation
Um... no idea man. You made modifications... so possibly just roll them back one by one. Maybe also check the docs for the plugin I was using.
yeah, Agreed. Did some modifications and it works but there are few limitations of this tag:
1) Cancel only works if the single file is upload, a made an ajax call on cancel event and deleted the file.
2) if you upload multiple files, it will fail, The Cancel comand will not work properly, it will reload the Partial file in the folder and database if that is used to store file name
So, there are my findings, still there needs to be added some kind of code in JS or custom function which can handle the cancellation of the single file and delete it when the othe files are being uploaded. I tried exploring the XHR and tried using the abort() function but that did not worked either, So figers crossed, will keep working on it, someday i may finish it off
:)