View source on GitHub.
See also: Part 1 and Part 3.
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.
|
<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:
|
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.
|
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:
|
<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.