Creative Juices Bo. Co.

Satisfy Your Thirst For Something Refreshing!

CJ Image 1.0

Easily Calculate Image Scaling to Fit or Fill a Destination Box

CJ Image: An Image Scaling Algorithm

One of the most requested functions that my customers seem to ask, is that they want to upload their own images to the website. Technically, it's not difficult, especially if your using a server technology like ColdFusion, php, etc, but from a design perspective, it's a big headache. When designing a website, you create "blocks" (for lack of a better term) that are either going to be used for text or images. You might use an image editing programing like Adobe® Photoshop® to scale and crop the images to make sure they load fast and fit into the block's dimensions. Unfortunately, most of our customers do not have these tools and wind up trying to upload an image that are to large and weren't really designed to fit within our designated "block".

We needed to create a function that would take our customers uploaded image and scale it into these blocks. Not knowing what the customer was going to upload, we had to create something that was dynamic enough to adapt to what ever size the final destination image was going to be and also to the source image the user uploads.

For example, let's say we have a slide show that's 500 x 150 pixels in dimension. The client has an images that are 640 x 480. Or maybe you have a photo gallery page with thumbnails that are 150 x 150 pixels. And the client's images vary in size. When a user clicks on one of the thumbnails, we need to display the full image with no cropping. With both of these scenerios, you really only have two scaling methods:

  1. We can scale the source image to fill the destination box. (Scale to fill)
    This might cause a portion of the source image to be cropped or offset from the destination box. Fig. 1A

  2. We can scale the entire source image to fit into the destination box. (Scale to fit)
    This might cause a gap on some of the sides, because the source image might not be proportional to my destination box. Fig. 1B

Scale to FILL and FIT example
FIG. 1 - Example of the two type of scaling methods.
(A) The left shows SCALE-TO-FILL and (B) on the right we are using SCALE-TO-FIT.

For most of the slideshows and photo gallery thumbnails, we want to fill the entire destination block with the source image (Scale to fill). But for the full photo gallery image, we want to show the full image (Scale to fit).

You're probably thinking, well why don't you just crop the images when the client uploads them. Good question. The answer is simple, we reuse a lot of what our clients upload for other things on the site. These destination blocks typically have different dimensions. We might use photo gallery images for slideshows and visa versa. Another reason is because we don't always use server technology to upload images. Sometimes the client uploads them directly to the site via FTP, which means we needed something that was simple enough to do with client-side scripting.

This brings us back to our function. We need something that we can pass the source and destination dimensions and also the scaling method we want to use. It needs to return the final scaled width and height and tell us the offset values from the destination block.

 

Scaling Principle

One of the biggest stumbling blocks when creating this function was getting caught up with the whole "Is it a landscape, portrait or square?" thing. If you go this route, you'll go crazy trying to compare each of these for both the source and destination box.

The solution is realy quite simple. What you want to do is determine the RATIO between the destination and source x-axis (the widths) and y-axis (the heights). These ratios will then be used to multiply against source images dimensions, depending on the scaling method you choose.

// Find the ratio between the destination width and source width.
RATIO-X = DESTINATION-WIDTH / SOURCE-WIDTH

// Find the ratio between the destination height and source height.
RATIO-Y = DESTINATION-HEIGHT / SOURCE-HEIGHT

// To use the SCALE TO FILL method, 
// we use the greater of the two ratios, 
// let's assume the RATIO-X is greater than RATIO-Y.
SCALE-W = RATIO-X * SOURCE-WIDTH
SCALE-H = RATIO-X * SOURCE-HEIGHT

// To use the SCALE TO FIT method, 
// we use the lesser of the two ratios, 
// let's assume the RATIO-Y is less than RATIO-X.
SCALE-W = RATIO-Y * SOURCE-WIDTH
SCALE-H = RATIO-Y * SOURCE-HEIGHT

To figure out the offset of the new scaled image compared to the center of the destination box, you must take the scaled image dimensions and subtract them from the destination dimensions and divide that number in half.

// Find the offsets to center the image compared to the destination box.
OFFSET-X = ( DESTINATION-WIDTH - SCALE-WIDTH ) / 2
OFFSET-Y = ( DESTINATION-HEIGHT - SCALE-HEIGHT ) / 2

That's all there is too it.

 

Source Code

Javascript Source

Place the following JavaScript source code in your HTML document, before any functions that may make a call to it.

var CJ_Image = {
   Calculate: function(options) {
      // declare some local variables
      var ratioX, ratioY, scale, newWidth, newHeight;
      // Check to make sure all the required variables were sent and pass validation
      if (typeof options.srcWidth !== "number" || typeof options.srcHeight !== "number" || typeof options.destWidth !== "number" || typeof options.destHeight !== "number" || typeof options.method !== "string") {
         return;
      }
      // Grab scale ratios
      ratioX = options.destWidth / options.srcWidth;
      ratioY = options.destHeight / options.srcHeight;
      // Determine which algorithm to use
      if (options.method === "fit") {
         scale = ratioX < ratioY ? ratioX: ratioY;
      } else if (options.method === "fill") {
         scale = ratioX > ratioY ? ratioX: ratioY;
      }
      // Set new dimensions
      newWidth = parseInt(options.srcWidth * scale, 10);
      newHeight = parseInt(options.srcHeight * scale, 10);
      // Return the new dimensions, plus the offsets, and if the destination box
      // is smaller or equal to the source image dimensions
      return {
         width: newWidth,
         height: newHeight,
         offset: {
            x: parseInt((options.destWidth - newWidth) / 2, 10),
            y: parseInt((options.destHeight - newHeight) / 2, 10)
         },
         fits: options.srcWidth >= options.destWidth && options.srcHeight >= options.destHeight ? true: false
      };
   }
};

The following snippet is an example of how it might be called and used:

function ScaleMyImage() {
   var thumbNail = CJ_Image.Calculate({
      srcWidth: 640,
      srcHeight: 480,
      destWidth: 100,
      destHeight: 100,
      method: "fill"
   });
   var str = "New Image Width = " + thumbNail.width + "\n";
   var str += "New Image Height = " + thumbNail.height + "\n";
   var str += "X Offset = " + thumbNail.offset.x + "\n";
   var str += "Y Offset = " + thumbNail.offset.y + "\n";
   var str += "Does it fit my thumbnail box = " + thumbNail.fits;
   document.write(str);
}

 

ColdFusion Source

I haven't forgotten about my roots, here's the same function for ColdFusion.

<cffunction name="CJ_Image" returntype="struct" output="false" hint="A simple function that will return the width, height and offset of scaled image thumbnail.">
   <cfargument name="srcWidth" type="numeric" required="yes" hint="The width of the source image." />
   <cfargument name="srcHeight" type="numeric" required="yes" hint="The height of the source image." />
   <cfargument name="destWidth" type="numeric" required="yes" hint="The width of the destination image." />
   <cfargument name="destHeight" type="numeric" required="yes" hint="The height of the destination image." />
   <cfargument name="method" type="string" required="no" default="fit" hint="The scaling method to use to calculate the thumbnail image dimensions." />
   <!--- Initialize our variables --->
   <cfset var err = "" />
   <cfset var scale = "" />
   <cfset var fits = false />
   <cfset var thumbInfo = StructNew() />   <!--- Grab scale ratios --->
   <cfset var xscale = destWidth / srcWidth />
   <cfset var yscale = destHeight / srcHeight />
   <!--- Determine which scaling method is to be used --->
   <cfif method eq "fit">
      <cfset scale = Min(xscale, yscale) />
   <cfelseif method eq "fill">
      <cfset scale = Max(xscale, yscale) />
   </cfif>
   <!--- Determine if the destination is smaller or equal to the source image --->
   <cfif srcWidth gte destWidth and srcWidth gte destWidth>
      <cfset fits = true />
   </cfif>
   <!--- Set new dimensions --->
   <cfset err = StructInsert(thumbInfo, "width", Round(srcWidth * scale)) />
   <cfset err = StructInsert(thumbInfo, "height", Round(srcHeight * scale)) />
   <cfset err = StructInsert(thumbInfo, "offset", StructNew()) />
   <cfset err = StructInsert(thumbInfo.offset, "x", Round((destWidth - (srcWidth * scale)) / 2)) />
   <cfset err = StructInsert(thumbInfo.offset, "y", Round((destHeight - (srcHeight * scale)) / 2)) />
   <cfset err = StructInsert(thumbInfo, "fits", fits) />
   <!--- Return the information --->
   <cfreturn thumbInfo />
</cffunction>

... and a sample on how you can use it:

<!--- Read an image in (Uses CFIMAGE, but you can do this with other methods) --->
<cfimage action="read" name="MyPhoto" source="C:\inetpub\myServer\gallery\img_01.jpg">
<cfset thumbNail = CJ_Image(MyPhoto["width"], MyPhoto["height"], 150, 125, "fit")>
<cfoutput>
   width: #thumbNail.width#<br />
   height: #thumbNail.height#<br />
   offset X: #thumbNail.offset.x#<br />
   offset Y: #thumbNail.offset.y#<br />
   does fit: #thumbNail.fits#<br />
</cfoutput>