Ask Ben: Zipping Images With ColdFusion 8's CFZip And CFHttp

Posted November 7, 2007 at 2:18 PM by Ben Nadel

Tags: ColdFusion, Ask Ben

I have to download image files off of a another server and zip them up. I have a url for each image and right now I'm using cfhttp to get the image. Any idea how to zip them up without writing them out to a tmp directory?

When I read this question, I got all giddy 'cause I thought I was gonna be able rock some spicy CFImage - CFZip cohabitation. I did go down that road, but in the long run, it didn't work. Originally, I had this plan to use ColdFusion 8's new CFImage tag to grab the images from the given URLs. Then, all I would have to do is grab the image Blob object (ImageGetBlob()) and zip that into the archive.

This had sweet benefits. For starters, it's much less code than a CFHttp call - you put in the URL as the CFImage source attribute and it just downloads and creates the ColdFusion 8 image object. There was also implicit type validation; we were gonna have to use CFTry / CFCatch no matter what, since we are dealing with an external resource, but CFImage had the implicit image type validation since you can't just load any old file into a ColdFusion image object. It would have also put us in a great position if we wanted to do any type of image conversion.

So, with all the goodness, what went wrong with the CFZip / CFImage approach? There were simply too many variables when it comes to dealing with URL-based images. Most importantly, though, we lose file-type information since we cannot see the response header. Sure, all is easy when you are calling .JPG or .GIF files directly, but what happens when you call a ColdFusion page (.CFM) that is serving up image content? The CFImage tag knows how to read that in, but what type of image is it from our perspective? We didn't see the response header so we don't know. This isn't a problem when we are writing the images to the file system, but this causes a huge problem when we need to utilize the Blob data (which is file-type dependent).

It would have been sweet-ass-sweet, but when it comes to in-memory blob usage and zip archiving, CFImage just didn't quite get us there. The next solution is to rely on ColdFusion's standard CFHttp call. Using ColdFusion's CFHttp tag, we can still grab the image files as binary objects, which complements ColdFusion 8's new CFZipParam tag quite nicely, and it still gives us the ability to look the response header and more easily determine the file type.

When it comes to zipping the image data, I am using a Hash() of the URL so that we lower the chances of us overwriting other images we are downloading at the same time. If you KNOW THE FILE NAMES ahead of time and you know how the foreign server is serving up files, then you don't need this; additionally, you could use ColdFusion 8's CFImage tag as well. I tried to build the more flexible solution, but if you know the file naming for fact, then you could have written something a bit shorter and more elegant.

As far as the zip file is concerned, you do have to have the zip file written to the file system. There is no getting around that using ColdFusion 8's CFZip tag as it does not allow variable-based file creation. This is not a bad thing since Zip files might become very large and become a drain on the system RAM. If it is absolutely necessary to do all in-memory stuff, you might want to check out my ZipUtility.cfc which can handle in-memory zipping.

