Ask Ben: Extracting And Resizing A ZIP Archive Full Of Images With Coldfusion
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!
Want to use code from this post? Check out the license.
Reader Comments
Thanks Ben, will start running some tests today and will post the results later.
The CForce will be with you, always. :)
@Felipe,
Always glad to help. I hope this gives you some inspiration.
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.
@John,
That's an interesting idea. I guess you could then add a CFThead / JOIN and wait for the timeout. Cool idea.
@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.
@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.
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.
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 :)
Just wanted to toss in another reason for isImageFile() (and others): to help circumvent attacks (where executable code is uploaded, then ran).
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
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?
@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.
@Jason,
Does IsImageFile() actually check to make sure the file is a valid image?
Not only that, it specifically checks that the file is an image type that the CF image functions can operate on ... pretty handy.
@Jason,
Really? That is bad ass!
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...
Thanks so much for this nice article.