Converting between IPictureDisp and System.Drawing.Image

This is an interesting manifestation of the gap between COM-based native servers, such as the Office client apps, and managed code. There are a number of scenarios where you need to convert image formats between the native COM-based IPictureDisp and the managed System.Drawing.Image class and its derivatives such as Bitmap. One such scenario is in providing images for custom ribbons, including ribbon images used for Outlook custom form regions.

You have several choices for performing the conversion, depending on the direction of the conversion.

The "traditional" approach is to use the GetIPictureDispFromPicture and GetPictureFromIPicture methods in the System.Windows.Forms.AxHost class. These are both protected members of the class, so you can't use them externally. For this reason, it is common to subclass the AxHost class and expose public methods that internally call the base class protected methods. This approach allows you to convert in both directions:

internal class AxHostConverter : AxHost

{

    private AxHostConverter() : base("") { }

    static public stdole.IPictureDisp ImageToPictureDisp(Image image)

    {

        return (stdole.IPictureDisp)GetIPictureDispFromPicture(image);

    }

    static public Image PictureDispToImage(stdole.IPictureDisp pictureDisp)

    {

        return GetPictureFromIPicture(pictureDisp);

    }

}

Note that this is a peripheral capability of the AxHost class – its main purpose is to act as a base class for derived classes that host native ActiveX controls in managed Windows.Forms controls. When you add an ActiveX control to a managed project, Visual Studio uses the aximp.exe tool to generate and compile a class derived from AxHost. For documentation, see here.

Your second option is to use OleLoadPicture or OleCreatePictureIndirect. Tom Quinn mentions this in his blog post here, and there's an old support article on the topic here. OleLoadPicture creates a new picture object and initializes it from the contents of a stream. This is equivalent to calling OleCreatePictureIndirect(NULL, ...) followed by IPersistStream::Load. You'll need to p/invoke to these native methods. As you can see from the managed declaration of OleCreatePictureIndirect below, it takes a particular struct type as its first parameter – so, you need to declare this struct type also. This method (and the real definition of the native struct) can accommodate multiple image formats, including bitmap, icon and metafile. The simplified example listed here only supports bitmap. The struct is only used when converting from an Image to an IPictureDisp – the reverse operation is simpler, being basically just a call to Image.FromHbitmap. For documentation, see here.

internal class OleCreateConverter

{

    [DllImport("oleaut32.dll", EntryPoint = "OleCreatePictureIndirect",

        CharSet = CharSet.Ansi, ExactSpelling = true, PreserveSig = true)]

    private static extern int OleCreatePictureIndirect(

        [In] PictDescBitmap pictdesc, ref Guid iid, bool fOwn,

        [MarshalAs(UnmanagedType.Interface)] out object ppVoid);

    const short _PictureTypeBitmap = 1;

    [StructLayout(LayoutKind.Sequential)]

    internal class PictDescBitmap

    {

        internal int cbSizeOfStruct = Marshal.SizeOf(typeof(PictDescBitmap));

        internal int pictureType = _PictureTypeBitmap;

        internal IntPtr hBitmap = IntPtr.Zero;

        internal IntPtr hPalette = IntPtr.Zero;

        internal int unused = 0;

        internal PictDescBitmap(Bitmap bitmap)

        {

            this.hBitmap = bitmap.GetHbitmap();

        }

    }

    public static stdole.IPictureDisp ImageToPictureDisp(Image image)

    {

        if (image == null || !(image is Bitmap))

        {

            return null;

        }

        PictDescBitmap pictDescBitmap = new PictDescBitmap((Bitmap)image);

        object ppVoid = null;

        Guid iPictureDispGuid = typeof(stdole.IPictureDisp).GUID;

        OleCreatePictureIndirect(pictDescBitmap, ref iPictureDispGuid, true, out ppVoid);

        stdole.IPictureDisp picture = (stdole.IPictureDisp)ppVoid;

        return picture;

    }

    public static Image PictureDispToImage(stdole.IPictureDisp pictureDisp)

    {

        Image image = null;

        if (pictureDisp != null && pictureDisp.Type == _PictureTypeBitmap)

        {

            IntPtr paletteHandle = new IntPtr(pictureDisp.hPal);

            IntPtr bitmapHandle = new IntPtr(pictureDisp.Handle);

            image = Image.FromHbitmap(bitmapHandle, paletteHandle);

        }

        return image;

    }

}

