Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

BorderBug

0.00/5 (No votes)
31 Jul 2006 2  
A workaround for the DrawImage border problem.

Demo project

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)
{
    // This rectangle defines the part

    // we want to use from the source image

    Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);
    
    // This rectangle defines where we want to draw on the control

    Rectangle destinationRectangle = new Rectangle(0, 0, 
                                     this.Width, this.Height);
    
    // And this procedure draws it for us. Easy. OR IS IT? 

    e.Graphics.DrawImage(sourceImage, destinationRectangle, 
                         sourceRectangle, GraphicsUnit.Pixel);

    // Bother! It didn't work properly!

    
    base.OnPaint(e);

}

What do we get?

Bad edges!

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)
{
    // This rectangle defines the part

    // we want to use from the source image

    Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);

    // Define an intermediate image the same size

    // as the part we are taking from the source image

    Bitmap intermediateImage = new Bitmap(40, 40);

    // Get the graphics object of the intermediate image,

    // so we can draw to it.

    Graphics graphics = Graphics.FromImage(intermediateImage);

    // This rectangle defines where we want

    // to draw in the intermediate image

    Rectangle intermediateRectangle = new Rectangle(0, 0, 
      intermediateImage.Width, intermediateImage.Height);

    // Draw the part we want in the intermediate image.

    graphics.DrawImage(sourceImage, intermediateRectangle, 
                       sourceRectangle, GraphicsUnit.Pixel);
    graphics.Dispose(); // Let's be tidy!


    // This rectangle defines where we want to draw on the control

    Rectangle destinationRectangle = new Rectangle(0, 0, 
                                     this.Width, this.Height);

    // Draw the intermediate image on the control

    e.Graphics.DrawImage(intermediateImage, destinationRectangle, 
                         intermediateRectangle, GraphicsUnit.Pixel);
    intermediateImage.Dispose(); // Let's be tidy!


    // Surely that nailed it? Apparently not!!!!!

    // Now the edges are transparent.

 
    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?

Bad edges!

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)
{
    ...

    // Now create another intermediate image,

    // this time the size of our destination

    Bitmap intermediateImage2 = new Bitmap(this.Width, this.Height);

    // This rectangle defines where we want

    // to draw on the second intermediate image

    Rectangle intermediateRectangle2 = new Rectangle(0, 0, 
     intermediateImage2.Width, intermediateImage2.Height);

    // Get the graphics object of the second

    // intermediate image, so we can draw to it.

    Graphics graphics2 = Graphics.FromImage(intermediateImage2);

    // Draw the first intermediate image on the second

    // intermediate image (this is where it gets stretched)

    graphics2.DrawImage(intermediateImage, intermediateRectangle2, 
                       intermediateRectangle, GraphicsUnit.Pixel);
    intermediateImage.Dispose(); // Let's be tidy!

    graphics2.Dispose(); // Let's be tidy!


    // Remove the alpha channel from the second

    // intermediate image by cloning it to itself

    intermediateImage2 = intermediateImage2.Clone(intermediateRectangle2, 
                                             PixelFormat.Format24bppRgb);
      
    // This rectangle defines where we want to draw on the control

     Rectangle destinationRectangle = 
               new Rectangle(0, 0, this.Width, this.Height);

    // Draw the second intermediate image on the control AT THE SAME SIZE

    e.Graphics.DrawImage(intermediateImage2, destinationRectangle, 
                         intermediateRectangle2, GraphicsUnit.Pixel);
    intermediateImage2.Dispose(); // Let's be tidy!


    ...
}

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?

Fixed it!

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)
{
    // This rectangle defines the part we want to use from the source image,

    // but this time we offset the start point half a pixel up and to the 

    // left!

    RectangleF sourceRectangle = new RectangleF(39.5f, 39.5f, 40, 40);

    // This rectangle defines where we want to draw on the control

    RectangleF destinationRectangle = new RectangleF(0, 0, this.Width, 
        this.Height);

    // Set the interpolation mode to NearestNeighbor. (It should work now!)

    e.Graphics.InterpolationMode = 
                 System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

    // And this procedure draws it for us.

    e.Graphics.DrawImage(sourceImage, destinationRectangle, sourceRectangle, 
         GraphicsUnit.Pixel);

    // YES!!! A fast and easy solution

      
    base.OnPaint(e);
}

Note that we have also set the interpolation mode to NearestNeighbor. This would not have helped before, but it should now:

Fixed it!

...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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here