That being said, here is what I came up with:

  • <!---
  • Param the FORM variable that will hold the list of URLs
  • (delimited by line breaks and carriage returns).
  • --->
  • <cfparam
  • name="FORM.urls"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!--- Check to see if the user has entered any URLs. --->
  • <cfif Len( FORM.urls )>
  •  
  • <!---
  • Get a temporary file in the system's temp directory.
  • Since CFZip cannot write to memory, we have to have
  • some sort of physical file (but this will be our
  • only one).
  • --->
  • <cfset strZipPath = GetTempFile(
  • GetTempDirectory(),
  • "images"
  • ) />
  •  
  •  
  • <!---
  • Set a valid flag so that we will know if at least one
  • valid image was zipped.
  • --->
  • <cfset blnIsValidZip = false />
  •  
  •  
  • <!--- Now, let's loop over the URLs. --->
  • <cfloop
  • index="strImageURL"
  • list="#FORM.urls#"
  • delimiters="#Chr( 13 )##Chr( 10 )#">
  •  
  • <!---
  • Try to read in the image url using CFHttp. Since
  • we have to rely on a network connection and an
  • outside source, be sure to wrap in CFTry / CFCatch.
  • --->
  • <cftry>
  •  
  • <!--- Grab the image using CFHttp. --->
  • <cfhttp
  • method="get"
  • getasbinary="yes"
  • url="#strImageUrl#"
  • useragent="#CGI.http_user_agent#"
  • result="objImage">
  •  
  • <!--- Deal with hot-linking issues. --->
  • <cfhttpparam
  • type="header"
  • name="referer"
  • value="#GetDirectoryFromPath( strImageURL )#"
  • />
  •  
  • </cfhttp>
  •  
  •  
  • <!---
  • Check to make sure the get was successful.
  • This means that we got a 200 status code and
  • that the return type was some sort of image.
  • --->
  • <cfif (
  • FindNoCase( "200", objImage.StatusCode ) AND
  • StructKeyExists( objImage.ResponseHeader, "content-type" ) AND
  • FindNoCase( "image", objImage.ResponseHeader[ "content-type" ] )
  • )>
  •  
  • <!--- Grab the file type. --->
  • <cfset strExt = REReplace(
  • objImage.ResponseHeader[ "content-type" ],
  • "^.*?image[\\\/](\w+).*$",
  • "\1"
  • ) />
  •  
  • <cfelse>
  •  
  • <!--- The image was not valid in some way. --->
  • <cfset objImage = "" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Catch any errors. --->
  • <cfcatch>
  • <!--- Reset image. --->
  • <cfset objImage = "" />
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!---
  • Check to see if we now have a valid image to
  • work with. If something went wrong with the image,
  • then it will be a simple value.
  • --->
  • <cfif NOT IsSimpleValue( objImage )>
  •  
  • <!---
  • Add this image to the Zip. All we have to do is
  • grab the File Content from our CFHttp grab. I am
  • using the Hash() of the URL so as to not worry
  • about naming conflicts / overwriting.
  • --->
  • <cfzip
  • action="zip"
  • file="#strZipPath#">
  •  
  • <cfzipparam
  • entrypath="#Hash( strImageURL )#.#strExt#"
  • content="#objImage.FileContent#"
  • />
  •  
  • </cfzip>
  •  
  •  
  • <!---
  • Set the valid flag so that we know at least one
  • valid image was archived in the zip file.
  • --->
  • <cfset blnIsValidZip = true />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • Check to see if we have any zip files. If we do, then
  • we want to stream the zip. If we do not, then we want
  • to delete it.
  • --->
  • <cfif blnIsValidZip>
  •  
  • <!--- Set the header information. --->
  • <cfheader
  • name="content-disposition"
  • value="attachment; filename=images.zip"
  • />
  •  
  • <!---
  • Set the content and stream the zip file. Be sure
  • to delete it when the streaming is done.
  • --->
  • <cfcontent
  • type="application/zip"
  • file="#strZipPath#"
  • deletefile="true"
  • />
  •  
  • <cfelse>
  •  
  • <!--- Delete the unused Zip file. --->
  • <cffile
  • action="delete"
  • file="#strZipPath#"
  • />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: If we have made it this far then either the user
  • did not enter any URLs or none of the urls were valid. We
  • will need to render the form page once again.
  • --->
  •  
  •  
  • <!--- Set the content type and reset the buffer. --->
  • <cfcontent
  • type="text/html"
  • reset="true"
  • />
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>ColdFusion 8 CFZip / CFHttp Download Example</title>
  • </head>
  • <body>
  •  
  • <h1>
  • ColdFusion 8 CFZip / CFHttp Download Example
  • </h1>
  •  
  • <p>
  • Enter in your image URLs, one per line, in the
  • textarea below and then submit. Your images will
  • be downloaded, zipped, and returned to you.
  • </p>
  •  
  • <form action="#CGI.script_name#" method="post">
  •  
  • <label for="urls">
  • Image URLs:
  • </label>
  •  
  • <textarea name="urls" id="urls"></textarea>
  •  
  • <button>Zip Images</button>
  •  
  • </form>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

Check out the Online Demo if you want to see this in action. Hope that helps a bit.



Reader Comments

Roe
Nov 7, 2007 at 3:37 PM // reply »
9 Comments

Awesome dude, I was able to make it work but like you said I had to write the zip out to a file. The issue I was having was I was neglecting to use the getasbinary attribute!


Nov 7, 2007 at 4:06 PM // reply »
10,640 Comments

@Paul,

If you use the Java ZIP libraries, you can write the zip file directly to a byte array output stream, but then you lose the convenience of the CFZip tag.


Jan 19, 2010 at 1:19 PM // reply »
8 Comments

Streaming the zip is a brilliant solution. Every time I visit this site I think I get a little smarter.


Jan 19, 2010 at 1:24 PM // reply »
10,640 Comments

@Jon,

Ha ha, awesome :D Thanks!


Feb 28, 2011 at 5:46 AM // reply »
1 Comments

Link to the online demo is not working. Please check


Feb 28, 2011 at 10:12 AM // reply »
10,640 Comments

@Kasun,

Sorry, that domain (CF8Testing) no longer works. Try this one:

http://www.bennadel.com/resources/cf8testing/misc/cfzip_cfhttp.cfm


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »
Feb 9, 2012 at 10:29 PM
Learning ColdFusion 9: Application-Specific Data Sources
@Ben, No offence, but if people were really wanting advanced features they would be using a platform like ASP.NET MVC. CFML is so structurally compromised as a tag-based scripting language that ... read »
Feb 9, 2012 at 10:03 PM
Subversion - Cleanup Failed To Process The Following Paths
@Leviaguirre, do you still have problems with this? ... read »