Bagikan melalui


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

Comments

  • Anonymous
    October 10, 2007
    PingBack from http://www.artofbam.com/wordpress/?p=7015

  • Anonymous
    October 10, 2007
    Thanks again and a lot Misha, you thrown away all of my nightmares. You are an MMVP (Mega most valuable person) ;-).

  • Anonymous
    November 16, 2007
    Thanks Misha, I just ran into this 'problem' and you've given the perfect solution! :-)

  • Anonymous
    May 27, 2008
    Can it work on .NET Icon or Bitmap? (not IPictureDisp)

  • Anonymous
    May 30, 2008
    Yaron, all I can tell you is "it depends" :) Really, if you want something more intelligent than that - I would need you to ask a more intelligent and focused question.

  • Anonymous
    June 24, 2008
    I am having problems with transparent icons in my Microsoft Project 2007 add-in menus. I have tried using masks, but what I really want is real alpha values. Currently my menus look like this: http://img261.imageshack.us/img261/1361/problemra1.jpg The background of the second menu item should be transparent. Does your code help me solve this problem? What is the offical/best way to assign IPictureDisp that include transparency. I noticed that native office icons (such as the hyperlink icon) seem to be transparent. Is it possible for me to do the same? Thanks.

  • Anonymous
    November 06, 2008
    AMAZING POST!! Any idea how to programmatically tell if the result is a 32x32 icon (i.e. not a 16x16 one that has been automatically resized)?? Thanks

  • Anonymous
    August 11, 2009
    How do I use the attachment? :S

  • Anonymous
    July 27, 2012
    Would be nice if this could be done in VBA instead of C+.  Unfortunately, setting a userform's image picture property to the result of Application.CommandBars.GetImageMSO(picName,32,32) yields ugly results.

  • Anonymous
    February 10, 2014
    Finding the best image suited for a custom control is not that easy, as there are about 4,000 unique icons in Office 2013. This free-UNLOCKED Excel VBA Add-in uses the Ribbon Commander framework to display dynamically built-in imageMSO icons or imported images in Excel's Ribbon (in buttons or gallery items). What you see, is what you get in your Ribbon ! The imageMSO or any image list can be filtered using with a description keyword search or can be browsed sequentially in a ribbon gallery. The descriptions of any buttons clicked are saved in a list, which can be exported for use with the Custom UI editor or re-imported to the Add-in for viewing. See more at: www.spreadsheet1.com/dynamic-icon-browser.html