WPF - Modifying Image Colors on the Fly (C#)

Andez picture Andez · Dec 31, 2013 · Viewed 7.7k times · Source

Is it possible to modify the colors of an Image in WPF via code (or even using Templates)?

Suppose I have an image which I need to apply to a Tile - which will have a White Foreground color by default and a Transparent Background. Something like the following PNG (it is somewhere here!):

Add New PNG

Instead of adding different images - with different colors, I just want to manipulate the White - and say change it to Black.

If it can be done, can someone give me a few pointers on what I need to do/look into.

Answer

Ian Griffiths picture Ian Griffiths · Dec 31, 2013

One way to do this would be to use the BitmapDecoder class to retrieve the raw pixel data. You can then modify the pixels, and build a new WriteableBitmap from that modified pixel data:

// Copy pixel colour values from existing image.
// (This loads them from an embedded resource. BitmapDecoder can work with any Stream, though.)
StreamResourceInfo x = Application.GetResourceStream(new Uri(BaseUriHelper.GetBaseUri(this), "Image.png"));
BitmapDecoder dec = BitmapDecoder.Create(x.Stream, BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapFrame image = dec.Frames[0];
byte[] pixels = new byte[image.PixelWidth * image.PixelHeight * 4];
image.CopyPixels(pixels, image.PixelWidth*4, 0);

// Modify the white pixels
for (int i = 0; i < pixels.Length/4; ++i)
{
    byte b = pixels[i * 4];
    byte g = pixels[i * 4 + 1];
    byte r = pixels[i * 4 + 2];
    byte a = pixels[i * 4 + 3];

    if (r == 255 &&
        g == 255 &&
        b == 255 &&
        a == 255)
    {
        // Change it to red.
        g = 0;
        b = 0;

        pixels[i * 4 + 1] = g;
        pixels[i * 4] = b;
    }


}

// Write the modified pixels into a new bitmap and use that as the source of an Image
var bmp = new WriteableBitmap(image.PixelWidth, image.PixelHeight, image.DpiX, image.DpiY, PixelFormats.Pbgra32, null);
bmp.WritePixels(new Int32Rect(0, 0, image.PixelWidth, image.PixelHeight), pixels, image.PixelWidth*4, 0);
img.Source = bmp;

This works after a fashion, but there's a problem. Here's how the result looks if I show it on a dark background:

red cross on black background

As you can see, it's got a sort of white border. What's happened here is that your white cross had anti-aliased edges, meaning that the pixels around the edges are actually a semi-transparent shade of grey.

We can deal with that using a slightly more sophisticated technique in the pixel modification loop:

if ((r == 255 &&
        g == 255 &&
        b == 255 &&
        a == 255) ||
    (a != 0 && a != 255 &&
        r == g && g == b && r != 0))
{
    // Change it to red.
    g = 0;
    b = 0;

    pixels[i * 4 + 1] = g;
    pixels[i * 4] = b;
}

Here's how that looks on a black background:

red on black without white border

As you can see, that looks right. (OK, you wanted black not red, but the basic approach will be the same for any target colour.)

EDIT 2015/1/21 As ar_j pointed out in the comments, the Prgba format requires premultiplication. For the example I've given it is actually safe to ignore it, but if you were modifying colour channels in any way other than by setting them to 0, you'd need to multiple each value by (a/255). E.g., as aj_j shows for the G channel: pixels[i * 4 + 1] = (byte)(g * a / 255); Since g is zero in my code, this makes no difference but for non-primary colours you would need to do it that way.

Here it is on a gradient fill background just to show that the transparency is working:

red cross on gradient background

You could also write out the modified version:

var enc = new PngBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bmp));
using (Stream pngStream = File.OpenWrite(@"c:\temp\modified.png"))
{
    enc.Save(pngStream);
}

Here's the result:

modified PNG

You can see the red cross, and it'll be on top of whatever background colour StackOverflow is using. (White, as I write this, but maybe they'll redesign one day.)

Whether this will work for the images you want to use is harder to know for certain, because it depends on what your definition of 'white' is - depending on how your images were produced, you may find things are ever so slightly off-white (particularly around the edges), and you may need further tweaking. But the basic approach should be OK.