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