Using Image Source with big images in WPF
- by xyzzer
I am working on an application that allows users to manipulate multiple images by using ItemsControl. I started running some tests and found that the app has problems displaying some big images - ie. it did not work with the high resolution (21600x10800), 20MB images from
http://earthobservatory.nasa.gov/Features/BlueMarble/BlueMarble_monthlies.php, though it displays the 6200x6200, 60MB Hubble telescope image from http://zebu.uoregon.edu/hudf/hudf.jpg just fine.
The original solution just specified an Image control with a Source property pointing at a file on a disk (through a binding). With the Blue Marble file - the image would just not show up. Now this could be just a bug hidden somewhere deep in the funky MVVM + XAML implementation - the visual tree displayed by Snoop goes like:
Window/Border/AdornerDecorator/ContentPresenter/Grid/Canvas/UserControl/Border/ContentPresenter/Grid/Grid/Grid/Grid/Border/Grid/ContentPresenter/UserControl/UserControl/Border/ContentPresenter/Grid/Grid/Grid/Grid/Viewbox/ContainerVisual/UserControl/Border/ContentPresenter/Grid/Grid/ItemsControl/Border/ItemsPresenter/Canvas/ContentPresenter/Grid/Grid/ContentPresenter/Image...
Now debug this! WPF can be crazy like that...
Anyway, it turned out that if I create a simple WPF application - the images load just fine. I tried finding out the root cause, but I don't want to spend weeks on it. I figured the right thing to do might be to use a converter to scale the images down - this is what I have done:
ImagePath = @"F:\Astronomical\world.200402.3x21600x10800.jpg";
TargetWidth = 2800;
TargetHeight = 1866;
and
<Image>
<Image.Source>
<MultiBinding Converter="{StaticResource imageResizingConverter}">
<MultiBinding.Bindings>
<Binding Path="ImagePath"/>
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="TargetWidth"/>
<Binding Path="TargetHeight"/>
</MultiBinding.Bindings>
</MultiBinding>
</Image.Source>
</Image>
and
public class ImageResizingConverter : MarkupExtension, IMultiValueConverter
{
public Image TargetImage { get; set; }
public string SourcePath { get; set; }
public int DecodeWidth { get; set; }
public int DecodeHeight { get; set; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
this.SourcePath = values[0].ToString();
this.TargetImage = (Image)values[1];
this.DecodeWidth = (int)values[2];
this.DecodeHeight = (int)values[3];
return DecodeImage();
}
private BitmapImage DecodeImage()
{
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.DecodePixelWidth = (int)DecodeWidth;
bi.DecodePixelHeight = (int)DecodeHeight;
bi.UriSource = new Uri(SourcePath);
bi.EndInit();
return bi;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
Now this works fine, except for one "little" problem. When you just specify a file path in Image.Source - the application actually uses less memory and works faster than if you use BitmapImage.DecodePixelWidth. Plus with Image.Source if you have multiple Image controls that point to the same image - they only use as much memory as if only one image was loaded. With the BitmapImage.DecodePixelWidth solution - each additional Image control uses more memory and each of them uses more than when just specifying Image.Source. Perhaps WPF somehow caches these images in compressed form while if you specify the decoded dimensions - it feels like you get an uncompressed image in memory, plus it takes 6 times the time (perhaps without it the scaling is done on the GPU?), plus it feels like the original high resolution image also gets loaded and takes up space.
If I just scale the image down, save it to a temporary file and then use Image.Source to point at the file - it will probably work, but it will be pretty slow and it will require handling cleanup of the temporary file. If I could detect an image that does not get loaded properly - maybe I could only scale it down if I need to, but Image.ImageFailed never gets triggered. Maybe it has something to do with the video memory and this app just using more of it with the deep visual tree, opacity masks etc.
Actual question: How can I load big images as quickly as Image.Source option does it, without using more memory for additional copies and additional memory for the scaled down image if I only need them at a certain resolution lower than original? Also, I don't want to keep them in memory if no Image control is using them anymore.