The Most Important Silverlight WriteableBitmap Gotcha - Does It Lose/Change Colors?

Jan 27, 2010

Edit Nov 21, 2011: changed 256/a to 255/a due to Gert’s comment – thanks Gert!

The first time I tried saving WriteableBitmap to png, the transparencies didn't look right. The color was "washed out" and replaced with gray:

 

Moreover: Let's say you wanted to set a half-opaque red pixel color inside a WriteableBitmap.

To my biggest surprise this didn't work:

bitmap.Pixels[i] = (255 << 24) + (255 << 16)

I got a redder pixel than expected! What's going on?

After long search and speaking with people, it turned out it's because WriteableBitmap for Silverlight uses pARGB (pre-multiplied alpha) format.

If you look at MSDN carefully, you'll notice the single place (as far as I know) that mentions it: http://msdn.microsoft.com/en-us/library/system.windows.media.imaging.writeablebitmap(VS.95).aspx

It reads: "When assigning colors to pixels in your bitmap, use pre-multiplied colors."

The reason for this initially unintuitive choice of format is speed. Blending with pre-multiplied colors can save significant time as compared to just ARGB.

For more info and blending formulas, read here: http://www.teamten.com/lawrence/graphics/premultiplication/

Here is the source code to call before saving WriteableBitmap to PNG in order to convert the colors to "regular" ARGB:

public static void CompensateForRender(int[] bitmapPixels)
{
    int count = bitmapPixels.Length;

    for (int i = 0; i < count; i++)
    {
        uint pixel = unchecked((uint)bitmapPixels[i]);
        // decompose
        double a = (pixel >> 24) & 255;
        if ((a == 255) || (a == 0)) continue;

        double r = (pixel >> 16) & 255;
        double g = (pixel >> 8) & 255;
        double b = (pixel) & 255;

        double factor = 255 / a;
        uint newR = (uint)Math.Round(r * factor);
        uint newG = (uint)Math.Round(g * factor);
        uint newB = (uint)Math.Round(b * factor);
        // compose
        bitmapPixels[i] = unchecked((int)((pixel & 0xFF000000) | (newR << 16) | (newG << 8) | newB));
    }
}

If you look at the previous post, you'd call CompensateForRender(_screen.Pixels) just before TgaWriter.Write(_screen, fileStream);

Obviously the above function can be optimized, and should if you use it often (e.g. several times per second or as part of batch processing).


You know that speed is a benefit, the drawback is that there is some loss of precision. It's nice that for completely opaque images there is no precision loss and the pARGB format is equivalent to the ARGB format. So in most cases you wouldn't care :)

In the cases where the alpha gets below 255, you have to go all the way to 128 alpha to lose one bit-per-pixel from the color information. Not that bad, given how many CPU cycles can be saved in blending and used for something else.

Hope this helps, please comment - if you had a choice, which format would you choose and why?

  
blog comments powered by Disqus

nokola.com | Terms | Log in

Recent

About the author

Happy & enjoying life. Software enthusiast.
The opinions I express here and on nokola.com are mine and not my employeer's (Microsoft).
This is the official blog of nokola.com. You can find Silverlight samples, coding stuff, and hopefully other interesting things here.