Resizing Uploaded Images in ASP.NET

When you create a web-based application that will be used by the general public, you have to be ready for users who are not technically savvy. Your interface must be clear, and it must not assume technical skills that your users may or may not have.

If your interface allows users to upload images, you’ll quickly discover just how ignorant many people are about how image files work and how to optimize graphics for the web. I’ve struggled with this problem since about 1998, and have applied various solutions to solve it.

Key Technologies and Concepts

Resizing Bitmap Images

System.Drawing.Graphics

System.Drawing.Bitmap

Maintaining Aspect Ratio

Resizing Browser Uploads

Resizing Server Image Files

Dealing with Multi-Megapixel User Images

I recently created a real estate application in ASP.NET for a client who wanted the application to work essentially like a membership site. Users could upload a photo of themselves, a logo for their company, and the images they wanted to show of their property. One of the things the application needed to do was resize the "primary" photo down to a thumbnail. Also, all of the property photos had to be limited to a certain width.

After a few customer beta testers got involved, we discovered that we needed to include the resizing logic everywhere we uploaded photos. For example, members would routinely upload an enormous image for their head shot, even though the site obviously displays a relatively small picture. I’m sure these photos came straight from their camera.

Certainly, when visitors used the public site to look at listings, their browser could automatically resize these images on the fly at display time, but the problem with that scenario is that the entire multi-megapixel image has to be downloaded to the visitor’s browser for it to do that. Site performance would suck.

We could have instructed members to resize the photo to a specific size themselves, or reject images that didn’t fit our size limitations, but that’s not a very user-friendly solution, and it assumes the member has the tools and knowledge to manipulate photos.

We also could have gotten really fancy and provided image cropping and other tools in the application itself, but we wanted to keep it simple. Members are there to get their listing online, not fiddle with images.

Resizing On-the-Fly

We decided that the best solution was to just automatically resize all images as they were uploaded. That way, the stored images would take up less disk space and they would already be the correct size when displayed in the browser.

I knew I’d need the resizing code in multiple places, so I naturally created a helper class to handle the task. That class is what I’ll show you here.

The trick to resizing is easy once you know what to do. The .NET Framework provides everything you need. Specifically, the Bitmap and Graphics classes provide basic photo manipulating features that you can easily control with program code.

Here is the full listing for the BitmapHelper class:

using System;
using System.IO;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Web.UI.WebControls;
 
namespace NerdyMusings.DemoSite.Components {
  public class BitmapHelper {
    public static void SaveImageFile(
        String sourceImagePath,
        String saveImagePath,
        int maxImageWidth) {
      Bitmap sourceImage = new Bitmap(sourceImagePath);
      SaveImageFile(sourceImage, saveImagePath, maxImageWidth);
    }
 
    public static void SaveImageFile(
        FileUpload clientFile,
        String saveImagePath,
        int maxImageWidth) {
      Bitmap sourceImage =
        new Bitmap(clientFile.PostedFile.InputStream);
      SaveImageFile(sourceImage, saveImagePath, maxImageWidth);
    }
 
    public static void SaveImageFile(
        Bitmap sourceImage,
        String saveImagePath,
        int maxImageWidth) {
      // Resize if source image width is greater than the max:
      if (sourceImage.Width > maxImageWidth) {
        int newImageHeight = (int)(sourceImage.Height *
          ((float)maxImageWidth / (float)sourceImage.Width));
        Bitmap resizedImage = new Bitmap(maxImageWidth, newImageHeight);
        Graphics gr = Graphics.FromImage(resizedImage);
        gr.InterpolationMode = InterpolationMode.HighQualityBicubic;
        gr.DrawImage(sourceImage, 0, 0, maxImageWidth, newImageHeight);
        // Save the resized image:
        resizedImage.Save(saveImagePath, FileExtensionToImageFormat(saveImagePath));
      } else {
        // Save the source image (no resizing necessary):
        sourceImage.Save(saveImagePath, FileExtensionToImageFormat(saveImagePath));
      }
    }
 
    private static ImageFormat FileExtensionToImageFormat(String filePath) {
      String ext = Path.GetExtension(filePath).ToLower();
      ImageFormat result = ImageFormat.Jpeg;
      switch (ext) {
        case ".gif":
          result = ImageFormat.Gif;
          break;
        case ".png":
          result = ImageFormat.Png;
          break;
      }
      return result;
    }
  }
}

The class provides a SaveImageFile method that is overloaded with three signatures. The overloads provide three variations on the first parameter:

  • String sourceImagePath (for resizing an existing server file)
  • FileUpload clientFile (for resizing a browser upload file)
  • Bitmap sourceImage (for resizing an image already loaded into a bitmap)

The first two overloads exist for convenience only. They handle the pre-loading of a Bitmap object from either a file or a browser request stream. The third overload is where all the real work is done.

All three overloads take two additional parameters:

  • String saveImagePath (pass a fully-qualified server file path)
  • int maxImageWidth (pass the maximum width for the saved image)

Now take a look at the third SaveImageFile overload in the listing above. The method has two main logic branches, depending upon whether or not the image needs to be resized. If the source image is wider than the maximum allowed width, the method resizes the image and saves the resized version. If the source image does not need to be resized, it just saves the source image.

Here’s what the resizing logic does, step-by-step:

Calculate the new image height, based on the bitmap’s current proportions:

int newImageHeight = (int)(sourceImage.Height *
  ((float)maxImageWidth / (float) sourceImage.Width));

The idea here is that you just want to resize the image, not change its aspect ratio (width divided by height). Breaking it down, you can see that the new image width (maxImageWidth) divided by the bitmap’s current width (sourceImage.Width) tells me how much I’m reducing the image, so I can multiply the bitmap’s current height (sourceImage.Height) by that same factor.

Create a new bitmap at the new image size:

Bitmap resizedImage = new Bitmap(maxImageWidth, newImageHeight);

The Bitmap class includes a constructor that lets you pass the dimensions of image to be created. Once the resizedImage bitmap is allocated, it is essentially an empty canvas.

Get a reference to the Graphics instance:

Graphics gr = Graphics.FromImage(resizedImage);

The drawing tool you need to use for resizing the original bitmap comes from the Graphics object. The Graphics object needs some kind of graphical context to work within, in other words, it needs to be associated with a "drawing surface" of some kind. Since I want to draw in the resizedImage bitmap object I just created, I get a Graphics object associated with that drawing surface.

Adjust quality settings:

gr.InterpolationMode = InterpolationMode.HighQualityBicubic;

The Graphics object offers several options that let you control how your image is rendered, including CompositingQuality, InterpolationMode, and SmoothingMode. After messing around with these settings, I found that InterpolationMode had the biggest impact on the size and quality of the image.
HighQualityBicubic seemed to return the best looking image with an acceptable disk footprint. Feel free to experiment with these settings yourself and select the options that work best for your requirements.

You can also delve into the JPEG encoder to gain more control over things like compression ratio in JPEG files. Thanks to reader Joe Fawley for passing on this MSDN link on the subject:

  Encoder.Quality Field

Draw the resized image:

gr.DrawImage(sourceImage, 0, 0, maxImageWidth, newImageHeight);

Call the DrawImage method of the Graphics object to draw the original bitmap (sourceImage) into the resized bitmap (resizedImage). The DrawImage method supports quite a few signature overloads, one of which lets you specify the position and size of the area you want to fill. I want to fill the entire resized image, so the starting position is 0, 0 and the size matches the resized bitmap dimensions.

Save the resized image:

resizedImage.Save(saveImagePath, FileExtensionToImageFormat(saveImagePath));

If you don’t specify a file format, the default appears to be PNG. This is true regardless of what extension you specify, so you can accidentally save a PNG file with a .jpg extension. A Nerdy Musings reader encountered difficulties opening resized jpg files in an image editing program, and when I investigated the problem, I discovered that it is essential to specify the output format you want to use when you save the file to disk (assuming you want something other than PNG).
Oddly enough, the misnamed files display fine in a browser, but an image editing program gets confused by the discrepancy between the file extension and the actual format of the file.

I use the FileExtensionToImageFormat method to select the format that matches the extension of the file being saved. You can save the image in any file format supported by the Bitmap object. My application
only works with JPG, GIF, and PNG files, so if you work with others, you will need to tweak FileExtensionToImageFormat accordingly.

That’s it! You just resized a bitmap and saved it to a folder on the server.

Variations on the Theme

My implementation of the image resizing algorithm assumes you are mostly concerned with the width of the image, not the height. On most Web sites, the width of the page is more critical than the height because scrolling left-to-right is not generally considered acceptable while scrolling top-to-bottom is quite common. Resizing based on width keeps the images within the design limitations of the web page while allowing the resized image to remain proportional to the original.

However, your requirements may dictate that images must fit within a placement that limits both the width and height. In that case, you could add a maxImageHeight parameter as well and resize the image based on both a maximum width and a maximum height.

Resizing isn’t the only thing you can do with the Graphics.DrawImage method. It has something like 30 overloads that give you a great deal of flexibility regarding how you manipulate images. For example, you could crop your images to a specific aspect ratio and then resize them to fit perfectly into a particular placement.

There’s one thing to look out for with regard to resizing images: avoid making them larger than the original. For example, if you resize all images to a standard width even though the source image may be smaller, you will end up with some fuzzy images. Resizing an image to a smaller size produces reasonably good results, but expanding them almost never does. Those TV shows where you see someone zoom way in on a fuzzy, pixelated picture and "sharpen it" to pull out some tiny detail are pure fantasy.

Check User File Types

As with any situation where you are getting information directly from an end user, you should do as much validation as possible to avoid problems in the application. For example, it is a good idea to make sure that your users are uploading supported image formats. The Bitmap class supports a limited number of file formats, although it covers the basics (BMP, GIF, EXIG, JPG, PNG and TIFF).

You have a couple of choices regarding checking the file type. Both use the PostedFile property of the FileUpload control:

  • Use the PostedFile.ContentType property to check the file’s MIME type.
  • Get the PostedFile.FileName property and check the extension of the uploaded file.

Neither option is perfect because users can set the file extension or spoof the MIME type, so you should have graceful exception handling when an unsupported type blows up when you try to load it into the Bitmap object.

Have Fun with Photos

The technique I demonstrate in this article for programmatically resizing photos is just one way to do things, of course. The Graphics class gives you the power to perform many standard photo manipulations, including cropping, and stretching.

Using the examples in this article, you now know how to load an image into a Bitmap object and manipulate it using the Graphics object.