Preserving transparency when rendering Office icons

 

Today I have a guest writer on my blog - Eric Faller.  Eric shows the correct way to render Office icons returned by GetImageMso API. I got involved into this by replying to this post in our forums but the explanation just would not fit into a regular forums post - so here we go.

-----------------------------------------------------------------------------------------

This is a follow-up to Andrew’s post about converting between the image formats used by Office and the .NET framework.  I’ll be talking about handling the alpha channel (transparency) of the images, mentioned at the end of that post and in the comments. I’d recommend reading that post first in order to get up to speed on the IPictureDisp interface and some of the other concepts we’ll be discussing.

I’d also recommend reading the RibbonX Image FAQ on Jensen Harris’ blog. It has a lengthy discussion about the different formats Office has used for image transparency in the past, as well as some common pitfalls when loading images into Office. In this post I’ll be talking about getting images out of Office, but many of the problems will be similar (DDB vs DIB, etc.).

Office 2007 introduces a new API for fetching icon images, the GetImageMso function on the CommandBars object. It takes the ID of a Ribbon control and returns its icon in IPictureDisp format. You can use one of the many methods discussed in Andrew’s previous post to convert these objects into .NET-friendly System.Drawing.Bitmap objects.

If you do, you might notice that the icons don’t look exactly correct when you draw them – the transparent edges show up white and shadow elements look black. For example here’s what the “Paste” icon looks like if drawn on a WinForm:

clip_image002

If you’re only using the smaller versions of the icons (16x16), drawing them on a white background, and don’t care too deeply about pixel-perfect visuals, you might be OK with this. Calling Bitmap.MakeTransparent on the icon will help get rid of the white border, but it’s still not quite perfect.

The bad news is that if you want to stick with purely .NET code, you’re stuck with this – that’s the best that your icon can look. The problem is that the alpha channel has already been lost during the conversion from IPictureDisp to System.Drawing.Bitmap.

The CLR and GDI+ internally call Win32 GDI functions during the conversion, and these functions are not alpha channel-aware. GDI itself was written long before alpha channels became popular, and as a result almost all of the standard Win32 GDI functions will ignore the alpha channel and appear to “throw it away” during various copy and conversion operations.  Alpha channel support was only added with the AlphaBlend function in Windows 98/2000 with the addition of MSIMG32.DLL.

The good news is that we can get a lot better transparency in our images if we’re willing to do a little native code interop and call AlphaBlend ourselves. It’s slightly complicated, so I’ll just show you the code and then explain it. Here’s a function that will convert an IPictureDisp object to a System.Drawing.Bitmap object, using the AlphaBlend function:

public static Bitmap ConvertWithAlphaBlend(IPictureDisp ipd)

{

    // get the info about the HBITMAP inside the IPictureDisp

    DIBSECTION dibsection = new DIBSECTION();

    GetObjectDIBSection((IntPtr)ipd.Handle, Marshal.SizeOf(dibsection), ref dibsection);

    int width = dibsection.dsBm.bmWidth;

    int height = dibsection.dsBm.bmHeight;

    // zero out the RGB values for all pixels with A == 0

    // (AlphaBlend expects them to all be zero)

    unsafe

    {

        RGBQUAD* pBits = (RGBQUAD*)(void*)dibsection.dsBm.bmBits;

        for (int x = 0; x < dibsection.dsBmih.biWidth; x++)

            for (int y = 0; y < dibsection.dsBmih.biHeight; y++)

            {

                int offset = y * dibsection.dsBmih.biWidth + x;

                if (pBits[offset].rgbReserved == 0)

                {

                    pBits[offset].rgbRed = 0;

                    pBits[offset].rgbGreen = 0;

                    pBits[offset].rgbBlue = 0;

                }

            }

    }

    // create the destination Bitmap object

    Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);

    // get the HDCs and select the HBITMAP

    Graphics graphics = Graphics.FromImage(bitmap);

   

    IntPtr hdcDest = graphics.GetHdc();

    IntPtr hdcSrc = CreateCompatibleDC(hdcDest);

    IntPtr hobjOriginal = SelectObject(hdcSrc, (IntPtr)ipd.Handle);

    // render the bitmap using AlphaBlend

    BLENDFUNCTION blendfunction = new BLENDFUNCTION(AC_SRC_OVER, 0, 0xFF, AC_SRC_ALPHA);

    AlphaBlend(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, width, height, blendfunction);

    // clean up

    SelectObject(hdcSrc, hobjOriginal);

    DeleteDC(hdcSrc);

    graphics.ReleaseHdc(hdcDest);

    graphics.Dispose();

    return bitmap;

}

