Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with:

Ask Ben: Extracting And Resizing A ZIP Archive Full Of Images With Coldfusion

By Ben Nadel on

Great!... Ok, What I need is to upload a .zip file containing large jpgs (1024X768 max), the form would contain two fields ID and the ZIP file. The ID field is the name of the folder that will be created or overwritten (inside this folder will be two others: "tn" and "lg"), then your imageutils will process the images saving 3 versions: "large" into "../ID/lg" and "small" into "../ID/tn" the medium size will be saved into another existing folder. The only part im totally "ignorant" about is how to process the zip file containing the images. Also zip file needs to be deleted when all done.

While you mention the "ImageUtils" project, from our previous emails back and fourth, you mentioned that you were running ColdFusion 8. As such, rather than worry about using the Java layer to handle the Zip and the Image manipulation, I'm simply going to address this using ColdFusion 8's CFZip and CFImage tags. Trust me, if you have the option, use them - they will make your life a whole lot easier.

For this demo, I'll carry through your intent, but change it slightly to make it easier to read. I'm going to take a ZIP of images and a target directory name. Then, I'm going to extract all of the images from the ZIP and store them in the target directory at three different image dimensions - large, medium, and small. Each of the three versions will be stored in a sub-directory, with the large being the raw, uploaded image, the medium being 50% size, and the small being 25% size.

 
 
 
 
 
 
 
 
 
 

There's not a whole lot of explaining to do as far as the code goes, so I hope that the following code demo will be sufficient. The only real caveat in the situation is the ability for ColdFusion to work with the selected images. Sometimes, ColdFusion just can't work with some images. I have no idea why - it just doesn't like it; and, what's worse is that it will simply hang, not throwing any errors (at least not in the short term - I could have played with the request timeout to try and force an error). In the video above, you can see that I'm working with a ZIP archive, "shooter2.zip". I'm doing that because some images in my first zip were not playing nice and the CFImage / resize action was hanging.

