Grayscale Avalonia Icons

For disabled icons in Avalonia toolbar, you can go two ways. One is just using an external tool to convert your existing color icons into their color-less variant and have them as a completely separate set. The one I prefer is to actually convert images on demand.

As I'm currently playing with this in Avalonia, I decided to share my code. And it's not as straightforward as I would like. To start with, here is the code:
As I'm currently playing with this in Avalonia, I decided to share my code. And it's not as straightforward as I would like. To start with, here is the code:

public static Bitmap BitmapAsGreyscale(Bitmap bitmap) {
    var width = bitmap.PixelSize.Width;
    var height = bitmap.PixelSize.Height;

    var buffer = new byte[width * height * 4];
    var bufferPtr = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    try {
        var stride = 4 * width;
        bitmap.CopyPixels(default, bufferPtr.AddrOfPinnedObject(), buffer.Length, stride);

        for (var i = 0; i < buffer.Length; i += 4) {
            var b = buffer[i + 0];
            var g = buffer[i + 1];
            var r = buffer[i + 2];

            var grey = byte.CreateSaturating(0.299 * r + 0.587 * g + 0.114 * b);

            buffer[i + 0] = grey;
            buffer[i + 1] = grey;
            buffer[i + 2] = grey;
        }

        var writableBitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), Avalonia.Platform.PixelFormat.Bgra8888);
        using (var stream = writableBitmap.Lock());
        Marshal.Copy(buffer, 0, stream.Address, buffer.Length);

        return writableBitmap;
    } finally {
        bufferPtr.Free();
    }
}

Since Avalonia doesn't really expose pixel-level operations, first we need to obtain values of all the pixels. The easiest approach I found was just using the CopyPixels method to get all the data to our buffer. As this code in Avalonia is quite low-level and requires a pointer, we need to have our buffer pinned. Anything pinned also needs releasing, thus our finally block.

Once we have raw bytes, there is just a matter of figuring out which byte holds which value, and here I suspect that pretty much anybody will use the most common RGBA byte ordering. It's most common by far, and I would say it will be 99% what you end up with.

To get gray, we can use averages, but I prefer using slightly more complicated BT.601 luma calculation. And yes, this doesn't take into account gamma correction; nor is it the only way to get a grayscale. However, I found it works well for icons without much calculation needed. You can opt to use any conversion you prefer as long as the result is a nice 8-bit value. Using this value for each of RGB components gives us the gray component. Further, note that in code above, I only modify RGB values, leaving the alpha channel alone.

Once the bytes are in the desired state, just create a WritableBitmap based on that same buffer and with the same overall properties (including 32-bit color).

Leave a Reply

Your email address will not be published. Required fields are marked *