Creating a windows thumbnail control : IExtractImage


In our last product release, I spent a few days creating a windows explorer thumbnail plug-in to see previews of our scene graph files. To say that there are few good examples is an understatement. Probably the best that I found was this one:

Codeproject: Create Thumbnail Extractor objects for your MFC document types
By Philipos Sakellaropoulos

In looking up this article, I also found this one:
Thumb Plug TGA

I found that this was a good starting point for understanding how thumbnail extraction works, but it left a lot to solve myself. It at least introduced me to the right API’s and let me debug a COM app (which I’d never really touched before.. ugh regsvr32)

The first problem was a practical one for the file types I had to deal with. NIF is a self-streaming file format. Each library may add one or more objects to be streamed. The files can often get pretty big as well. This would make it a nightmare for explorer to load up and render a NIF file every time a thumbnail needed to be generated. Rather than do that, I added some post-processing to embed a thumbnail at export time from the DCC apps (Max, Maya, Softimage). This was an optional step that people could choose to disable. What format to store the thumbnail in? I chose PNG for a few reasons:

  • D3D automatically supports writing to it using the D3DXSaveSurfaceToFileInMemory function.
  • PNG is relatively well compressed.
  • The explorer plug-in could relatively easily load PNG’s using the Gdiplus::Bitmap::FromStream function.

Then I had to modify our streaming format to accept an optional chunk of data as part of the header. Normal asset reads simply seek past this data if it exists. The thumbnail extractor reads the data into memory then sends it the Gdiplus for rendering. Here’s some code redacted for Emergent proprietary bits:

Gdiplus::Bitmap* CreateBitmapFromFile(WCHAR* filename)
{    
    Gdiplus::Bitmap* pThumbImage = NULL;
    const NiUInt8* pkSrcBuffer = NULL;
    unsigned int uiBufferSize = 0;

    // Load only the meta-data, not the graphics information
    //pseudo-code
    pkSrcBuffer = ReadMetaDataFromFile(filename, uiBufferSize);

    if (pkSrcBuffer == NULL)
    {
        ERROR_REPORT("!pBuffer");
        return NULL;
    }

    // Allocate and copy the embedded PNG file into a Bitmap
    HANDLE hBuffer  = ::GlobalAlloc(GMEM_MOVEABLE, uiBufferSize);
    if (hBuffer)
    {
        void* pBuffer = ::GlobalLock(hBuffer);
        if (pBuffer)
        {
            CopyMemory(pBuffer, pkSrcBuffer, uiBufferSize);

            IStream* pStream = NULL;
            if (::CreateStreamOnHGlobal(hBuffer, FALSE, &pStream) == S_OK)
            {
                pThumbImage = Gdiplus::Bitmap::FromStream(pStream);
                pStream->Release();

                if (pThumbImage)
                {
                    ::GlobalUnlock(hBuffer);
                    ::GlobalFree(hBuffer);
                    hBuffer = NULL;
                }
            }
        }
    }

    return pThumbImage;
}

Testing this plug-in was a huge pain. I essentially had to resort to MessageBox debugging as the windows shell won’t let you debug it. That is all of the ERROR_REPORT macros seen above. When I wasn’t being defensive about programming, the shell would crash and need to restart itself, a slow and tedious process.

The following code snippets outline how I dealt with the IExtractImage API. I chose to only handle IExtractImage and not IExtractImage2 as the extensions provided by IExtractImage2 didn’t really help get this quick-and-dirty project off the ground. Vista and above OSes have even better API’s, but I couldn’t live with that limitation and didn’t want to implement this twice. Note that if the PNG meta-data is missing, I use a missing image bitmap that I had embedded into the DLL. When testing it out I found that depending on the context, explorer would send really odd rectangles for me to render into, so I had to adjust aspect ratios. Note that in GetLocation I save off the filename and other info for later use in Extract. Why this wasn’t just passed in, I have no idea.