Your third option is to use the VB6 compatibility library, documented here. To use this, you'll need to add a reference to Microsoft.VisualBasic.Compatibility.dll, which is listed on the .NET tab of the Add References dialog (it resides in the GAC). Then, you can use the ImageToIPictureDisp and IPictureDispToImage methods in the Support class. This is obviously by far the simplest approach, although it does pull in the VB6 compatibility DLL. Internally, the VB6 compatibility code looks a lot like the second option above – using OleCreatePictureIndirect.

using Microsoft.VisualBasic.Compatibility.VB6;

internal class VB6CompatibilityConverter

{

    public static stdole.IPictureDisp ImageToPictureDisp(Image image)

    {

        return (stdole.IPictureDisp)Support.ImageToIPictureDisp(image);

    }

    public static Image PictureDispToImage(stdole.IPictureDisp pictureDisp)

    {

        return Support.IPictureDispToImage(pictureDisp);

    }

}

Finally, you can implement IPictureDisp and IPicture yourself, documented here and here. This is fine if you just want to convert from an Image to an IPictureDisp, but doesn't help you converting in the other direction. The implementation below relies on the Image actually being a derived Bitmap type, because we call Bitmap.GetHbitmap internally. If you want to keep the support to the generic Image type, you'll have to do a lot more work to p/invoke to a bunch of undocumented GDI methods instead.

internal class PictureDispConverter

{

    public static stdole.IPictureDisp BitmapToPictureDisp(Bitmap bitmap)

    {

        return new PictureDispImpl(bitmap);

    }

    public static Image PictureDispToBitmap(stdole.IPictureDisp pictureDisp)

    {

        // TODO

        return null;

    }

}

internal class PictureDispImpl : stdole.IPictureDisp, stdole.IPicture

{

    #region Init

    [DllImport("gdi32.dll")]

    static extern void DeleteObject(IntPtr handle);

    private Bitmap _image;

    private IntPtr _handle;

    public PictureDispImpl(Bitmap image)

    {

        _image = image;

    }

    ~PictureDispImpl()

    {

        if (_handle != IntPtr.Zero)

        {

            DeleteObject(_handle);

        }

    }

    #endregion

    #region IPictureDisp Members

    public int Width

    {

        get { return _image.Width; }

    }

    public int Height

    {

        get { return _image.Height; }

    }

    public short Type

    {

        get { return 1; }

    }

    public int Handle

    {

        get

        {

            if (_handle == IntPtr.Zero)

            {

                _handle = _image.GetHbitmap();

            }

            return _handle.ToInt32();

        }

    }

    public int hPal

    {

        get { return 0; }

        set { }

    }

    public void Render(

        int hdc, int x, int y, int cx, int cy, int xSrc, int ySrc, int cxSrc, int cySrc, IntPtr prcWBounds)

    {

        Graphics graphics = Graphics.FromHdc(new IntPtr(hdc));

        graphics.DrawImage(

            _image, new Rectangle(x, y, cx, cy), xSrc, ySrc, cxSrc, cySrc, GraphicsUnit.Pixel);

    }

    #endregion

    #region IPicture Members

    public int Attributes

    {

        get { return 0; }

    }

    public int CurDC

    {

        get { return 0; }

    }

    public bool KeepOriginalFormat

    {

        get { return false; }

        set { }

    }

    public void PictureChanged()

    {

    }

    public void SaveAsFile(IntPtr pstm, bool fSaveMemCopy, out int pcbSize)

    {

        pcbSize = 0;

    }

    public void SelectPicture(int hdcIn, out int phdcOut, out int phbmpOut)

    {

        phdcOut = 0;

        phbmpOut = 0;

    }

    public void SetHdc(int hdc)

    {

    }

    #endregion

}

All of the methods and types described above are documented, so all of these approaches are valid. Obviously, you'll have to do some work to write the helper classes along the lines indicted above, and some require more work (and maintenance) than others, of course, but you have options to choose from.

The one thing that is missing from all the IPictureDisp-to-Image conversions is the handling of alpha channels. I haven't quite figured out how to preserve the alpha channel, although Eric assures me that some tinkering with AlphaBlend ought to work. I'll play with that another day.