Except for the “unsafe” block, the code should be pretty straightforward if you’re a Win32 GDI programmer: we create a new blank 32-bit HDC from a Bitmap object, create a compatible HDC to select the IPictureDisp’s HBITMAP into, render it with AlphaBlend, and clean up.

Now we need to look at the pixel manipulations inside the “unsafe” block. If we leave that section out, this is what we would get:

clip_image004

This is better – the shadow inside if the icon doesn’t look as bad, but we still have the white border in the regions of the icon that are completely transparent.

The problem happens because of an ambiguity that occurs when a pixel is completely transparent. In this case the A (‘alpha’) component of the pixel is zero, but the R, G and B components of the pixel can be anything since they don’t show up. What actually happens with those values is dependent on the convention that you follow. Unfortunately, Office follows a different convention than the AlphaBlend function does. The AlphaBlend function expects the RGB values to all be zero if the A value is zero. Office leaves the R, G and B values all equal to 255, which creates the white color seen in the images above. It does this so that the transparent pixels don’t turn out black if the image is “compacted” by GDI+ or the CLR, leaving us with images that look like this by default, which is even worse than what we started with:

clip_image006

Fortunately we can convert between the two conventions for the completely transparent pixels by checking for zero A values and zeroing out the RGB values. It takes some unsafe code to do it, but it works. Here’s how it looks:

clip_image008

It looks a lot better, but if you look carefully, it’s still not perfect. The shadow has been “halftoned”: all of the alpha values have been rounded to either 0 or 255, making the shadow either completely transparent or completely black. We want a nice gray gradient shadow.  It looks like the problem happens in the Bitmap object, when converting to and from the HDC. If you skip the intermediate Bitmap object and use the above code to draw directly to a Graphics object on a window, then it will render properly. I’ve played around with the PixelFormat, CompositingMode, and other parameters to the Graphics and Bitmap objects, but haven’t been able to make it work.

It looks like we’ll have to give up on using AlphaBlend and go down to the lowest level: pixel-by-pixel copying.  Since we were already doing per-pixel processing in the previous function, the new one actually looks simpler:

public static Bitmap ConvertPixelByPixel(IPictureDisp ipd)

{

    // get the info about the HBITMAP inside the IPictureDisp

    DIBSECTION dibsection = new DIBSECTION();

    GetObjectDIBSection((IntPtr)ipd.Handle, Marshal.SizeOf(dibsection), ref dibsection);

    int width = dibsection.dsBm.bmWidth;

    int height = dibsection.dsBm.bmHeight;

    // create the destination Bitmap object

    Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);

    unsafe

    {

        // get a pointer to the raw bits

        RGBQUAD* pBits = (RGBQUAD*)(void*)dibsection.dsBm.bmBits;

        // copy each pixel manually

        for (int x = 0; x < dibsection.dsBmih.biWidth; x++)

            for (int y = 0; y < dibsection.dsBmih.biHeight; y++)

            {

                int offset = y * dibsection.dsBmih.biWidth + x;

                if (pBits[offset].rgbReserved != 0)

                {

                    bitmap.SetPixel(x, y, Color.FromArgb(pBits[offset].rgbReserved, pBits[offset].rgbRed, pBits[offset].rgbGreen, pBits[offset].rgbBlue));

                }

            }

    }

    return bitmap;

}

 

Here’s what the final pixel-perfect result looks like:

clip_image010

The final question you should have now is "where can I see the complete source code?". Easy. See attached DisplayIconAddIn.zip to get a shared add-in that demos this concept.

DisplayIconAddIn.zip