In Part 1 of this series, I explored the issue of displaying pixel-perfect bitmap images in the Windows Presentation Foundation. In this article, I’ll describe a method of displaying different images depending on the system DPI setting using a custom Markup Extension and multi-image TIFF files.
Tagged Image File Format (TIFF) files may contain multiple images, and WPF contains support for this format out of the box. You can use the TiffBitmapEncoder class to combine multiple images into one TIFF – I made a tool called PNGToMultiDPITIFF that does just this, but I’ll leave that to Part 3.
To pick the best-matching image from a multi-frame TIFF, I created two Markup Extensions – one for creating an ImageSource, and one for setting the image’s BitmapScalingMode. If the TIFF contains an exact match for the current DPI, the BitmapScalingMode can be set to NearestNeighbour (as there should be no scaling). If not, it will be set to ‘Unspecified’ (which means ‘Linear’ in WPF 4 or newer) so it looks better.
MultiDPIImage.7z
1,229 bytes; SHA-1: BB0B8867C48ECEADD7655E792DAA780A30299747
You can download the code for the Markup Extensions above.
1 |
<Image Source="{local:MultiDPIImageSource 'MultiDPIImage.tif'}" RenderOptions.BitmapScalingMode="{local:MultiDPIImageScalingMode 'MultiDPIImage.tif'}" /> |
As discussed in the previous post, remember to set UseLayoutRounding and SnapToDevicePixels to true on your Windows.
Code Discussion
The code for the markup extension is quite simple. To get the image frames, we use the TiffBitmapDecoder class:
1 |
TiffBitmapDecoder decoder = new TiffBitmapDecoder(this.imageuri, BitmapCreateOptions.None, BitmapCacheOption.Default); |
We loop through the frames to see if any match the system DPI. If there is no exact match, the first frame with a DPI above the system DPI is selected. If there is no such frame, we just pick the frame with the highest DPI.
1 2 3 4 5 6 7 8 9 10 11 12 |
SortedList<int, BitmapFrame> sortedframes = new SortedList<int, BitmapFrame>(); foreach (BitmapFrame frame in frames) if (!sortedframes.ContainsKey(GetDPI(frame.DpiX, frame.DpiY))) sortedframes.Add(GetDPI(frame.DpiX, frame.DpiY), frame); foreach (var frame in sortedframes) { if (frame.Key == DPI) return frame.Value; if (DPI < frame.Key) return frame.Value; } return sortedframes.Last().Value; |
The code for choosing the BitmapScalingMode is similar – instead of returning an image, we return ‘NearestNeighbour’ if there is an exact DPI match or ‘Unspecified’ otherwise. If you want to use a different BitmapScalingMode fallback, you can specify an optional second paramater in MultiDPIImageScalingMode:
1 |
<Image Source="{local:MultiDPIImageSource 'MultiDPIImage.tif'}" RenderOptions.BitmapScalingMode="{local:MultiDPIImageScalingMode 'MultiDPIImage.tif', Fant}" /> |
Drawbacks
- Images set with the markup extension will not be visible in the WPF designer (Visual Studio or Blend). I’d welcome any suggestions on how to fix this.
- Avoid using PNGOUT or PNGGauntlet on PNG images before putting them into multi-frame TIFF files. The Windows TIFF decoder has some issues with compressed PNGs.
The reason the images don’t show up in the design is because your missing the the assembly name from the pack uri. In order to fix this you need to remove the “pack://application:,,,/” from MultiDPIImageSource method and instead try and resolve the uri. The best way I could come up with is to grab the the IUriContext interface that the ImageSource implements.
This is what you need.
“pack://application:,,,/AssemblyName;component/someimage.png”
rather than
“pack://application:,,,/someimage.png”
So you end up with:
public override object ProvideValue(IServiceProvider serviceProvider)
{
var resolvedUri = imageuri;
if (!resolvedUri.IsAbsoluteUri)
{
var ipvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if (ipvt == null || ipvt.TargetObject == null)
return;
if (DesignerProperties.GetIsInDesignMode((DependencyObject)ipt.TargetObject))
{
var uriContext = ipt.TargetObject as IUriContext;
if (uriContext != null && uriContext.BaseUri != null)
{
resolvedUri = new Uri(uriContext.BaseUri, imageuri.OriginalString);
}
}
}
……..CreateBitmap
One question: When you use a multi image tiff, are you not loading all the images for all the dpi’s even when you only want one?
so if you have 100+ glyphs in an app you may actualy be loading 200-400 (depending on number of dpi images).
Neal
Thanks for figuring out the issue with the designer not showing images.
You’re right about this method being inefficient and always loading the bitmaps for all DPIs when only one will actually be used. If performance is a concern, multi-frame TIFFs are probably not the answer. I liked the idea of having a single file holding all the data for a single image rendered at different sizes, but if I were to revisit this, I might go with the WinRT approach of having multiple files with the scale factor included in the filename as a suffix.