Caveats aside, I tried to take care of most errors in my error handling, delivering insightful error message back to the end user. Let's take a look at the code:

  • <!--- Param the form fields. --->
  • <cfparam name="form.submitted" type="boolean" default="false" />
  • <cfparam name="form.name" type="string" default="" />
  • <cfparam name="form.archive" type="string" default="" />
  •  
  • <!--- Create an array for errors. --->
  • <cfset errors = [] />
  •  
  •  
  • <!---
  • Define the path of our temp directory. This will be the
  • directory into which our upload will be stored while it's
  • being processed.
  • --->
  • <cfset tempDirectory = (
  • getDirectoryFromPath( getCurrentTemplatePath() ) &
  • "temp\"
  • ) />
  •  
  • <!---
  • Define the path of our upload directory. This will be the
  • directory in which we create our target directory (in which
  • the images will be saved).
  • --->
  • <cfset uploadDirectory = (
  • getDirectoryFromPath( getCurrentTemplatePath() ) &
  • "upload\"
  • ) />
  •  
  •  
  • <!--- Check to see if the form has been submitted. --->
  • <cfif form.submitted>
  •  
  • <!--- Validate the form data. --->
  •  
  • <!--- Was name entered. --->
  • <cfif !len( form.name )>
  •  
  • <cfset arrayAppend(
  • errors,
  • "Please enter the name of your target folder."
  • ) />
  •  
  • <cfelseif reFind( "[^\w_\-]", form.name )>
  •  
  • <cfset arrayAppend(
  • errors,
  • "Your name contains invalid characters. Only alpha-numeric and _ and - are allowed."
  • ) />
  •  
  • </cfif>
  •  
  • <!--- Was archive selected. --->
  • <cfif !len( form.archive )>
  •  
  • <cfset arrayAppend(
  • errors,
  • "Please select an image ZIP file to upload."
  • ) />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Check to see if we have any preliminary form errors at
  • this point. If not, we can start to investigate deeper.
  • --->
  • <cfif !arrayLen( errors )>
  •  
  • <!---
  • If we have no errors yet, then we should try to upload
  • the image ZIP file. At that point, we can perform some
  • more validation of the ZIP and the contained files.
  • --->
  •  
  • <!--- Upload the selected archive file. --->
  • <cffile
  • result="upload"
  • action="upload"
  • filefield="archive"
  • destination="#tempDirectory#"
  • nameconflict="makeunique"
  • />
  •  
  • <!---
  • Store an easy short-hand for our uploaded zip file
  • (since we are going to be referring to it a lot later
  • on).
  • --->
  • <cfset targetZip = "#upload.serverDirectory#\#upload.serverFile#" />
  •  
  • <!---
  • Now that the archive was uploaded, let's search it for
  • image files. Because the file interaction might throw
  • an error (if it was not a zip), let's wrap in a try /
  • catch block.
  • --->
  • <cftry>
  •  
  • <!---
  • Search target ZIP for images. We are going to limit
  • our search to JPG images.
  • --->
  • <cfzip
  • name="zippedImages"
  • action="list"
  • file="#targetZip#"
  • recurse="true"
  • filter="*.jpg"
  • />
  •  
  • <!---
  • Check to see if there were any images found. If
  • there we none found, then this ZIP is invalid.
  • --->
  • <cfif !zippedImages.recordCount>
  •  
  • <!--- Throw an error. --->
  • <cfthrow
  • type="NoImagesFound"
  • message="There were no images found in the selected ZIP file."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Catch any errors about target images. --->
  • <cfcatch type="NoImagesFound">
  •  
  • <!--- Add error message. --->
  • <cfset arrayAppend( errors, cfcatch.message ) />
  •  
  • </cfcatch>
  •  
  • <!--- Catch any file interaction errors. --->
  • <cfcatch type="any">
  •  
  • <!--- Add error message. --->
  • <cfset arrayAppend(
  • errors,
  • "There was a problem reading your ZIP file. Make sure that the selected file is a valid ZIP archive."
  • ) />
  •  
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!---
  • Check to see if we have any errors. If we do, then
  • something is wrong with the selected ZIP file and we
  • don't need to have it on our server. Let's delete it.
  • --->
  • <cfif arrayLen( errors )>
  •  
  • <!--- Delete the invalid zip file. --->
  • <cffile
  • action="delete"
  • file="#targetZip#"
  • />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • At this point, we've done all of the primary and secondary
  • testing of the form data and the selected ZIP file. Let's
  • check to see if we have any errors.
  • --->
  • <cfif !arrayLen( errors )>
  •  
  • <!---
  • There are no errors which means that conditions should
  • be perfect for unzipping and resizing our images.
  • --->
  •  
  • <!---
  • Create the path to our target directory (where we will
  • be uploading the images).
  • --->
  • <cfset targetDirectory = "#uploadDirectory##form.name#\" />
  •  
  • <!---
  • Check to see if our target directory exists in our
  • upload directory. If it does, then we need to stip out
  • existing files. If it doesn't, then we need to crate
  • it (and its sub-directories).
  • --->
  • <cfif directoryExists( targetDirectory )>
  •  
  • <!---
  • Since the directory exists, we are going to ASSUME
  • that the sub-directory structure is already valid.
  • As such, we are ONLY going to strip out existing
  • files, leaving directories in place.
  •  
  • NOTE: The reason we don't simply overwrite them
  • later on is that there might be less images in the
  • new zip than in the existing directory.
  • --->
  •  
  • <!---
  • Query for files in the target diretory. We don't
  • care about the sub-directories as these should be
  • the ones we want to exist.
  • --->
  • <cfdirectory
  • name="fileList"
  • action="list"
  • directory="#targetDirectory#"
  • type="file"
  • recurse="true"
  • />
  •  
  • <!--- Loop over the existing images to delete. --->
  • <cfloop query="fileList">
  •  
  • <!--- Delete the existing files. --->
  • <cffile
  • action="delete"
  • file="#fileList.directory#\#fileList.name#"
  • />
  •  
  • </cfloop>
  •  
  • <cfelse>
  •  
  • <!---
  • Since the target directory does not exist, we
  • need to create it as well as the sub-directories
  • for large, medium, and small images.
  • --->
  •  
  • <!---
  • Create the taret directory (based on the user-
  • entered name in the form submission).
  • --->
  • <cfdirectory
  • action="create"
  • directory="#targetDirectory#"
  • />
  •  
  • <!--- Create our "large" image directory. --->
  • <cfdirectory
  • action="create"
  • directory="#targetDirectory#large\"
  • />
  •  
  • <!--- Create our "medium" image directory. --->
  • <cfdirectory
  • action="create"
  • directory="#targetDirectory#medium\"
  • />
  •  
  • <!--- Create our "small" image directory. --->
  • <cfdirectory
  • action="create"
  • directory="#targetDirectory#small\"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our target directories in place,
  • let's read in our zipped images and resize them.
  • For this demo, I'm going to ASSUME that the uploaded
  • images are the *large* versions and that for sake of
  • simplicity, the sizes will be as follows:
  •  
  • Large: 100% (dimensions)
  • Medium: 50% (dimensions)
  • Small: 25% (dimensions)
  • --->
  •  
  • <!---
  • Because we are dealing again with file interaction
  • (and file interaction that requires a specific file
  • type), let's wrap this up in a try / catch block.
  • --->
  • <cftry>
  •  
  • <!---
  • Loop over the zipped images (that we gathered
  • above when checking for valid images).
  • --->
  • <cfloop query="zippedImages">
  •  
  • <!---
  • Read the images in as a binary value (which
  • we can use as the source in our image
  • manipulation).
  • --->
  • <cfzip
  • variable="imageData"
  • action="readBinary"
  • file="#targetZip#"
  • entrypath="#zippedImages.name#"
  • />
  •  
  • <!---
  • Since we are uploading multiple images, I have
  • decided to use the current record of the image
  • query as the image name. This is only to
  • prevent duplicates. Create a short hand for
  • the file name here since we are going to be
  • using it many times below.
  • --->
  • <cfset imageName = "#zippedImages.CurrentRow#.jpg" />
  •  
  •  
  • <!---
  • Write this image this raw, original image to
  • our large image folder.
  • --->
  • <cffile
  • action="write"
  • file="#targetDirectory#large\#imageName#"
  • output="#imageData#"
  • />
  •  
  • <!---
  • Resize the raw image to 50% for the medium
  • image (and store it in the medium directory
  • in same step).
  • --->
  • <cfimage
  • action="resize"
  • source="#imageData#"
  • destination="#targetDirectory#medium\#imageName#"
  • width="50%"
  • height="50%"
  • />
  •  
  • <!---
  • Resize the raw image to 25% for the small
  • image (and store it in the small directory
  • in the same step).
  • --->
  • <cfimage
  • action="resize"
  • source="#imageData#"
  • destination="#targetDirectory#small\#imageName#"
  • width="25%"
  • height="25%"
  • />
  •  
  • </cfloop>
  •  
  •  
  • <!--- Catch any file interaction errors. --->
  • <cfcatch type="any">
  •  
  • <!--- Add error message. --->
  • <cfset arrayAppend(
  • errors,
  • "There was a problem reading / resizing one of the zipped images. Please make sure that all images are valid JPG images (NOTE: Try opening and resaving the images)."
  • ) />
  •  
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!---
  • At this point, we are done with all of our procesing -
  • or, as much as we could. No matter how much we got
  • through in this last step, let's delete the ZIP file
  • as it no longer servers any purpose.
  • --->
  • <cffile
  • action="delete"
  • file="#targetZip#"
  • />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • <cfoutput>
  •  
  • <!--- Reset the buffer. --->
  • <cfcontent type="text/html" />
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Extracting And Resizing A ZIP Archive Full Of Images With Coldfusion</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Extracting And Resizing A ZIP Archive Full Of Images With Coldfusion
  • </h1>
  •  
  • <p>
  • Please upload your image ZIP file and specify the
  • name of the target directory into which the processed
  • images will be put.
  • </p>
  •  
  • <!--- Check to see if there were any errors. --->
  • <cfif arrayLen( errors )>
  •  
  • <h2>
  • Please review the following:
  • </h2>
  •  
  • <ul>
  • <cfloop
  • index="errorMessage"
  • array="#errors#">
  •  
  • <li>
  • #errorMessage#
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfif>
  •  
  • <form
  • action="#cgi.script_name#"
  • method="post"
  • enctype="multipart/form-data">
  •  
  • <!--- Submission flag. --->
  • <input type="hidden" name="submitted" value="true" />
  •  
  •  
  • <p>
  • <label>
  • <strong>Folder Name:</strong><br />
  • <input type="name" name="name" size="30" />
  • </label>
  • </p>
  •  
  • <p>
  • <label>
  • <strong>Image ZIP:</strong><br />
  • <input type="file" name="archive" size="50" />
  • </label>
  • </p>
  •  
  • <p>
  • <input type="submit" value="Upload Image Zip" />
  • </p>
  •  
  • </form>
  •  
  •  
  • <!---
  • Check to see if the form has been submitted and that
  • we don't have any errors. If this is true, we can
  • show the thumbnails of the uploaded images.
  • --->
  • <cfif (
  • form.submitted &&
  • !arrayLen( errors )
  • )>
  •  
  • <h2>
  • Uploaded Images
  • </h2>
  •  
  • <ul>
  • <cfloop query="zippedImages">
  •  
  • <li>
  • <img
  • src="./upload/#form.name#/small/#zippedImages.CurrentRow#.jpg"
  • alt="Small Image: 25%"
  • style="border: 2px solid black ;"
  • />
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfif>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

