Introduction
Trouble living on the edge? Blurred borders when you resize an image or bitmap? Can't get that custom control to draw properly? Transparent edges that should be opaque? Pixel creep? Problems with DrawImage
? You might be suffering from the dreaded BorderBug!
The DrawImage
method seems to have a bug in it. Or 'this behavior is by design', as Microsoft likes to put it. If you are copying all or part of one image to another, and if the destination rectangle is larger than the source rectangle, then you better be prepared for unexpected results and funny edges.
This article demonstrates a workaround that seems to solve the problem.
Background
When authoring custom controls, it's fairly common to do custom painting in the OnPaint
event.
Purists may prefer to do most of their painting using graphic methods like DrawRectangle
, DrawPolygon
, and so forth. These are great for controls that resize, as everything is sharply redrawn. But it can involve a huge amount of work for graphically complex controls. If you are cloning something like an MS Office 2007 scrollbar, then trying to get an exact pixel match to Microsoft's colors and layout will drive you crazy. After several hours of blends, opacity gradients, color arrays, and second-guessing just what graphics methods the MS designers used, you will probably still be working on the top scroll button.
Those of us who are on tight deadlines, or have better things to do than push pixels around, are much more likely to cheat and draw a bitmap (or do a screen-grab) and paint the resulting image, or the required parts of it, to our control. The upside of this is it's quick and easy - particularly if the control needs a lot of skins. The downside is if the bitmap needs to be resized, it may lose sharpness. If it needs to be stretched significantly, and you don't know the simple trick to avoid it, then the dreaded BorderBug comes to life and the edges misbehave.
Why does it happen?
The sample project shows four ways to copy part of an image to a control.
The following code does it the "obvious" way, and draws the top control in our demo solution, which shows unexpected red and green edges.
This is how it works: the following line of code defines our source image, which is the 120 x 120 pixel image containing nine colored boxes.
Bitmap sourceImage = Resource1.SourceImage;
To draw our control, we define the source rectangle as the part of the source image that we want. We then define a destination rectangle, and execute the DrawImage
method which does the work. Simple!
protected override void OnPaint(PaintEventArgs e)
{
Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);
Rectangle destinationRectangle = new Rectangle(0, 0,
this.Width, this.Height);
e.Graphics.DrawImage(sourceImage, destinationRectangle,
sourceRectangle, GraphicsUnit.Pixel);
base.OnPaint(e);
}
What do we get?
What happened? Did we get the source rectangle wrong and include part of the red and green squares? Clearly not. If the destination and source rectangles are identical in size, or fairly close, then no significant red or green edges can be seen.
The answer lies in the fact that DrawImage
needs to stretch the image to fit the destination. Since the pixels are not going to match up one-for-one, each pixel in the destination image needs to be calculated. Most resize calculations are very complex, and pixels in the original are not considered in isolation, but in conjunction with the pixels surrounding them. For pixels on the edge of the original rectangle, the pixels surrounding them include pixels that are not within the source rectangle. This is where the effects from the red and green pixels creep in. Despite them being outside of the selected area, they still enter into the calculation.
If the destination rectangle is exactly the same size as the source, then a perfect copy is possible as no stretching takes place. You can see this by resizing the form in the demo solution. If you get the three yellow controls roughly the same size as the yellow square in the source image, they are virtually perfect. Only at larger sizes do they start to fail.
Towards a solution
Post this as a problem in any forum, and I'll practically guarantee you'll be told to change a graphics setting such as the InterpolationMode
to something else - probably NearestNeighbor
. It doesn't work unless you take some other steps. Trust me. It won't make the problem go away by itself.
One solution that seems promising is to copy the desired part of the source image to an intermediate image of the same size. That should theoretically be a pixel-for-pixel copy as no stretching takes place. There should be no effects from stray red or green pixels, and we should end up with a clean image that can then be stretched into the destination control.
The following code draws the middle control in our demo solution, which has black edges:
protected override void OnPaint(PaintEventArgs e)
{
Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);
Bitmap intermediateImage = new Bitmap(40, 40);
Graphics graphics = Graphics.FromImage(intermediateImage);
Rectangle intermediateRectangle = new Rectangle(0, 0,
intermediateImage.Width, intermediateImage.Height);
graphics.DrawImage(sourceImage, intermediateRectangle,
sourceRectangle, GraphicsUnit.Pixel);
graphics.Dispose();
Rectangle destinationRectangle = new Rectangle(0, 0,
this.Width, this.Height);
e.Graphics.DrawImage(intermediateImage, destinationRectangle,
intermediateRectangle, GraphicsUnit.Pixel);
intermediateImage.Dispose();
base.OnPaint(e);
}
The code copies the part of the source image that we want into an intermediate image of the same size. It then stretches the intermediate image onto the control.
Did it work?
No. What happened this time? We have black edges!
A close inspection of the image and a little diagnostic analysis shows that what we actually have are transparent edges. They seem black because the background color of the control is black and it's showing through. (It's only black because I set it that way to show the problem. If the control background color and the form background color are the same, the control just looks as if it fades away at the edge.)
Why does it do that? It seems the DrawImage
method is still considering pixels from outside the selected area when calculating the edges. Of course, since we selected the whole image, there are no pixels outside of the selected area.
DrawImage
just imagines them and probably considers them as null in its calculations. Unfortunately, a null pixel appears to be completely transparent, i.e., has an alpha channel value of zero, which I suppose is what you would expect with everything zeroed-out. When worked into the calculations, this puts some transparency into the edges.
We did not succeed, but we are getting closer. We have the image we want. We should be able to solve the problem by adjusting the transparency.
A Workaround
We can simply repeat the above steps, but before we paint the final image into the control, we can reset the Alpha (transparency) values.
The following code draws the bottom control in our demo solution, which has no edge problems:
protected override void OnPaint(PaintEventArgs e)
{
...
Bitmap intermediateImage2 = new Bitmap(this.Width, this.Height);
Rectangle intermediateRectangle2 = new Rectangle(0, 0,
intermediateImage2.Width, intermediateImage2.Height);
Graphics graphics2 = Graphics.FromImage(intermediateImage2);
graphics2.DrawImage(intermediateImage, intermediateRectangle2,
intermediateRectangle, GraphicsUnit.Pixel);
intermediateImage.Dispose();
graphics2.Dispose();
intermediateImage2 = intermediateImage2.Clone(intermediateRectangle2,
PixelFormat.Format24bppRgb);
Rectangle destinationRectangle =
new Rectangle(0, 0, this.Width, this.Height);
e.Graphics.DrawImage(intermediateImage2, destinationRectangle,
intermediateRectangle2, GraphicsUnit.Pixel);
intermediateImage2.Dispose();
...
}
This code copies our intermediate image, which is still the same size as the source image, into a second intermediate image which is the same size as the destination, i.e., it stretches it and the second intermediate image will have transparent edges.
The tricky part of the code resets the alpha so that the second intermediate image becomes completely non-transparent again:
intermediateImage2 = intermediateImage2.Clone(intermediateRectangle2,
PixelFormat.Format24bppRgb);
We have simply cloned the image to itself, using a pixel format that does not include an alpha channel. This strips-off the alpha information. If converted back to an alpha pixel format, all alpha values will default to 255. We have removed the transparency. We can now paint the image to the control at the same size so there will be no further stretching or edge effects.
Did it work?
Of course, it did! But it's a bit of a hack and takes several steps. There must be a better way...
A better way.
The root cause of this unexpected behavior is that the method considers the origin of the source rectangle to be the middle of the upper-left pixel, not the top-left corner of that pixel. This seems a bit odd, but you can see why it might be desirable to define a pixel by its middle for other graphic manipulations, such as rotations.
We can get around this problem by starting our rectangle half a pixel up and to the left. This compensates for the method starting half a pixel down and to the right, i.e. our origin is now the top-left of the origin pixel.
It may seem a bit strange to select half-pixels since we tend to think of them as discrete units, but it's quite valid.Why else would there be a RectangleF
?
Here's the code:
protected override void OnPaint(PaintEventArgs e)
{
RectangleF sourceRectangle = new RectangleF(39.5f, 39.5f, 40, 40);
RectangleF destinationRectangle = new RectangleF(0, 0, this.Width,
this.Height);
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
e.Graphics.DrawImage(sourceImage, destinationRectangle, sourceRectangle,
GraphicsUnit.Pixel);
base.OnPaint(e);
}
Note that we have also set the interpolation mode to NearestNeighbor
. This would not have helped before, but it should now:
...and it does. This is the best solution to the problem.
Acknowledgements
The first time I heard this peculiarity of DrawImage
called the BorderBug was in a discussion thread, following Joel Neubeck's article: Resizing a Photographic image with GDI+ for .NET. I think it was BigAndy who coined the term and came up with the first effective workaround, which consists of surrounding a desired image with a 10-pixel matching border so that DrawImage
gets appropriate out-of-source-rectangle pixels. There is some discussion of this under the thread Dark Edge Solution.
But the biggest acknowledgement must go to GDI+ guru darrellp for his invaluable guidance through the GDI+ jungle. Thanks Darrell.
History
- July 19, 2006 - Submitting this, my first ever article for CodeProject.
- July 27, 2006 - Modified to include a much better workaround suggested by darrellp.