Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Michael Wiesmeyr and Cynthia Ramos and Raphael Mürwald and Marcel Mayr and Joseph Pernerstorfer and Daniel Pötscher and Andreas Breit and Luccas Hansch and Roman Schick and Christian Immitzer
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Michael Wiesmeyr Cynthia Ramos Raphael Mürwald Marcel Mayr Joseph Pernerstorfer Daniel Pötscher Andreas Breit Luccas Hansch Roman Schick Christian Immitzer

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

By
Published in , Comments (17)

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

1 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.

140 Comments

@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.

15,841 Comments

@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.

140 Comments

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.

76 Comments

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 :)

14 Comments

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

5 Comments

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

3 Comments

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?

15,841 Comments

@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.

140 Comments

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

76 Comments

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...

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel