4
votes

I want to create a linear gradient with 7 step colors and custom size - from black, blue, cyan, green, yellow, red to white. My problem is that the final bitmap has a black stripe on the right side. Anyone have an idea what's the matter?

    public static List<Color> interpolateColorScheme(int size)
    {
        // create result list with for interpolated colors
        List<Color> colorList = new List<Color>();
        // use Bitmap and Graphics from bitmap
        using (Bitmap bmp = new Bitmap(size, 200))
        using (Graphics G = Graphics.FromImage(bmp))
        {
            // create empty rectangle canvas
            Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
            // use LinearGradientBrush class for gradient computation
            LinearGradientBrush brush = new LinearGradientBrush
                                    (rect, Color.Empty, Color.Empty, 0, false);
            // setup ColorBlend object
            ColorBlend colorBlend = new ColorBlend();
            colorBlend.Positions = new float[7];
            colorBlend.Positions[0] = 0;
            colorBlend.Positions[1] = 1 / 6f;
            colorBlend.Positions[2] = 2 / 6f;
            colorBlend.Positions[3] = 3 / 6f;
            colorBlend.Positions[4] = 4 / 6f;
            colorBlend.Positions[5] = 5 / 6f;
            colorBlend.Positions[6] = 1;
            // blend colors and copy them to result color list
            colorBlend.Colors = new Color[7];
            colorBlend.Colors[0] = Color.Black;
            colorBlend.Colors[1] = Color.Blue;
            colorBlend.Colors[2] = Color.Cyan;
            colorBlend.Colors[3] = Color.Green;
            colorBlend.Colors[4] = Color.Yellow;
            colorBlend.Colors[5] = Color.Red;
            colorBlend.Colors[6] = Color.White;
            brush.InterpolationColors = colorBlend;
            G.FillRectangle(brush, rect);
            bmp.Save("gradient_debug_image_sarcus.png", ImageFormat.Png);
            for (int i = 0; i < size; i++) colorList.Add(bmp.GetPixel(i, 0));
            brush.Dispose();
        }

        // return interpolated colors
        return colorList;
    }

Here is my gradient:enter image description here

1
A possibly silly fix for this is to try brush.WrapMode = WrapMode.TileFlipX , but I assume you're looking for some insight as to why the original code isn't doing what it by all appearances should do. - Jerry Federspiel
Even with the proof image, I tested your code and there is no black line on the right side (imgur.com/fI2uaQ5). Is there something that you changed in your code you posted vs what you used to generate the image? (All I changed was the Path to save to and new Bitmap(size, 200) > new Bitmap(size, 10) - Bernd Linde
@BerndLinde This absolutely should not make a difference, but... did you try 200 before you changed it to 10? - Jerry Federspiel
@JerryFederspiel That I did yes and same result, no black line. I reduced the height to more accurately reflect the proof image - Bernd Linde
As you see, bitmap is generated and saved inside this function. In fact, I put the image with new Bitmap(size, 10) and forgot about it. But anyway, does height of bitmap really has anything to do with this bloody black stripe? - Paweł Dyląg

1 Answers

3
votes

I took your code and tried every size from 2 to ushort.MaxValue, generating the gradient and scanning from the right edge to determine how many black pixels there were.

For many sizes, there are no black pixels. However, for certain consecutive runs of sizes, as the size increases, the number of black pixels also increases. There are approximately 2140 such runs in the tested range. This implies that there is a rounding error in the gradient drawing.

This bug has been encountered before (http://www.pcreview.co.uk/threads/error-on-lineargradientbrush.2165794/). The two solutions that link recommends are to

  1. draw the gradient larger than you need it or
  2. use WrapMode.TileFlipX.

What that link gets wrong is that the rounding error is not just 1 pixel at all times; at large image sizes it can be as large as 127 pixels (in the range I tested). Drawing the gradient larger than you need it requires you to know (or estimate) how much bigger you need to make the gradient. You could try scaling by (size + Math.Ceiling(size / 512.0)) / size, which is an upper bound on the error for the range of image sizes I have tested.

If you're looking for a simpler solution, specifying brush.WrapMode = WrapMode.TileFlipX will cause the brush to draw normally up to the (incorrect) edge of the gradient, then repeat the gradient in reverse until the actual edge of the specified rectangle. Since the rounding error is small compared to the size of the rectangle, this will look like the final color of the gradient has been extended to the edge of the rectangle. Visually, it looks good, but it may be unsuitable if you require very precise results.