cwinapi

A faster way to test 32bpp DDBs for a valid Alpha channel


Windows GDI object HBITMAP does not contain any flags that mark it as having a valid alpha channel. It is possible to obtain some information about it through the function GetObject() which can return the BITMAP structure. This structure contains the bmBitsPixel field that can be used to eliminate bitmap formats that are less than 32bpp and thus cannot contain the alpha channel.

When bmBitsPixel==32 the format of the bitmap can be 0RGB or ARGB and as far as I know, the only way to distinguish between these two cases is checking ALL pixel values for the presence of non-zero Alpha value (please correct me if I am wrong). Even if 1 pixel has a non-zero Alpha value, Windows treats it as an ARGB bitmap.

This determination has profound consequences for ICON handling and the behavior of some function such as, but not limited to, the DrawIcon()and DrawIconEx(). These functions ignore or account for the monochrome mask (ICONINFO.hbmMask) that contains the transparency data for the entire icon, depending on some flags and whether the format of the icon's color bitmap is 0RGB or ARGB.

The code below performs the classification of a DDB bitmap as 0RGB or ARGB by shrinking it to 1x1px and testing for non-zero Alpha value.
Q: Is there a more optimal way to perform such classification ?

Note: I heard that bringing DDB's pixel data to User Mode is costly.

BOOL HasAlpha(HBITMAP hBmp)
{
    BOOL res = FALSE;
    BITMAP bmp = {0};
    GetObject(hBmp, sizeof(BITMAP), &bmp);

    if (bmp.bmBitsPixel == 32)
    {
        HDC hDCsrc = CreateCompatibleDC(0);
        HBITMAP hBmpOldSrc = (HBITMAP)SelectObject(hDCsrc, hBmp);
        HDC hDCdst = CreateCompatibleDC(hDCsrc);
        HBITMAP hBmpDst = CreateCompatibleBitmap(hDCsrc, 1, 1);
        HBITMAP hBmpOldDst = (HBITMAP)SelectObject(hDCdst, hBmpDst);
        PDWORD pPixel;

        SetStretchBltMode(hDCdst, STRETCH_ORSCANS); //STRETCH_ANDSCANS
        StretchBlt(hDCdst, 0, 0, 1, 1, hDCsrc, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
           
        if (pPixel = (PDWORD)GetBMBits(hDCdst, DIB_RGB_COLORS))
        {
            if (*pPixel & 0xFF000000)
                res = TRUE;

            _aligned_free(pPixel);
        }

        SelectObject(hDCsrc, hBmpOldSrc);
        DeleteObject(SelectObject(hDCdst, hBmpOldDst));
        DeleteDC(hDCsrc);
        DeleteDC(hDCdst);
    }

    return res;
}

LPVOID GetBMBits(HDC hDC, UINT usage)    //When done must free the result with _aligned_free().  Also, usage = DIB_RGB_COLORS or DIB_PAL_COLORS
{
    LPVOID pMem = NULL;
    BITMAPINFO BmpInf[2]; BmpInf[0].bmiHeader.biBitCount = 0; BmpInf[0].bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

    HBITMAP hBmp = HBITMAP(SelectObject(hDC, CreateCompatibleBitmap(hDC, 0, 0)));  //Push out the bitmap that was in DC

    if (GetDIBits(hDC, hBmp, 0, 1, NULL, &BmpInf[0], usage))
    {
        if (pMem = _aligned_malloc(BmpInf[0].bmiHeader.biSizeImage, sizeof(DWORD)))
        {
            if (!GetDIBits(hDC, hBmp, 0, BmpInf[0].bmiHeader.biHeight, pMem, &BmpInf[0], usage))
            {
                _aligned_free(pMem);
                pMem = NULL;
            }
        }
    }

    DeleteObject(SelectObject(hDC, hBmp));
    return pMem;
}

Solution

  • if you have HBITMAP as input code can be next

    BOOL HasAlpha(PBYTE pvBits, ULONG cb)
    {
        pvBits += 3;// pointer to alpha byte
        cb >>= 2;
        do 
        {
            if (*pvBits)
            {
                break;
            }
        } while (pvBits += 4, --cb);
    
        return cb;
    }
    
    BOOL HasAlpha(HBITMAP hBmp)
    {
        BITMAP bm;
        if (GetObject(hBmp, sizeof(bm), &bm) && 
            1 == bm.bmPlanes && 
           32 == bm.bmBitsPixel)
        {
            ULONG cb = bm.bmWidthBytes * bm.bmHeight;
    
            if (bm.bmBits)
            {
                return HasAlpha((PBYTE)bm.bmBits, cb);
            }
            
            if (bm.bmBits = LocalAlloc(LMEM_FIXED, cb))
            {
                if (cb = GetBitmapBits(hBmp, cb, bm.bmBits))
                {
                    cb = HasAlpha((PBYTE)bm.bmBits, cb);
                }
                LocalFree(bm.bmBits);
                return cb;
            }
        }
    
        return FALSE;
    }
    

    as noted Simon Mourier some system DLLs (ExplorerFrame, shell32, Taskbar, twinui.pcshell, Windows.Internal.Shell.Broker, Windows.Storage, Windows.UI.CredDialogController) have internal function:

    HRESULT _DoesBitmapHaveAlpha(_In_ HBITMAP hbmp, _Out_ bool * pHave);
    

    which have similar implementation:

    first call GetObject, then GetDC(0) and GetDIBits. It does not check for BITMAP::bmBits which can be already not 0, so we not need call GetDIBits. I use GetBitmapBits instead GetDIBits. then function check only alpha byte from bmBits and decide that alpha exist if some alpha byte not 0. I bit optimize my implementation, after I look for system code:

    was

    BOOL HasAlpha(PULONG pvBits, ULONG cb)
    {
        cb >>= 2;
        do 
        {
            if (*pvBits++ >> 24)
            {
                break;
            }
        } while (--cb);
    
        return cb;
    }
    

    and I replace it with:

    BOOL HasAlpha(PBYTE pvBits, ULONG cb)
    {
        pvBits += 3;// pointer to alpha byte
        cb >>= 2;
        do 
        {
            if (*pvBits)
            {
                break;
            }
        } while (pvBits += 4, --cb);
    
        return cb;
    }
    

    sense here - no more need >> 24 on DWORD or & 0XFF000000 - we just read single appha byte, instead ARGB dword.

    but my code stop loop after just found first not 0 alpha, when system implementation always iterate over all image bytes unconditionally ( which is not the best)


    the _DoesBitmapHaveAlpha is not exported, so we can not call it from any dll.

    it always called from another not exported API:

    HRESULT CreateBitmapFromIconWithAlpha(_In_ HICON hi, _In_ int size, _Out_ HBITMAP * pbmp);
    

    which can be called from several locations, as example

    HRESULT _CreateBitmapFromSystemImageListWithAlpha(
        int iIndex, //= SHMapPIDLToSystemImageListIndex
        int dwFlags,
        int size,
        HBITMAP *);
    HRESULT CShellItem::_GetIcon(SIZE s, int dwFlags, bool b, HBITMAP *);
    virtual HRESULT CShellItem::GetSharedBitmap(SIZE s, int dwFlags, ISharedBitmap * *);