« Photoscroll -- the worst named WPF demo application with source code! | Main | CommandTarget, MenuItem, ContextMenus, and Binding, oh my! »

Extracting thumbnails from a JPEG/JPG in .NET 3.0

.NET 3.0 contains a vast array of media related classes. So many that at times it can be overwhelming! Classes that relate to image handling and manipulation number greater than ten!

In this post though, I'll discuss only a few of those that relate to retrieving or creating a thumbnail of a JPG/JPEG. If you need to create only a few thumbnails and want your code to be entirely in XAML, the easiest way to do that is to use the BitmapImage class.

Instead of specifying the Source directly on the Image element, you need to set the Source property to a BitmapSource object. (If you pass a file name or web address directly,  a converter is used to automatically create a BitmapImage for you as a coding-time saving technique.).

 

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:sys="clr-namespace:System;assembly=mscorlib" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
  <Canvas>
  <Border Canvas.Left="20" Canvas.Top="20" 
    Background="White" BorderBrush="Silver" BorderThickness="1" Padding="8" Width="Auto" Height="Auto">
  <Border.BitmapEffect>
    <DropShadowBitmapEffect />
  </Border.BitmapEffect>
  <Image Width="200">
    <Image.Source>
      <BitmapImage DecodePixelWidth="200"  
         UriSource="D:\temp\needle.JPG" />
      </Image.Source>
  </Image>
  </Border>
  </Canvas>
</Page>

(If you want to use this sample in XAMLPad, make sure you change the UriSource property to a file on your computer). The result should look something like this:

The BitmapImage class has two helpful properties to reduce the size of the image. But you might be saying to yourself, why can't I just use the Width property on the Image class? Sure, that works. If the image isn't very big, nothing bad will happen. However, if the image is large, you're using memory unnecessarily. The image is loaded and cached in memory, and then transformed to the necessary size. So, if you have an image that is 2000x1500, and you want to show it at 200x150, you're using 10 times the memory you actually need! That hardly seems like a good idea. Not only will it take more valuable memory, it's going to be a bit slower as the memory is copied and the image is shrunk to fit. That's where the two useful properties I hinted at come in, DecodePixelWidth and DecodePixelHeight. You'll only need to use one of them as they proportionally reduce the size of your image when you just set one. If you know that the image is going to be smaller than the original, do your users a favor and use them.

What happens if you set the DecodePixelWidth/Height too low and end up displaying the image at a larger size? Depending on the ratio, the effect can be subtle to dramatic. For a dramatic example, I've used the same code as above, but changed the DecodePixelWidth to 20:

That might be the Space Needle in Seattle or a picture of your skinny uncle. Be sure that you set the DecodePixelWidth/Height to a size that makes sense for your application.

That's one way to create a thumbnail.

For my recent PhotoScroll demo application, I wanted more control -- I wanted a class that could dynamically provide the images and create a thumbnail (and optionally extract it from a JPG if the thumbnail was embedded). That required some tricks.

I won't paste all of the code here, for the complete code, download the sample application available on the other post.

The code uses a couple of simple "oops, we don't need to load anymore" exit points (the variable _loading), so you can ignore those here. After the code, I'll walk through the important parts.

 
protected virtual void DoLoadImage(object o)
{
  // if we don't need to load, drop out ...
  if (!_loading) { return; }

if (System.IO.File.Exists(_filename))
{
MemoryStream mem = null;
try
{
byte[] buffer = File.ReadAllBytes(_filename);
// by reading the data into an in-memory buffer, we prevent the file from being read in the UI thread
// -- which speeds up access dramatically!
mem = new MemoryStream(buffer);

if (!_loading)
{
mem.Dispose();
return;
}

// we need to read the metadata in the UI thread, so we'll open the bitmap and grab it using a
// delegate/invoke. (since the user is waiting for this, there's little benefit to doing this
// as an async invoke).
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (NoArgumentDelegate)delegate()
{
bool foundThumbnail;
BitmapDecoder imgDecoder = BitmapDecoder.Create(mem, BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapFrame frame = imgDecoder.Frames[0];
_imageSource = frame.Thumbnail;
foundThumbnail = _imageSource != null;

if (!foundThumbnail)
{
// we'll make a thumbnail image then ... (too bad as the pre-created one is FAST!)
TransformedBitmap thumbnail = new TransformedBitmap();
thumbnail.BeginInit();
thumbnail.Source = frame as BitmapSource;

// we'll make a reasonable sized thumnbail with a height of 240
int pixelH = frame.PixelHeight;
int pixelW = frame.PixelWidth;
int decodeH = 240;
int decodeW = (frame.PixelWidth * decodeH) / pixelH;
double scaleX = decodeW / (double)pixelW;
double scaleY = decodeH / (double)pixelH;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY));
thumbnail.Transform = transformGroup;
thumbnail.EndInit();

// this will disconnect the stream from the image completely ...
WriteableBitmap writable = new WriteableBitmap(thumbnail);
writable.Freeze();
_imageSource = writable;
}

// Reading some metadata in a NON-UI thread throws an exception
// It's not as simple as doing a freeze, or cloning or ... anything
// else I could discover ... those all CRASHED.
Metadata = new Metadata(frame.Metadata as BitmapMetadata);
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
if (mem != null)
{
mem.Close();
}
_loading = false;
RaisePropetyChanged("IsLoading");
RaisePropetyChanged("ImageSource");
}

}
}


