Ask Ben: CFImage And Dynamic Image Compression With File Size Limits

Posted July 10, 2009 at 9:19 PM

Tags: ColdFusion, Ask Ben

Thanks so much! Still wish there was a function that would get the image's quality to see whether it needs to be reduced or not. Apparently, the way the quality attribute works is that if set at say .8, it reduces the existing quality by 20%. If user has already reduced quality before uploading, then setting quality to .8 or something reduces it even further. At a few Adobe CF gatherings, I've pointed out this need, but it doesn't seem to be coming after hearing more about the next CF release. Again, thanks! Appreciate the good and quick help.

Your understanding of the way image compression works is correct. Once an image is compressed, it essentially becomes a completely new image with 100% quality. At that point, any further compression will be based of the new image rather than the old image. While this can be frustrating, it's the only thing that's feasible; keeping the concept of compression over time would require the image to maintain its original image data.

However, I would guess that your concern isn't really about image quality at all; more likely, your concern is over file size. If file size wasn't the true concern, then you'd always use 100% quality. This would make the file size large, but the photo would look its best. The reason that we ever really want to reduce the quality of an image is to make the resultant file smaller in size (not to be confused with dimension, which remains constant). As such, I think that what you really want to do is reduce the quality of the target image only enough to get the resultant file size to drop under some arbitrary limit.

 
 
 
 
 
 
 
 
 
 

To be honest, I've never really worried about this problem before; typically, I would just pick a decently balanced image quality and use that as the quality going forward. As such, I can't really say that this algorithm is the right approach. And, if anyone else has a better suggestion, I'd love to hear it - I think this problem is quite fascinating. That said, the approach that I came up with below is a simple, iterative approach: we start with a given photo quality and try to save the image. We then get the file size of the resultant image and if it's too high, we slightly reduce the quality and repeat the compression process, overwriting the target file.

This might seem like a very intensive process, and in fact, I can practically hear some of you blowing steam out your ears at the thought of having the server do so much file access. In reality, however, the iterative approach outlined below is quite efficient. If you've ever played around with photo quality in a graphics program, you learn quickly that small changes in quality can lead to rather large changes in file size. As such, there's a naturally small limit to the number of iterations that we would ever need to execute.

On top of the natural limit, I've also opted for an initial quality of 95% rather than a 100%. This is because 100% quality (zero compression) is almost always too large. In fact, if you read in an image and then re-save it with 100% quality, the new image will generally be significantly larger than the original (which was, in its own right, already at 100% quality).

