Preserving the alpha channel when converting images
Here is a guest post by Eric Faller, Ribbon developer guru and all-round nice guy. It's a follow-up to my 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:
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:
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:
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:
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:
Comments
- Anonymous
October 10, 2007
PingBack from http://www.artofbam.com/wordpress/?p=6996 - Anonymous
October 11, 2007
There's a nice screencast to check out that shows how to use the Ribbon designer and use built in Office - Anonymous
February 13, 2008
Its quite amazing how all the awesome and really helpfull stuff is always written in C# I would once and for all like to know (as an avid vb.net programmer) if C# is actually better, the pro's certainly think so, i am tired of reading flame wars backwards and forwards and just want a straightforward honest answer so I can move on...Great article, I love you altruistic bloggers, sharing the love - Anonymous
June 28, 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.jpgThe 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
July 02, 2008
Unfortunately the CommandBars API does not support alpha channels in icons. The API was created back for Office 97, long before alpha channels became popular for Office icons. Fortunately, this problem is fixed now that we have the Ribbon, as discussed above.I understand that it doesn't help you right now with Project 2007, but one good thing is that it has been confirmed that the next version of Project will have the Ribbon:http://www.pcworld.com/businesscenter/article/139092/microsoft_shows_off_preview_of_office_project_software.html - Anonymous
January 19, 2009
Excellent Article.I was able to use your code to extract all the icons in Word 2007, something like this: IPictureDisp ipd = MyApplication.CommandBars.GetImageMso(<idMSO OF ICON>, 32, 32); Bitmap bm = IPictureDispConverter.ConvertPixelByPixel(ipd); AtalaImage img = AtalaImage.FromBitmap(bm); img.Save(@"C:Temp32" + input + ".png", new Atalasoft.Imaging.Codec.PngEncoder(), null);I used the Atalasoft Imaging software to save the bitmap in png format. Your code worked perfectly. Thank you. - Anonymous
January 29, 2009
What's the best way to convert a .png or an HBITMAP to an IPictureDisp without loosing the alpha channel? - Anonymous
January 30, 2009
Hasso,The best way to do that is to use GDI+, either in C++ or .NET. If you're loading images for the Ribbon in .NET, you can actually just return a System.Bitmap object directly - no need to convert it to IPictureDisp.See this post on Jensen Harris' blog for more details about loading up images and preserving the alpha channel:http://blogs.msdn.com/jensenh/archive/2006/11/27/ribbonx-image-faq.aspx - Anonymous
February 04, 2009
Will you please post a code snippet? The RibbonX Image FAQ blog was helpful in recommending the use of .png files and GDI+, but it didn't give enough detail for me. I couldn't find anything about IPictureDisps in Microsoft's GDI+ documentation, either.So far, I've tried PICTDESC's (the entire icon is transparent) and Gdiplus::Bitmap's (the icon is a white square with scattered black pixels).Are there any options besides OleCreatePictureIndirect?Thanks! - Anonymous
February 04, 2009
Hasso,Please note that this is not a technical support forum. That said, here is a rough outline of how the code would work:Load your PNG file using a GDI+ Bitmap object.Get the HBITMAP from the Bitmap using the GetHBITMAP() method. Put the HBITMAP into a PICTDESC structure. Call OleCreatePictureIndirect() to create the IPictureDisp object. The only other method I know of to create IPictureDisp is to use OleLoadPicture, which unfortunately does not support alpha channels.A last-resort option is to write a class which implements the IPicture interface directly. However since it just returns an HBITMAP, there really is no reason not to just use OleCreatePictureIndirect.