First, the file is loaded into a MemoryStream. As I mentioned in an earlier post, I've found the fastest way to load a Bitmap in code is to avoid using a FileStream and the StreamSource property or the UriSource property directly. The best technique from my experiments is to load the file directly into a MemoryStream object and use that as the Stream parameter or property (depends on which Bitmap objects you're using).


 

      if (System.IO.File.Exists(_filename))
{
MemoryStream mem = null;
try
{
byte[] buffer = File.ReadAllBytes(_filename);
// by reading the data into an in-memory buffer, we prevent the file from being read in the UI thread
// -- which speeds up access dramatically!
mem = new MemoryStream(buffer);

 

The code verifies the file exists and then reads the entire file into a byte array and immediately creates a wrapping MemoryStream object.

Since I wanted to grab some of the Windows Imaging Component metadata, which isn't all threadsafe, I need to perform the next steps the main UI thread (I'll talk about why and what errors might happen if you don't later).

 

          // we need to read the metadata in the UI thread, so we'll open the bitmap and grab it using a 
          // delegate/invoke. (since the user is waiting for this, there's little benefit to doing this
          // as an async invoke).
          Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (NoArgumentDelegate)delegate()
          {
            bool foundThumbnail;
            BitmapDecoder imgDecoder = BitmapDecoder.Create(mem, 
BitmapCreateOptions.None,
BitmapCacheOption.None); BitmapFrame frame = imgDecoder.Frames[0]; _imageSource = frame.Thumbnail; foundThumbnail = _imageSource != null;

Up until this point, I had created a background thread to load photos on demand from a queue -- so it was the background thread that fetched the data for the file.

Using the delegate syntax to make the code easier to debug/trace, I use the BitmapDecoder static method Create to open the file. Since this is a JPG file, there will only be one frame. The BitmapFrame object returned can be used directly as an image elsewhere, but in this case, I want something from that BitmapFrame, the Thumbnail.

Until I started working with these classes a few months ago, I hadn't realized that most modern digital cameras embed a thumbnail directly within the JPG file! Not only is that very cool, it's very practical as it's easy to access the data without needing to load and decode the entire image. That makes accessing the thumbnail fast. Very fast. I've got plenty of images that don't have a thumbnail however. Rather than maintaining an memory expensive large image and shrinking it on demand, I added code to create my own thumbnail on the fly.

 

              TransformedBitmap thumbnail = new TransformedBitmap();
              thumbnail.BeginInit();
              thumbnail.Source = frame as BitmapSource;
              
              // we'll make a reasonable sized thumnbail with a height of 240
              int pixelH = frame.PixelHeight;
              int pixelW = frame.PixelWidth;
              int decodeH = 240;
              int decodeW = (frame.PixelWidth * decodeH) / pixelH;
              double scaleX = decodeW / (double)pixelW;
              double scaleY = decodeH / (double)pixelH;
              TransformGroup transformGroup = new TransformGroup();
              transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY));
              thumbnail.Transform = transformGroup;              
              thumbnail.EndInit();

With a little bit of math, a ScaleTransform, and using yet another media class, TransformedBitmap, I created a thumbnail, with a height of 240. Since I used the empty constructor, I needed to follow the ISupportInitialize pattern:  BeginInit and EndInit. Only within the context of a Begin/End can the properties of the TransformedBitmap class be set (well, at least only the generally useful properties). In the code above, it only sets the Transform property to the new TransformGroup.

When EndInit is called, the magic of the TransformedBitmap class happens (it's transformed!). The bitmap is now ready for use.

The thing that was bugging me though was the hard connection between the TransformedBitmap and the original BitmapFrame returned from the BitmapDecoder (and thus to the MemoryStream). Unless I could sever that connection, I couldn't close/dispose the MemoryStream. So, my simple work-around was to create a new image based on the transformed bitmap:

 

              // this will disconnect the stream from the image completely ...
              WriteableBitmap writable = new WriteableBitmap(thumbnail); 
              writable.Freeze();
              _imageSource = writable;

Bing. Done. I stored the new image away for use by a listbox (which is binding to a thumbnail property of my Photo class).

 

            // Reading some metadata in a NON-UI thread throws an exception
            // It's not as simple as doing a freeze, or cloning or ... anything
            // else I could discover ... those all CRASHED.
            Metadata = new Metadata(frame.Metadata as BitmapMetadata);

The final piece that I wanted to mention bugged me to no end! The BitmapMetadata classes -- they're great, but they aren't always thread safe. For certain types of metadata, they throw crazy exceptions.  This is my favorite:

A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
System.Runtime.InteropServices.COMException (0x800706F4): A null reference pointer was passed to the stub. (Exception from HRESULT: 0x800706F4)
at MS.Internal.HRESULT.Check(Int32 hr)
at System.Windows.Media.Imaging.BitmapMetadataEnumerator.MoveNext()
at PhotoScroll.Metadata.LoadKeywords(BitmapMetadata metadata) in d:\Dev\Source\PhotoScroll\PhotoScroll\Metadata.cs:line 187

Of course, the stub had a null reference pointer. (uhh?) If you ever mistakenly thought all this new great .NET 3.0 was all managed code -- BZZZT! Wrong. What was particularly frustrating about that exception is that it only happens with certain types of metadata queries! xmp appears to be the most likely to crash if it's not queried on the UI thread -- but I didn't do an exhaustive test by any means. So, my solution was, as I mentioned, to create the image and load the metadata on the UI thread. It affects overall performance of the application, but using the tricks I mentioned above, I've kept the performance to a very respectable level.

Help support my web site by searching and buying through Amazon.com (in assocation with Amazon.com).