Thursday, April 02, 2009

Aspect Ratio & You

There are no shortages of libraries and toolkits available to programmers for scaling images. But if you ever find yourself in a position, as I recently have, where you need to roll your own (or maybe you are just curious) I'll explain everything you need to know about maintaining the aspect ratio of a scaled image.

When the issue of scaling images landed on me, the first thing I did was to google it. The search results were not very satisfactory, thus this blog entry.

So what are we talking about when we use the term "aspect ratio"? It's the relationship between an image's height and its width. From a programmatic point of view, the aspect ratio can tell us how much the width of an image should change if the height changes and vice versa. Aspect ratios are normally expressed in the form H:W (i.e., 1:1, 7:3, 4:5, etc). It can also be expressed as a fraction (i.e. 1/1, 7/3, 4/5, etc) and finally, for programmatic purposes, a decimal. The formula for the aspect ratio is:

A = H/W
where A is the aspect ratio, H is the height of the image, and W is the width of the image. Using a bit of algebra we can rewrite the formula to solve for any of the variables. So given that A = H/W then H = A*W and W = H/A.

Lets assume we have an 485px x 1024px image that we need to generate a thumbnail for. The first thing we need to do is determine the aspect ratio of the image:

A = H/W => A = 485/1024 => A = 0.4736328125
Lets also assume that we have this rule that says a thumbnail image must be no more than 140 pixels high. We now have enough information to figure out what the width must be in order to maintain the image's aspect ratio:
W = H/A => W = 140/0.4736328125 => W = 295.587628866
We know the new width maintains the aspect ratio because 140/295.587628866 = 0.4736328125. Now let's look at some code:
/** 
 * Scale <tt>src</tt>'s dimensions to <tt>max</tt> pixels starting w/ the largest side. 
 * 
 * @param image      The source image. 
 * @param max        The maximum number of pixels in each dimension(HxW). 
 * @param heightOnly Indicates that only the image's height should be scaled. 
 * 
 * @return The scaled image. 
 */ 
public static BufferedImage scale(BufferedImage image, final int max, boolean heightOnly) 
{ 
    if (heightOnly) 
        image = scaleByHeight(image, max); 
    else if (image.getHeight() > image.getWidth()) 
    { 
        image = scaleByHeight(image, max); 
        image = scaleByWidth(image, max); 
    } 
    else 
    { 
        image = scaleByWidth(image, max); 
        image = scaleByHeight(image, max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> by <tt>height</tt>. 
 * 
 * @param image The source image. 
 * @param max   The value to scale the image down to. If the current height of the image is less than <tt>max</tt> then this 
 *              method does nothing. 
 * 
 * @return A (possibly) scaled image. 
 */ 
public static BufferedImage scaleByHeight(BufferedImage image, final int max) 
{ 
    int height = image.getHeight(); 
    if (height > max) 
    { 
        int width = image.getWidth(); 
        final float aspectRatio = height / (float)width; 
        do 
        { 
            height >>= 1; 
            if (height < max) 
                height = max; 
            int k = (int)(height / aspectRatio); 
            if (k > 0) 
                width = k; 
            image = scale(image, height, width); 
        } 
        while (height > max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> by <tt>width</tt>. 
 * 
 * @param image The source image. 
 * @param max   The value to scale the image down to. If the current width of the image is less than <tt>max</tt> then this 
 *              method does nothing. 
 * 
 * @return A (possibly) scaled image. 
 */ 
private static BufferedImage scaleByWidth(BufferedImage image, final int max) 
{ 
    int width = image.getWidth(); 
    if (width > max) 
    { 
        int height = image.getHeight(); 
        final float aspectRatio = height / (float)width; 
        do 
        { 
            width >>= 1; 
            if (width < max) 
                width = max; 
            int k = (int)(width * aspectRatio); 
            if (k > 0) 
                height = k; 
            image = scale(image, height, width); 
        } 
        while (width > max); 
    } 
    return image; 
} 
 
/** 
 * Scale <tt>src</tt> down to height x width pixels. 
 * 
 * @param src    The source image. 
 * @param height The scaled height. 
 * @param width  The scaled width. 
 * 
 * @return The scaled image. 
 */ 
private static BufferedImage scale(BufferedImage src, final int height, final int width) 
{ 
    int type = src.getType(); 
    if (BufferedImage.TYPE_CUSTOM == type) 
        type = src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 
    BufferedImage img = new BufferedImage(width, height, type); 
    Graphics2D gscale = img.createGraphics(); 
    gscale.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 
    gscale.drawImage(src, 0, 0, width, height, null); 
    gscale.dispose(); 
    return img; 
}