//--------------------------------------------------------------------------------------------------
STDMETHODIMP NifShlExt::Extract(HBITMAP* pBitmapImage)
{
    Gdiplus::Bitmap* pThumbImage = CreateBitmapFromFile(m_pcFilename);

    // If there isn't valid data, try using the built-in missing image data
    if (!pThumbImage || (pThumbImage->GetHeight() == 0) ||
        (pThumbImage->GetWidth() == 0))
    {
        // Skip this for the moment
        pThumbImage = CreateMissingBitmap();
    }

    // If the missing image isn't there, then fail.
    if (!pThumbImage || (pThumbImage->GetHeight() == 0) ||
        (pThumbImage->GetWidth() == 0))
    {
        ERROR_REPORT("failed missing image");
        return E_FAIL;
    }

    // Create a compatible rendering context
    HWND hwnd = GetDesktopWindow();
    HDC hdc = GetDC(hwnd);
    HDC memDC = CreateCompatibleDC(hdc);

    HRESULT hr = E_FAIL;

    if (memDC)
    {
        Gdiplus::Rect kCanvasRect(0, 0, m_kImageSize.cx, m_kImageSize.cy);

        // We have no guarantees about the rectangle we will be rendering to
        // so we need to scale the image from the file to fit appropriately.
        // XP has a habit of sending odd aspect ratios, so getting this right
        // is important.
        int iOffsetX = 0;
        int iOffsetY = 0;
        Gdiplus::Rect kScaledRect = CalculateScaleRect(
            Gdiplus::Rect(0, 0, pThumbImage->GetWidth(),
            pThumbImage->GetHeight()), kCanvasRect,
            iOffsetX, iOffsetY);

        // Fill out the descriptor for the bitmap we are going
        // to render into.
        BITMAPINFO kBitmapInfo;
        ZeroMemory(&kBitmapInfo, sizeof(kBitmapInfo));
        kBitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        kBitmapInfo.bmiHeader.biWidth = m_kImageSize.cx;
        kBitmapInfo.bmiHeader.biHeight = m_kImageSize.cy;
        kBitmapInfo.bmiHeader.biPlanes = 1;
        kBitmapInfo.bmiHeader.biBitCount = (WORD)m_uiColorDepth;
        kBitmapInfo.bmiHeader.biCompression = BI_RGB;

        // Create the bitmap buffer
        LPVOID pBits;
        HBITMAP hBitmap = CreateDIBSection(memDC, (LPBITMAPINFO)&kBitmapInfo,
            DIB_RGB_COLORS, &pBits, NULL, 0);
        HGDIOBJ hOldObj = SelectObject(memDC, hBitmap);

        // Clear the bitmap and draw the thumbnail into it
        // using the right scale.
        Gdiplus::Graphics kGraphics(memDC);
        Gdiplus::SolidBrush kClearBrush(Gdiplus::Color(128, 255, 255, 255));
        kGraphics.FillRectangle(&kClearBrush, kCanvasRect);
        kGraphics.SetInterpolationMode(
            Gdiplus::InterpolationModeHighQualityBicubic);
        Gdiplus::Status stat = kGraphics.DrawImage(pThumbImage,
            iOffsetX, iOffsetY, kScaledRect.Width, kScaledRect.Height);

        if (stat == Gdiplus::Ok)
        {
            *pBitmapImage = hBitmap;
            hr = NOERROR;
        }

        // Return the device context to its original state.
        SelectObject(memDC, hOldObj);
        DeleteDC(memDC);
        ReleaseDC(hwnd, hdc);
    }

    delete pThumbImage;

    return hr;
}
//--------------------------------------------------------------------------------------------------
Gdiplus::Bitmap* CreateMissingBitmap()
{
    // Return the missing bitmap image from the resource table
    return Gdiplus::Bitmap::FromResource(g_hinstance,
        MAKEINTRESOURCEW(IDB_MISSING_THUMB_BMP));
}
//--------------------------------------------------------------------------------------------------
Gdiplus::Rect CalculateScaleRect(Gdiplus::Rect kSrc, Gdiplus::Rect kDest,
    int& iOffsetX, int& iOffsetY)
{
    iOffsetX = 0;
    iOffsetY = 0;
    Gdiplus::Rect kResult;
    if (kDest.Width <= kDest.Height)
    {
        kResult = Gdiplus::Rect(0, 0, kDest.Width,
            kSrc.Height * kDest.Width / kSrc.Width);
    }
    else
    {
        kResult = Gdiplus::Rect(0, 0, kSrc.Width * kDest.Height / kSrc.Height,
            kDest.Height);
    }

    iOffsetX = abs((kDest.Width - kResult.Width)/2);
    iOffsetY = abs((kDest.Height - kResult.Height)/2);

    return kResult;
}
//--------------------------------------------------------------------------------------------------
STDMETHODIMP NifShlExt::GetLocation(LPWSTR pFilename, DWORD dwBufferSize,
    DWORD* pPriority, const SIZE* pSize, DWORD dwColorDepth,
    DWORD* pFlags)
{
     if ((pSize == NULL) || (pFlags == NULL) ||
         ((*pFlags & IEIFLAG_ASYNC) && (pPriority == NULL)))
     {
         return E_INVALIDARG;
     }

     // Copy the values for later use in Extract
     m_kImageSize = *pSize;
     m_uiColorDepth = dwColorDepth;

     *pFlags = IEIFLAG_CACHE;

     // Copy the filename locally for use later
     wcsncpy_s(pFilename, dwBufferSize, m_pcFilename, dwBufferSize);

     return S_OK;
}
//--------------------------------------------------------------------------------------------------

The final result looks a little something like this:

As I was doing this I went to ask a co-worker, Todd Berkebile, if he knew anything about the IExtractImage API as I knew that he worked on the windows shell team at Microsoft years ago. It turns out that he was the author of this API. Small world. Unfortunately, his memory was hazy and I was already far enough along that I had gotten through the most major hurdles.

I’m sure that there are better ways to do some of this work. This was a quick project to make the assets a little easier to use. Feedback has been pretty positive thus far and I’m glad I took a few days out to write this little code extension. One major known limitation is that this will not work on 64-bit OS’es. You have to run the 32-bit shell on that OS to see the thumbnails.

Advertisements

~ by shaunkime on January 7, 2010.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: