The blog of dlaa.me

Bigger isn't always better [How to: Resize images without reloading them with WPF]

I've been doing some work with Windows Presentation Foundation lately and came across a scenario where an application needed to load a user-specified image and display it at a fairly small size for the entire life of the application. Now, WPF makes working with images easy and I could have simply used the Image class's automatic image scaling and moved on. But it seemed wasteful for the application to keep the entire (arbitrarily large!) image in memory forever when it was only ever going to be displayed at a significantly reduced size...

What I really wanted was a way to shrink the source image down to the intended display size so the application wouldn't consume a lot of memory storing pixels that would never be seen. The WPF documentation notes that the most efficient way to load an image at reduced size is to set Image's DecodePixelWidth/DecodePixelHeight properties prior to loading it so WPF can decode the original image to the desired dimensions as part of the load process. However, the comments in one of the overview's samples explain why this isn't suitable for the aforementioned scenario:

// To save significant application memory, set the DecodePixelWidth or
// DecodePixelHeight of the BitmapImage value of the image source to the desired
// height or width of the rendered image. If you don't do this, the application will
// cache the image as though it were rendered as its normal size rather then just
// the size that is displayed.
// Note: In order to preserve aspect ratio, set DecodePixelWidth or
// DecodePixelHeight but not both.

Basically, the problem with this approach occurs when an application doesn't know the aspect ratio of the image before loading it: the application doesn't know whether to set DecodePixelWidth or DecodePixelHeight to constrain the larger dimension. If the application picks the right one (width vs. height), then the image will be properly resized to fit within the bounds of the application - but if it picks the wrong one, then the image will be resized some but will still be unnecessarily large (though less unnecessarily large than before!). While the application could arbitrarily pick one dimension to constrain, load the image, check if it guessed correctly, and reload the image with the other constraint when necessary, I was looking for something a little more deterministic. After all, sometimes you get only one chance to load an image - or someone else loads it for you - or the cost of loading it a second time is prohibitive, so it's nice to have a way to dynamically resize an already-loaded image.

After a search of the documentation didn't turn up anything promising, I wrote a small helper function using the handy RenderTargetBitmap class to generate a new, properly sized image based on the original. The code for that method ended up being fairly simple:

/// <summary>
///
Creates a new ImageSource with the specified width/height
/// </summary>
///
<param name="source">Source image to resize</param>
///
<param name="width">Width of resized image</param>
///
<param name="height">Height of resized image</param>
///
<returns>Resized image</returns>
ImageSource CreateResizedImage(ImageSource source, int width, int height)
{
  
// Target Rect for the resize operation
  Rect rect = new Rect(0, 0, width, height);

  
// Create a DrawingVisual/Context to render with
  DrawingVisual drawingVisual = new DrawingVisual();
  
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
  {
    drawingContext.DrawImage(source, rect);
  }

  
// Use RenderTargetBitmap to resize the original image
  RenderTargetBitmap resizedImage = new RenderTargetBitmap(
      (
int)rect.Width, (int)rect.Height,  // Resized dimensions
      96, 96,                             // Default DPI values
      PixelFormats.Default);              // Default pixel format
  resizedImage.Render(drawingVisual);

  
// Return the resized image
  return resizedImage;
}

[Note: If you want to see this code in action, you can download the complete source code for a sample application (including Visual Studio 2008 solution/project files) that's attached to this post (click the WpfResizeImageSample.zip link below).]

Basically, the CreateResizedImage method works by creating a new image of exactly the size specified and then drawing the original image onto the new, blank "canvas". WPF automatically scales the original image during the drawing operation, so the resulting image ends up being exactly the right (smaller) size. All that remains is for the calling application to do a bit of math on the original image's dimensions to determine how to scale it, pass that information along to CreateResizedImage to get back a properly sized image, and then discard the large original image. It's that easy.

WPF and XAML make it easy to author compelling user interfaces. But sometimes it's worth a little extra effort to optimize some aspect of the user experience. So if you're looking to trim the fat from some of your in-memory images, consider something like CreateResizedImage to help you out!

[WpfResizeImageSample.zip]

Tags: WPF