With that in mind, let's take a look at the code:

 Launch code in new window » Download code as text file »

  • <!--- Param form variables. --->
  • <cfparam name="form.upload" type="string" default="" />
  •  
  • <!--- Check to see if the form was uploaded. --->
  • <cfif len( form.upload )>
  •  
  • <!--- Upload the photo to a temp directory. --->
  • <cffile
  • result="upload"
  • action="upload"
  • filefield="upload"
  • destination="#expandPath( './' )#"
  • nameconflict="makeunique"
  • />
  •  
  • <!---
  • Read the image from the temp directory so that we
  • can resize it to fit in the given area.
  • --->
  • <cfimage
  • name="photo"
  • action="read"
  • source="./#upload.serverFile#"
  • />
  •  
  • <!--- Resize the image to fit in the specified box. --->
  • <cfset imageScaleToFit(
  • photo,
  • 500,
  • 500
  • ) />
  •  
  •  
  • <!---
  • Now that we have our resized image, we need to write it
  • to disk. Because we want to impose a file size limit,
  • we are going to keep trying to save the image until we
  • have a decent file size.
  • --->
  •  
  • <!---
  • Start out with 95% quality. The reason that we are not
  • starting out with full quality is that 100% usually
  • creates a file that is *surprisingly large*.
  • --->
  • <cfset imageQuality = 0.95 />
  •  
  • <!--- Set the max file size. --->
  • <cfset maxFileSize = (60 * 1024) />
  •  
  • <!--- Set the name of the resized image file. --->
  • <cfset photoFile = (
  • upload.serverFileName &
  • "-resized." &
  • upload.serverFileExt
  • ) />
  •  
  •  
  • <!--- Keep track of the number of iterations. --->
  • <cfset saveCount = 0 />
  •  
  • <!--- Start image save loop. --->
  • <cfloop condition="true">
  •  
  • <!---
  • Write the image to the disk with the current level
  • of compression.
  • --->
  • <cfimage
  • action="write"
  • source="#photo#"
  • destination="./#photoFile#"
  • quality="#imageQuality#"
  • overwrite="true"
  • />
  •  
  • <!--- Increment the save count. --->
  • <cfset saveCount++ />
  •  
  • <!--- Get the file size of the new image file. --->
  • <cfset fileSize = getFileInfo(
  • ExpandPath( "./#photoFile#" )
  • ).size
  • />
  •  
  •  
  • <!---
  • Check to see if the file size is greater than the
  • target file size. If it is not, then we need to
  • decrease the photo quality and try again.
  • --->
  • <cfif (fileSize gt maxFileSize)>
  •  
  • <!--- Reduce the quality by 5%. --->
  • <cfset imageQuality -= .05 />
  •  
  • <cfelse>
  •  
  • <!---
  • The file size is fine, so just break out of
  • the loop.
  • --->
  • <cfbreak />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>CFImage Dynamic Compression Demo</title>
  • </head>
  • <body>
  •  
  • <h1>
  • CFImage Dynamic Compression Demo
  • </h1>
  •  
  • <form
  • action="#cgi.script_name#"
  • method="post"
  • enctype="multipart/form-data">
  •  
  • <p>
  • <input type="file" name="upload" size="40" />
  • </p>
  •  
  • <p>
  • <input type="submit" value="Upload Photo" />
  • </p>
  •  
  • </form>
  •  
  •  
  • <!--- Check to see if we have an upload photo. --->
  • <cfif structKeyExists( variables, "photo" )>
  •  
  • <h2>
  • Resized Image<br />
  •  
  • <!--- Image properties. --->
  • Size: #numberFormat( fileSize, "," )#
  • (Max: #numberFormat( maxFileSize, "," )#)<br />
  •  
  • Quality: #imageQuality#<br />
  • Iterations: #saveCount#
  • </h2>
  •  
  • <p>
  • <img src="./#photoFile#" />
  • </p>
  •  
  • </cfif>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

As you can see, the algorithm is really simple - for each iteration, I reduce the image quality by 5%. As seen in the video, I actually spend about 2 hours trying to come up with something much more intelligent based on the differences in file size between save iterations; but, the differences in quality and efficiency were not worth the complexity of the overall code. As such, I reverted back to the uniform decrement in quality.

Like I said in the beginning, I've never worried about this before, but I hope that maybe it can inspire someone to come up with an even better idea.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Print Page



Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

David Labbe
Jul 11, 2009 at 8:04 AM // reply »
2 Comments

Great stuff Ben. Right now I am working on a similar project myself for the back end of a shopping cart. I am also adding in border option and possibly a text option if I have time. I think I will include the looping trick of yours...seems slick. However I am useing the CF* image functions and not the cfimage tag itself...is there a advantage or disadvantage either way? I found with the function I can do something like..

<cfset ImageSharpen(myImage,"#sharpen#")>
<cfset ImageResize(myImage,"#imageWidth##widthPx_prcnt#","#imageHeight##heightPx_prcnt#","#quality#")>

I can then simply add a list of functions on upload. (the variables are input from a database).

Anyway...first try around with images in CF...awesome stuff

David


Jul 11, 2009 at 5:56 PM // reply »
5 Comments

Thanks, Ben, for ongoing thought on this. Checked a bit to see if cfimage data was any different on a lower quality image than higher one. Nothing different, so nothing to read there that might give info.

Building on your filesize idea, I tried to see if any trends that would show up among various sizes (dimensions) of images as well as quality in terms of something like bytes/sq pixel (thinking something like density). Couldn't see anything that made sense, as data was quite varied and didn't seem to translate to something usable. (I had thought this might be a little more valuable than just total file size.) Some higher quality pics had lower *density* than other much lower quality pics.

In the end, you are absolutely correct: the issue is file size (not actual quality). So, you iterations to reduce file size to a max level seems most practical and straightforward.

will keep on this, though to see if anything else makes sense as a way to get original quality so can then take care of reduction in one step (if necessary). I use Fireworks for most image work: wonder what data it "reads" on a jpg to get its *quality* number?

Thanks for sharing your approach. From it, I'm also learning more about the value of some of the image tags and file funtions (rather than cffile and cfdirectory).

Keith


Jul 12, 2009 at 5:26 PM // reply »
6,371 Comments

@David,

No, there should be no difference between using the CFImage tags and using the Image-based functions. Of course, other than that more functionality is available via the various functions.

@Keith,

Yeah, I have also noticed that Fireworks does seem to get some sense of "Quality" when it opens an image. I am not sure where that comes from.

As far as trending, I couldn't come up with anything. For some images, a 5% change in quality created a large file size change where as in others, 5% change hardly affected the file size at all. In fact, I had some test images that needed to drop to 20% quality before I would drop below a given file size (keep in mind, all of these images fit into 500x500).

I am not sure that a trend can even be found; I tried for 2 hours to play but came up with nada. As such, I think the incremental drop (which can be made even smaller) is the way to go.


Jul 12, 2009 at 5:53 PM // reply »
5 Comments

I tried a bit more today and nothing that would indicate any trend. Must just be too many variables at work. Your file size stuff makes the most sense--until perhaps, someone comes up with a way to read quality directly.

Thanks for all the help! Will post again if I find anything of interest on this topic.

Keith


Jul 13, 2009 at 3:49 PM // reply »
6 Comments

Thanks so much for showing me how to go about this! This is awesome! I love coldfuison!!! I am currently building a shopping cart for my company and needed a way to dynamically resize and optimize images that they upload. Now I understand how to do that!

Thanks sooooo much!!!

Nathan


Jul 13, 2009 at 3:51 PM // reply »
6,371 Comments

@Nathan,

Glad to help out. If you get stuck on anything let us know.


Jul 13, 2009 at 3:54 PM // reply »
6 Comments

Will do! I did have one question...how could I make this generate a thumb, medium and large photo all at the same time? I am fairly new to CF and I tell you it is the easiest most powerful thing I have ever learned!

Any help would greatly appreciated!

Thanks!

Nathan


Jul 13, 2009 at 4:19 PM // reply »
5 Comments

@Nathan,

If I understand the question, it's quite easy.
(In my app, I make a thumb right after the last save of the full file.)

Since you have the image object (In Ben's code--"photo") you just do another ImageScaleToFit (or whichever function for your needs) on the photo object; then do a "write" to wherever the thumb to be saved and name for it.

I want my full one to have max dimension of 475, so on my first ImageScaleToFit, I use that as the dimensions. Then for my thumb, I do the same but use 110 as my dimensions. All is done with the original "photo" object.
a. Make the photo obj (using the cfimage on the temp saved image)
b. Scale the obj for the full, saving that (by cfwrite). The obj is still available for further manipulation.
c. Now scale the obj for the thumb size and then write that as an image file.

For what you said you're trying to do, I'd assume you'd work from largest to smallest.

One thing I do before the scaletofit, is to use the data in the photo obj to see if resizing is necessary. Such as:
if photo.width gt maxDimension or photo.height gt maxDimension: if that, then do the resize, if not just write it as it is.

Hope this helps. Try a CFDump on your photo object and you can see the data available.

Keith


Jul 13, 2009 at 6:00 PM // reply »
6,371 Comments

@Nathan,

@Keith is right on. If you want to see a write up of this for clarification, just let me know.


Jul 14, 2009 at 9:57 AM // reply »
6 Comments

I think I understand...going to try and implement it...if I get a chance to today...busy busy busy


Jul 14, 2009 at 10:07 AM // reply »
6 Comments

If I can't figure it out though I will let ya know


Jul 14, 2009 at 1:26 PM // reply »
6 Comments

Hey Ben! I had a chance to work with it...but can't seem to get it working...
I tried looping over the image to resize it to the appropriate size. Then I looped through it to compress, or optimize it. I then printed the optimized image to the screen. This all worked...until I uploaded it to a webserver, it didn't care for the ./, being a n00b I don't really know why. Could it be that it needs the direct reference, like C:\Inetpub, etc.?

Thanks for any help!

Nathan


Jul 14, 2009 at 2:35 PM // reply »
6,371 Comments

@Nathan,

The "./" just means look in "this" directory. It depends on where you upload your files to. If you use a separate upload directory or getTempDirectory(), then you have to look there.


Jeff
Jul 31, 2009 at 3:26 PM // reply »
4 Comments

So your topic is more about image file size and compression.

Let's back up a step on your process of meeting a file size by looping through and checking file size and adjusting quality. Let's just simply talk about the quality / compression. Are they the same?

I have 200 - 400 images being re-sized every day on one of my sites which makes server resource utilization important. There have been some problems with memory and CPU utilization.

I have never adjusted the QUALITY settings on my resize function. So the question is ... if quality is the same as compression, doesn't compression take more processing power than no compression?

any thoughts would be appreciated.


Aug 5, 2009 at 9:39 AM // reply »
6,371 Comments

@Jeff,

I am by no means an image expert, so I am sorry if I am missing something; but, how do you adjust the file size without changing the compression?


Jeff
Aug 5, 2009 at 10:15 AM // reply »
4 Comments

@Ben,

Guess my question was ...

Do you know which uses more CPU? quality = 1.0 or quality = 0.5? I haven't been able to isolate this and benchmark it.


Aug 5, 2009 at 10:19 AM // reply »
6,371 Comments

@Jeff,

Hmm, interesting question. The lower the quality, the more the image has to be compressed. That's really what quality is - the amount of compression that is taking place. The less compression, the higher the quality (and file size) and vice-vesa.

I suppose, the more it has to compress an image, the more calculations it has to make? But, that is just a total guess, not based on anything.


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 7, 2009 at 5:53 PM
Ask Ben: Javascript String Replace Method
You can find here an advanced function that prepared with javascript replace function. This can make the first letters of words, sentences, lines and whatever you define automatically: http://www.m ... read »
Andrew Neely
Nov 7, 2009 at 4:56 PM
A Moment That Touched Me - The Fountainhead
Ben, Glad you enjoyed the podcast. Yeah, the Tank Riot guys can get really chatty during the episodes, but that's part of the charm of it for me. They've covered everything from Nichola Tesla to Cha ... read »
Nov 7, 2009 at 4:43 PM
Building A Fixed-Position Bottom Menu Bar (ala FaceBook)
Is it possible to make some more MenĂ¼`s ? ... read »
Jill
Nov 7, 2009 at 11:40 AM
How To Unformat Your Code (Like A Pro)
Derek, I think you might be right - sweet! Thanks for the link :) ... read »
Nov 7, 2009 at 11:25 AM
How To Unformat Your Code (Like A Pro)
I think it would be way easier to just use this http://www.logichammer.com/html-formatter/ He just released v3 and it rocks. ... read »
Jill
Nov 7, 2009 at 7:58 AM
How To Unformat Your Code (Like A Pro)
LMAO - this was pretty funny! I have to admit - I also love to reformat code so I can read it. My boss used to tell me to leave my OCD at home. Now I don't feel so bad after reading everyone else' ... read »
Nov 6, 2009 at 10:10 PM
How To Unformat Your Code (Like A Pro)
The timing of this post is just uncanny. I spent the last 15-20 minutes manually un-formatting my "Ben Nadel" style code within a CFC of mine. I was really digging the readability a few weeks ago, bu ... read »
Roe
Nov 6, 2009 at 5:11 PM
Passing Arrays By Reference In ColdFusion - SWEEET!
ArraySort also reorders the results of these java obj's ... read »