Like I said above, I think the code is fairly self-explanatory. I tried to comment it thoroughly. I hope this helps!



Reader Comments

Perhaps a way to get around CFImage hanging on some images would be to fork off CFImage inside a CFThread and then wait for the thread to process to a certain timeout. If it fails, then it only doesn't process the one image (and you get an error) and you can go on with the rest of ZIP.

Reply to this Comment

@John,

That's an interesting idea. I guess you could then add a CFThead / JOIN and wait for the timeout. Cool idea.

Reply to this Comment

@Ben,

One of the most common unexpected failures in CFIMAGE manipulation is dealing with JPEG images in CMYK format. There is no programmatic way (not even viewing File Properties in Windows Explorer) to tell the difference, but browsers cannot render CMYK jpegs (or progressive jpegs) and neither can the Adobe image engine. They have to be RGB and non-progressive.

Other than that, the other supported file formats seems to be pretty straightforward, although it's important to realize that not all supported formats are web-viewable: e.g., the cfimage engine will crunch TIFF files, but the browser can't display the results for you.

Reply to this Comment

@Jason,

That's good to know. Will CMYK throw an error though? My machine just seemed to hang on the image. Re-saving via Fireworks (as a JPG) seemed to take care of it.

I did what I could in the above demo, alerting the user to try such a movie if the CFImage tag was failing.

