帶Alpha通道的圖像(ARBG)在通過GDIPlus::Bitmap::FromHBITMAP等轉為GDI+位圖,再存儲時,透明區域會變成純黑(也有可能是純白?)。
網上找了兩段保持透明的實現代碼,列在下邊,經測試,第一段無效,第二段有效,這兩段代碼正好可以對比說明:FromHBITMAP在拷貝圖像數據時,原圖中的Alpha數據確實沒有Copy過來,而並非是未設置圖像屬性的問題。
第一段的思路是:直接用
FromHBITMAP創建一個GDI+位圖,新建另一個帶PixelFormat32bppARGB標識的位圖,再從前者拷貝數據到后者;
第二段的思路是:獲取BITMAP數據,新建一個
帶
PixelFormat32bppARGB標識的位圖,再從前者拷貝數據到后者;
第二段代碼也不夠好,應該先判斷一下位圖是不是32位的,帶Alpha通道的只有ARGB格式,ARGB是32位的,所以不是32位的不需要用這種方式來存儲。如果要拷貝數據的話,不是32位的格式必須處理行邊界對齊的問題。
bool ImageUtil :: CreateGdiplusBmpFromHBITMAP_Alpha ( HBITMAP hBmp , Gdiplus :: Bitmap ** bmp )
{
BITMAP bitmap ;
GetObject ( hBmp , sizeof ( BITMAP ), & bitmap );
if ( bitmap . bmBitsPixel != 32)
{
return false ;
}
Gdiplus :: Bitmap * pWrapBitmap = Gdiplus :: Bitmap :: FromHBITMAP ( hBmp , NULL );
if ( pWrapBitmap )
{
Gdiplus :: BitmapData bitmapData ;
Gdiplus :: Rect rcImage (0, 0, pWrapBitmap -> GetWidth (), pWrapBitmap -> GetHeight ());
pWrapBitmap -> LockBits (& rcImage , Gdiplus :: ImageLockModeRead , pWrapBitmap -> GetPixelFormat (), & bitmapData );
* bmp = new Gdiplus :: Bitmap ( bitmapData . Width , bitmapData . Height , bitmapData . Stride , PixelFormat32bppARGB , ( BYTE *) bitmapData . Scan0 );
pWrapBitmap -> UnlockBits (& bitmapData );
delete pWrapBitmap ;
return true ;
}
return false ;
}
Gdiplus :: Bitmap * ImageUtil :: CreateBitmapFromHBITMAP ( IN HBITMAP hBitmap )
{
BITMAP bmp = { 0 };
if ( 0 == GetObject ( hBitmap , sizeof ( BITMAP ), ( LPVOID )& bmp ) )
{
return FALSE ;
}
// Although we can get bitmap data address by bmp.bmBits member of BITMAP
// which is got by GetObject function sometime,
// we can determine the bitmap data in the HBITMAP is arranged bottom-up
// or top-down, so we should always use GetDIBits to get bitmap data.
BYTE * piexlsSrc = NULL ;
LONG cbSize = bmp . bmWidthBytes * bmp . bmHeight ;
piexlsSrc = new BYTE [ cbSize ];
BITMAPINFO bmpInfo = { 0 };
// We should initialize the first six members of BITMAPINFOHEADER structure.
// A bottom-up DIB is specified by setting the height to a positive number,
// while a top-down DIB is specified by setting the height to a negative number.
bmpInfo . bmiHeader . biSize = sizeof ( BITMAPINFOHEADER );
bmpInfo . bmiHeader . biWidth = bmp . bmWidth ;
bmpInfo . bmiHeader . biHeight = bmp . bmHeight ; // 正數,說明數據從下到上,如未負數,則從上到下
bmpInfo . bmiHeader . biPlanes = bmp . bmPlanes ;
bmpInfo . bmiHeader . biBitCount = bmp . bmBitsPixel ;
bmpInfo . bmiHeader . biCompression = BI_RGB ;
HDC hdcScreen = CreateDC ( L "DISPLAY" , NULL , NULL , NULL );
LONG cbCopied = GetDIBits ( hdcScreen , hBitmap , 0, bmp . bmHeight ,
piexlsSrc , & bmpInfo , DIB_RGB_COLORS );
DeleteDC ( hdcScreen );
if ( 0 == cbCopied )
{
delete [] piexlsSrc ;
return FALSE ;
}
// Create an GDI+ Bitmap has the same dimensions with hbitmap
Bitmap * pBitmap = new Bitmap ( bmp . bmWidth , bmp . bmHeight , PixelFormat32bppPARGB );
// Access to the Gdiplus::Bitmap's pixel data
BitmapData bitmapData ;
Rect rect (0, 0, bmp . bmWidth , bmp . bmHeight );
if ( Ok != pBitmap -> LockBits (& rect , ImageLockModeRead ,
PixelFormat32bppPARGB , & bitmapData ) )
{
delete ( pBitmap );
return NULL ;
}
BYTE * pixelsDest = ( BYTE *) bitmapData . Scan0 ;
int nLinesize = bmp . bmWidth * sizeof ( UINT );
int nHeight = bmp . bmHeight ;
// Copy pixel data from HBITMAP by bottom-up.
for ( int y = 0; y < nHeight ; y ++ )
{
// 從下到上復制數據,因為前面設置高度時是正數。
memcpy_s (
( pixelsDest + y * nLinesize ),
nLinesize ,
( piexlsSrc + ( nHeight - y - 1) * nLinesize ),
nLinesize );
}
// Copy the data in temporary buffer to pBitmap
if ( Ok != pBitmap -> UnlockBits (& bitmapData ) )
{
delete pBitmap ;
}
delete [] piexlsSrc ;
return pBitmap ;
}
使用ATL::CImage來保存圖像也會存在黑底的問題,但這並非上述丟失Alpha的問題,事實上,
ATL::CImage::Attach不會創建位圖的副本。
下面是ATL::CImage在Attach到一個位圖名柄后執行的操作,從ATL::Image::UpdateBitmapInfo的實現中可見,它的問題在於默認認為圖像是不帶Alpha通道的(置m_bHasAlphaChannel為false
),繼而在保存操作中,轉為Gdiplus::Bitmap時沒有使用PixelFormat32bppARGB標志。
最后,我從ATL::CImage里邊的代碼提取了一個函數,應該比上邊的靠譜並且效率高點。
bool ImageUtil::SavePng( HBITMAP hBmp, LPCTSTR lpszFilePath )
{
DIBSECTION dibsection;
int nBytes = ::GetObject( hBmp, sizeof( DIBSECTION ), &dibsection );
Gdiplus::Bitmap* bitmap = 0;
if(nBytes != sizeof(DIBSECTION) || dibsection.dsBm.bmBitsPixel != 32)
{
// Bitmap with plate or non-ARGB(32bpp)
bitmap = Gdiplus::Bitmap::FromHBITMAP(hBmp);
}
else
{
int width, height, bits_per_pixel, pitch;
LPVOID bits;
width = dibsection.dsBmih.biWidth;
height = abs( dibsection.dsBmih.biHeight );
bits_per_pixel = dibsection.dsBmih.biBitCount;
pitch = (((width*bits_per_pixel)+31)/32)*4; //計算行寬,四字節對齊 ATL::CImage::ComputePitch
bits = dibsection.dsBm.bmBits;
if( dibsection.dsBmih.biHeight > 0 )
{
bits = LPBYTE( bits )+((height-1)*pitch);
pitch = -pitch;
}
bitmap = new Gdiplus::Bitmap(width, height, pitch, PixelFormat32bppARGB, static_cast< BYTE* >(bits ));
}
bool ret = false;
CLSID clsid = GetGdiplusEncoderClsid(NULL, &Gdiplus::ImageFormatPNG);
if(clsid != CLSID_NULL)
{
ret = (Gdiplus::Ok == bitmap->Save(lpszFilePath, &clsid, NULL));
}
delete bitmap;
return ret;
}
補充關於Gdi+里圖像編碼器的一點:每個解碼器都有CLSID和FORMAT兩個數據,實際上都是GUID,不能搞混了,Gdiplus::Bitmap::Save中接收的是CLSID,Gdiplus中提供的預定義ImageFormatXXX是指解碼器的Format,很奇葩,細看了一下代碼才搞清楚。見ATL::CImage::FindCodecForFileType。
經測試,此代碼亦存在問題,見下一篇日記。
