windows gdi+ 是對 windows gdi 的一個c++封裝,同時增加了一些擴展功能,如反走樣,樣條曲線,變換矩陣,圖像編解碼等。
gdi+ 相對於 gdi 也存在一些不足之處,如 執行效率較低,不支持位運算(gdi可通過位運算實現局部透明)等。
在實際應用中,由於主要做位圖相關操作,我比較傾向選擇使用 gdi 進行繪圖,這樣可以得到較高的繪制速度,以及更多的靈活性。
對於 gdi+,我主要關注點在於 Bitmap 類,下面只討論 Bitmap 類相關內容。
1 使用 gdi+ 的安全考慮
首先,Microsoft 給出了使用 gdi+ 的 Security Considerations,我將其記錄下來,以提醒自己。
1)檢查構造函數是否成功
通過 Image::GetLastStatus() 函數檢查是否成功構造位圖,示例代碼如下:
Image myImage(L"Climber.jpg"); Status st = myImage.GetLastStatus(); if(Ok == st) // The constructor was successful. Use myImage. else if(InvalidParameter == st) // The constructor failed because of an invalid parameter. else // Compare st to other elements of the Status // enumeration or do general error processing.
2)某些函數調用前需要分配足夠內存,代碼如下:
GraphicsPath path; path.AddEllipse(10, 10, 200, 100); INT count = path.GetPointCount(); // get the size Point* pointArray = new Point[count]; // allocate the buffer if(pointArray) // Check for successful allocation. { path.GetPathPoints(pointArray, count); // get the data ... // use pointArray delete[] pointArray; // release the buffer pointArray = NULL; }
3)錯誤檢查,檢查函數返回值以確保函數執行成功。
4)線程同步
當一個gdi+對象在多個線程中使用時,gdi+沒有提供自動同步機制,需要程序員確保線程同步;
官網同時解釋到,某些gdi+函數可能返回ObjectBusy,但不要僅指望該機制實現線程同步,程序員需要使用如互斥量等方式以確保線程同步。
2 Bitmap類所支持文件格式
windows gdi+ 提供了 Image 類處理光柵圖像與矢量圖像,該類提供一些共有的處理函數,如圖像打開,圖像保存,圖像尺寸等。
對於更多不同的處理函數,Image 分別派生出 Bitmap 與 Metafile l類,Bitmap 負責光柵圖像相關處理,Metafile 負責矢量圖像相關處理。
Bitmap 包含了基本圖像編解碼功能,支持特定圖像格式,如果需要支持更多其他圖像格式,windows 提供了 WIC(windows image component)可進行擴展。
一般情況下,Bitmap 所提供的幾種圖像格式已經可以滿足需求,主要圖像文件格式包括:
1)BMP
這是一個標准的非壓縮圖像文件格式,用於存儲設備無關位圖,支持位深包括 1,2,4,8,15(16),24,32,64。
2)GIF(Graphics Interchange Format)
該圖像格式常用於網絡傳輸中,采用無損壓縮,支持透明,支持多幀(動畫),其最大位深為 8 位。
3)PNG(Portable Network Graphics)
PNG與GIF類似,采用無損壓縮,但支持更大位深,彩色圖像位深可以為 8, 24,48,黑白圖像位深可以為 1,2,4,8,16,同時支持漸進顯示,以及存儲gamma曲線等功能。
4)JPEG(Joint Photographic Experts Group)
JPEG采用有損壓縮,通過調整壓縮比例可以控制圖像文件大小。
5)Exif(Exchangeable Image File)
Exif 專為數碼相機使用,使用 JPEG 壓縮,同時增加了一些相機拍照時相關信息。
6)TIFF(Tag Image File Format)
TIFF是一個靈活可擴展的圖像文件格式,支持任意位深,可采用多種壓縮算法。
3 使用Bitmap類
1)創建Bitmap對象
可以通過構造函數創建一個Bitmap對象,也可以使用對應的靜態創建一個Bitmap對象,當使用靜態函數創建對象時,在對象使用完成后需要手動刪除。
創建Bitmap對象的數據源可以為:文件,文件流,內存DIB,內存DDB等,這里只關心文件與內存DIB。
Bitmap(IN const WCHAR *filename, IN BOOL useEmbeddedColorManagement = FALSE);
static Bitmap* FromFile(IN const WCHAR *filename, IN BOOL useEmbeddedColorManagement = FALSE);
filename 為文件名,注意需要使用 unicode 編碼,參數 useEmbeddedColorManagement 為色彩校正相關內容,一般使用默認值即可。
Bitmap(IN const BITMAPINFO* gdiBitmapInfo, IN VOID* gdiBitmapData);
static Bitmap* FromBITMAPINFO(IN const BITMAPINFO* gdiBitmapInfo, IN VOID* gdiBitmapData);
gdiBitmapInfo 為DIB位圖結構體信息,gdiBitmapData 為DIB位圖數據信息。
2)訪問Bitmap對象數據
可以通過 LockBits 與 UnlockBits 函數訪問 Bitmap對象數據,首先調用 LockBits 獲得 BitmapData 結構體,然后通過結構體訪問圖像數據,完成后調用 UnlockBits。
BitmapData 結構體定義如下:
class BitmapData
{
public:
UINT Width;
UINT Height;
INT Stride;
PixelFormat PixelFormat;
VOID* Scan0;
UINT_PTR Reserved;
};
Width,Height 表示圖像寬度與高度,PixelFormat 為圖像格式定義,包括:
#define PixelFormat1bppIndexed (1 | ( 1 << 8) | PixelFormatIndexed | PixelFormatGDI)
#define PixelFormat4bppIndexed (2 | ( 4 << 8) | PixelFormatIndexed | PixelFormatGDI)
#define PixelFormat8bppIndexed (3 | ( 8 << 8) | PixelFormatIndexed | PixelFormatGDI)
#define PixelFormat16bppGrayScale (4 | (16 << 8) | PixelFormatExtended)
#define PixelFormat16bppRGB555 (5 | (16 << 8) | PixelFormatGDI)
#define PixelFormat16bppRGB565 (6 | (16 << 8) | PixelFormatGDI)
#define PixelFormat16bppARGB1555 (7 | (16 << 8) | PixelFormatAlpha | PixelFormatGDI)
#define PixelFormat24bppRGB (8 | (24 << 8) | PixelFormatGDI)
#define PixelFormat32bppRGB (9 | (32 << 8) | PixelFormatGDI)
#define PixelFormat32bppARGB (10 | (32 << 8) | PixelFormatAlpha | PixelFormatGDI | PixelFormatCanonical)
#define PixelFormat32bppPARGB (11 | (32 << 8) | PixelFormatAlpha | PixelFormatPAlpha | PixelFormatGDI)
#define PixelFormat48bppRGB (12 | (48 << 8) | PixelFormatExtended)
#define PixelFormat64bppARGB (13 | (64 << 8) | PixelFormatAlpha | PixelFormatCanonical | PixelFormatExtended)
#define PixelFormat64bppPARGB (14 | (64 << 8) | PixelFormatAlpha | PixelFormatPAlpha | PixelFormatExtended)
#define PixelFormat32bppCMYK (15 | (32 << 8))
#define PixelFormatMax 16
根據格式可以確定每個像素所占用的位數,如:
PixelFormat1bppIndexed,PixelFormat4bppIndexed,PixelFormat8bppIndexed 每個像素分別占用1位,4位,8位內存,需要使用查找表以映射到真實顏色;
PixelFormat16bppGrayScale 是每個像素占用16位(2字節)的黑白圖像;
PixelFormat16bppRGB555 是每個像素占用16位的彩色圖像,其中,每個RGB分量占用5位,剩下1位內存未使用;
PixelFormat16bppRGB565 是每個像素占用16位的彩色圖像,其中,RB分量占用5位,G分量占用6位;
PixelFormat24bppRGB 是每個像素占用24位的真彩色圖像,其中,每個RGB分量占用8位,這是目前很常用的圖像格式;
PixelFormat32bppARGB 是每個像素占用32位的真彩色圖像,增加了 alpha 通道描述透明色;
PixelFormat32bppPARGB 是每個像素占用32位的真彩色圖像,P表示RBG分量被預乘以alpha透明分量,這用在半透明圖像融合中;
.......
Stride 表示圖像每行字節數,Scan0 位圖像數據指針,這兩個參數需要特別注意:
當 Stride > 0 時,Scan0 指向圖像內存區域的起始位置;當 Stride < 0 時,Scan0 指向圖像最后一行所在的內存地址;
之所以如此,我以為是坐標原點差異所引起(僅是猜測)。
gdi+位圖使用左上角點作為坐標原點,設備無關位圖(DIB)使用左下角作為坐標原點;
當從DIB構造gdi+位圖時,Scan0 指向DIB圖像最后一行,Stride 小於 0;
當從文件構造gdi+位圖時,gdi+並不知道該文件所存儲的圖像是有什么數據形成,所以默認以左上角為坐標原點,Scan0 指向數據起始點,Stride 大於0;
以下示例代碼從文件中構造gdi+位圖,拷貝gdi+數據,再使用拷貝數據從內存DIB中構造位圖:
void GdiPlusBitmapTest() { /* 假定3*3圖像如下: i_00 i_01 i_02 i_10 i_11 i_12 i_20 i_21 i_22 當從文件中構造Bitmap時,bit_data.Scan0指向i_00 當從內存中構造Bitmap時,bit_data.Scan0指向i_20 */ // 從文件中構造Bitmap, bit_data.Stride 為正數,bit_data.Scan0指向圖像數據第一行 Bitmap* bm_f = Bitmap::FromFile(L"1.bmp"); Gdiplus::BitmapData bit_data; Gdiplus::Rect rc(0, 0, bm_f->GetWidth(), bm_f->GetHeight()); bm_f->LockBits(&rc, ImageLockModeRead, bm_f->GetPixelFormat(), &bit_data); BYTE* data = (BYTE*)malloc(bit_data.Height * bit_data.Stride); memcpy(data, bit_data.Scan0, bit_data.Height * bit_data.Stride); // 拷貝圖像,用於內存構造GDI+位圖 bm_f->UnlockBits(&bit_data); BYTE buffer[1024]; memset(buffer, 0, 1024); LPBITMAPINFOHEADER lpBmpInfoHead = (LPBITMAPINFOHEADER)buffer; lpBmpInfoHead->biSize = sizeof(BITMAPINFOHEADER); lpBmpInfoHead->biBitCount = bit_data.Stride / bit_data.Width * 8; lpBmpInfoHead->biWidth = bit_data.Width; lpBmpInfoHead->biHeight = bit_data.Height; lpBmpInfoHead->biPlanes = 1; lpBmpInfoHead->biCompression = BI_RGB; // 從內存構造Bitmap,bit_data.Stride 為負數,bit_data.Scan0指向圖像數據最后一行 Bitmap* bm_mem = Bitmap::FromBITMAPINFO((LPBITMAPINFO)lpBmpInfoHead, data); Gdiplus::Rect rc_u(0, 0, bm_mem->GetWidth(), bm_mem->GetHeight()); bm_mem->LockBits(&rc_u, ImageLockModeRead, bm_mem->GetPixelFormat(), &bit_data); // !!!拷貝數據越界!!! //memcpy(data, bit_data.Scan0, bit_data.Height * (-bit_data.Stride)); // 需要將 Scan0 移動到圖像第一行位置再拷貝 memcpy(data, (BYTE*)bit_data.Scan0 + bit_data.Stride * (bit_data.Height - 1), bit_data.Height * (-bit_data.Stride)); bm_mem->UnlockBits(&bit_data); if (!data) free(data); if(bm_f) delete bm_f; }
