Blurry Bitmaps
Background: resolution independence
WPF was designed from the start to be resolution independent. So instead of designing your UI in terms of pixels, you are encouraged to use a physical measuring unit (like inches). When you specify coordinates in WPF, you are actually using "measure units", which are defined to be 1/96th of an inch. This was chosen because 96 DPI monitor settings are very common.
When WPF renders to the screen, it takes into account the system DPI, which you can set from the display control panel. Of course, one problem with this is that no one ever sets this correctly. When was the last time you took out a ruler, measured your screen, then divided your resolution by the result? Another reason people don't set the DPI correctly is that Windows doesn't always look good at non-standard DPI settings. This is something we will be improving in the future. Of course, since you have to recalculate the DPI settings everytime you adjust your resolution, it would be best if the hardware could just report it. For these reasons, WPF may not actually be able to draw a real inch on the screen, but it should come reasonably close.
Another aspect of resolution independence is being able to draw with more precision than pixels. If you recall the GDI APIs, drawing calls specified their coordinates as integers, which match the pixel grid. WPF specifies its coordinates using floating point numbers, so you can easily specify coordinates like (1.2,5.234). This is sometimes referred to as sub-pixel positioning. Of course, what does it mean to render smaller than a pixel, given that each pixel has a single final color? The answer is that we blend all of the contributors to a given pixel. You can think of this as a weighted average, so that if one primitive covers most of a pixel, it will contribute more than other primitives that cover very little of the pixel. But blending loses detail, so this can be very noticeable.
Bitmaps: what size should they be?
Bitmaps are often produced by designers using high-end editing tools. Presume that the goal is to preserve the designer's intent, what size should we display the image at? If the designer made a 96x96 bitmap, on a 96 DPI machine it will be 1 inch by 1 inch. But if you show that bitmap on a super-high-end 300 DPI monitor (I've seen one, its awesome), then the bitmap would be less than a centimeter in each dimension. So, did the designer mean 96 pixels, or one inch? Interestingly, modern image formats allow you to embed the designer's intended DPI into the file. WPF will utilize this information, and scale the bitmap so that it ends up being the desired size. However, here we have a problem - WPF always presumes the designer meant the image to be a certain physical size. Often designers mean for the image to be a certain pixel size - after all, they are carefully assigning colors to each pixel in the image. Its not the designer's intent to be a certain physical size, but the tools they use often stamp some DPI setting into the file anyways (commonly, some standard DPI setting like 72 or 96). WPF sees this DPI information and scales the image to match.
One problem: pixel scaling
Bitmaps are difficult because they encode "high frequency" information in the form of per-pixel color information. If the bitmap is displayed at a slightly different size, the result can be very blurry. I won't bore you with lots of examples, but consider this one. Say you have a bitmap that is 3x3 pixels. It looks like this:
Now, if we scale this just a bit, such that the image will fit into 4x4 pixels, the result suffers from the fact that we can't represent each source pixel equally in the destination grid. In fact, each original pixel "expands" by 1/3. But of course, the final pixel can only have one color, so we have to combine overlapping samples. We can try various blending techniques, but they all have problems. The result will look something like this:
The human eye can easily detect this artifact, and we tend to associate the result with being out of focus, or blurry. It can be very irritating.
Another problem: pixel alignment
As I mentioned in the background section, WPF can render with sub-pixel precision. What happens if you render the original 3x3 bitmap at the correct size, but at pixel coordinates (0.33,0.33)? The solid red line ends up straddling two pixel columns, and the best we can do is to blend the contributions, which will result in nearly the same result as the previously described scale.
SnapsToDevicePixels
WPF anticipated that there would be cases where people wanted to align with the pixel grid instead of using sub-pixel precision. You can set the SnapsToDevicePixels property on any UIElement. This will cause us to try and render to the pixel grid, but there are quite a few cases that don't work - including images. We will be looking to improve this in the future.
Now what?
All is not lost. Even though WPF wants really badly to be resolution independent, we can force it to be pixel aligned ourselves. In fact, this isn't even very hard. We just need to do two things:
- Size ourselves to real pixel sizes.
- Position ourselves on pixel boundaries.
For sizing, we can easily participate in the measure pass, and return a measure size that is equivalent to the desired pixel size. The transform used to factor in the system DPI is provided for us in CompositionTarget.TransformFromDevice. The pixel size is in "device" coordinates, and we transform from the device into measure units. Easy enough.
For positioning, we could participate in the arrange pass, and maybe apply a render transform to ourselves to offset appropriately. However, for this example I'm focusing on bitmaps, so I choose to participate in rendering and pass the appropriate offset to by DrawImage call. Either way would work, but the real problem is that we need to know when anything might shift our position. The WPF layout system is very conservative, meaning that it tries to do as little work as possible. Once you have been measured and arranged, you won't get called again unless your individual layout state is invalidated. If some parent decides to shift you around a little bit, you will most likely not be laid out again. I work around this by subscribing to the LayoutUpdated event. Even though this event is an instance event on UIElement, it actually gets raised anytime any layout activity happens. Lucky for us, this is pretty much what we want.
Bitmap class
The code included below introduces a new class called Bitmap. Bitmap is a replacement for Image, but instead of displaying any image source, it only displays bitmap sources. This lets me access the PixelWidth and PixelHeight properties for determining the appropriate size. The important aspects of this class are:
- Derived from UIElement instead of FrameworkElement because I don’t want things like MinWidth, MaxWidth, or even Width.
- Bitmap.Source can be set to any BitmapSource.
- When measured, it will return the appropriate measure units to display the bitmap’s PixelWidth and PixelHeight
- When rendered, it will offset the image it draws to align with the pixel grid
- Whenever layout updates, it checks to see if it needs to re-render to align to the pixel grid again.
Its a pretty straight-forward class, check out the source code for the details.
The sample application
Included in the project is a very simple sample application. I have embedded a number of small bitmaps with high-frequency data in them. Further, I encoded the bitmaps are various designer DPIs. In the app, the top stack panel contains instances of Bitmap. The bottom stack panel contains instances of Image. You can clearly see how Image responds to the bitmap DPI, while Bitmap does not. For fun, you can use the arrow keys which will adjust a render transform on the root. Bitmap will snap to the nearest pixel, while Image will use sub-pixel positioning.
Is there a down side?
Of course. By using the pixel size of the bitmap, the bitmap will look very different on different machines. The 300 DPI super monitor will make your bitmap look very small. Further, since we align to the pixel boundary, animating will cause the bitmap to jump by full pixels. Sub-pixel positioning is very good for animations. Finally, you can get gaps between elements due to rounding to the pixel grid. If you can live with these limitations, then maybe this Bitmap class will be useful to you.
Comments
Anonymous
October 05, 2007
PingBack from http://www.artofbam.com/wordpress/?p=5616Anonymous
February 03, 2008
Известно, что в текущей версии WPF существует проблема с качеством отображения растров. Возникает онаAnonymous
March 25, 2008
WPF gives you a lot of functionality, but it's mainly designed to work with vector based graphics. ItAnonymous
May 06, 2008
Information and possible fixes for blurry text in WPF and Silverlight applicationsAnonymous
May 06, 2008
Information and possible fixes for blurry text in WPF and Silverlight applicationsAnonymous
May 15, 2008
We've recently added a PixelSnapper decorator to the Actipro WPF Shared Library, which helps with pixel alignment by providing options for rounding off decimal measurements of child content to integer numbers instead of decimals. Using the PixelSnapper decorator is an easy way to keep everything crisp and clear in your WPF applications. More info is here: http://blog.actiprosoftware.com/post/How-to-prevent-blurry-images-and-lines-in-WPF.aspxAnonymous
August 18, 2008
It's been a while since I've done anything with GDI+ (i.e. System.Drawing). System.Windows (iAnonymous
August 25, 2008
This is a great piece of code and worked really well, although I was disapointed when if did not work for data bound items. However after a bit of playing I found that if you inherted from framework element instead and used the MeasureOverride Instead of MeasureCore then it will work. Thankyou for your great work.Anonymous
March 11, 2009
Thank you, willlan, for posting about changing the base class to FrameworkElement and changing MeasureCore to MeasureOverride. The version deriving from UIElement did not resize if you changed the Source later on to one with a different size than the original. By deriving from FrameworkElement, if you change the Source, it automagically gets remeasured. I understand Dwayne's instinct to pick UIElement to avoid the possibility of properties like Width getting explicitly set, but deriving from FrameworkElement makes the result much more generally useful. EricAnonymous
June 08, 2009
@Eric. I also needed data binding and so changed the base class from UIElement to FrameworkElement, and MeasureCore to MeasureOverride. One bug with this implementation is that if you add Margin to the element, the image appears to stretch (even non-uniformly) to fill the margin space too. Still, if you can work around that, it's a nice way to go. Thanks Dwayne for this post.Anonymous
June 10, 2009
I also noticed a problem with the modified code whereby if there's nothing to display, and endless loop can occur, maxing out the CPU. This check guards against this happening: private void OnLayoutUpdated(object sender, EventArgs e) { // Avoid getting into an endless loop if (ActualHeight == 0 || ActualWidth == 0) return; ...Anonymous
January 09, 2010
@drewnoakes: I corrected the rendering with nonzer omargin like this: In OnRender // Render the bitmap offset by the needed amount to align to pixels. // was (before taking margin into account): dc.DrawImage(bitmapSource, new Rect(_pixelOffset, DesiredSize)); Size desiredSize = new Size( DesiredSize.Width - Margin.Left - Margin.Right, DesiredSize.Height - Margin.Top - Margin.Bottom ); dc.DrawImage(bitmapSource, new Rect(_pixelOffset, desiredSize));Anonymous
May 03, 2012
Nice article, thanks! Referenced in SlidingImageControl: www.codeproject.com/.../Tefik-BecirovicAnonymous
September 14, 2013
One drawback is there a horrible flicker effect when the window is moved (in the sample at least). Funny the standard Image control also has it when the lines are pixel aligned. The sample shows that as wel.Anonymous
April 17, 2014
<Button > <StackPanel Orientation="Horizontal"> <TextBlock >Button</TextBlock> <Image Source="SomeImage.png"/> </StackPanel> </Button> 7 years on and its still not possible to use the basic code above and align image correct on a pixel boundary, very sad. Even using UseLayoutRounding does not do anything to solve this issue. Why O why does MS have to make everything so complicated. Nor is is possible to use a custom Bitmap class as it does not work with controls that take images such a the Ribbon. Come back Win32 all is forgiven (at least it worked they way we expect)Anonymous
February 27, 2016
Use RenderOptions.BitmapScalingMode="NearestNeighbor"