Reply to this Comment

Nope, generally I wouldn't get errors with invalid jpegs, just the churn. After trying a bunch of different try/catch setups, I ended up testing the initial file right after upload, before even trying any image functions on it:

path = cffile.serverDirectory & "\" & cffile.serverFile;
if (cffile.fileWasSaved and not isImageFile(path)) {
rtnStruct["error"] = "ERROR: The imaging system was unable to process the file you uploaded. Please ensure that you are not using a progressive JPEG or a CMYK JPG.";
}

So, if (rtnStruct.success) then I call my resize or reformat methods, otherwise I just show rtnStruct.error back to the user.

Reply to this Comment

Just a tip, thus far we cannot save images properly with built in CF8 save features - tag or function based, without getting all sorts of random errors... :(

However I use something like this to save which so far has fixed all random errors:

<!--- when save is needed, imageSource is a CF8 image object --->
<cfset saveBufferedImage(imageSource, destination) />

<!--- other code excerp --->
<cfset variables.TAB = chr(9) />

<cffunction name="init" access="public" output="false" returntype="model.system.image.ImageService" hint="Initializes the service.">

<cfset super.init() />

<cfset setJavaImageIO(createObject("java", "javax.imageio.ImageIO")) />
<cfreturn this />
</cffunction>

<cffunction name="getJavaImageIO" access="public" returntype="any" output="false">
<cfreturn variables.instance.javaImageIO />
</cffunction>
<cffunction name="setJavaImageIO" access="private" returntype="void" output="false">
<cfargument name="javaImageIO" type="any" required="true" />
<cfset variables.instance.javaImageIO = arguments.javaImageIO />
</cffunction>

<cffunction name="getWriterFormatName" access="private" output="false" returntype="string">
<cfargument name="filename" type="string" required="true" />

<cfset var validFormats = arrayToList(getJavaImageIO().getWriterFormatNames(), variables.TAB) />
<cfset var pos = listFind(validFormats, lCase(listLast(arguments.filename, ".")), variables.TAB) />

<cfif pos>
<cfreturn listGetAt(validFormats, pos, variables.TAB) />
<cfelse>
<cfreturn "jpeg" />
</cfif>
</cffunction>

<cffunction name="saveBufferedImage" access="private" output="false" returntype="void" hint="Takes a CF image object and gets the buffered image object from it. Saves via java to circumvent lame CF8 image bugs">
<cfargument name="image" type="any" required="true" />
<cfargument name="destination" type="string" required="true" />

<cfset getJavaImageIO().write(
imageGetBufferedImag(arguments.image),
getWriterFormatName(arguments.destination),
createObject("java", "java.io.File").init(arguments.destination)
) />
</cffunction>

Hope that helps, till they fix the bugs :)

Reply to this Comment

Just wanted to toss in another reason for isImageFile() (and others): to help circumvent attacks (where executable code is uploaded, then ran).

Reply to this Comment

Ben, thanks for sharing the blog post on "Extracting And Resizing A ZIP Archive Full Of Images With Coldfusion." I can relateā€¦no doubt!

I really thankful to u for doing such a great job

Reply to this Comment

Hi guys... thanks Ben gain!

About the CCFILE fucntion nameconflict="makeunique"Originally I was thinking to overwrite with new data. But it got me thinking that the user might enter an existing ID or folder name. Instead of makeunique or overwrite existing info; is there a way to let the user know that folder allready exist and give the option to overwrite or enter a new name?

Reply to this Comment

@Shuns,

Very interesting. I wonder where my stuff was hanging - on the resize or the file write. It looks like your stuff helps with the file write; but, if it was hanging on the resize, then I'm out of luck :)

@Felipe,

During the form validation, you would just need to check to see if the directory exists DirectoryExists(). If it does, perhaps the easiest way would be to allow them a checkbox for "overwriting" existing directories. If that box is checked, just proceed. If not, return a form validation error.

Reply to this Comment

Not only that, it specifically checks that the file is an image type that the CF image functions can operate on ... pretty handy.

Reply to this Comment

Well hope that helps Ben - I never had problems with the image operations, only when I went to actually save them to disk. Even though sometimes CF would report as though it was an operation issue...

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.