GDI只提供了加载和保存BMP图像的方法,对于现代化UI显示显然是不够的。GDI+提供了常用图像格式(BMP/TIFF/JPG/PNG/GIF等)的加载和保存,基于编码器/解码器的设计有利于进一步扩展,但是也存在一些坑,本文就这些加以详细说明,并给出一个封装类,可用于实际生产环境使用。
1.编码器/解码器设计
现代图像/音视频加载保存,都会把编码和解码独立出来,这样保证加载和保存的接口一致,只需要选用不同的编码器和解码器,即可完成对应功能,这样保证了接口一致性同时也增加了扩展性。GDI+也是这样做的。
编码查看当前支持的编码器和解码器,如下:
/************输出编解码信息*********************/
static void DumpAllEncoders()
{
UINT nEncoderNum = 0; //编码器总个数
UINT nEncoderInfoSize = 0; //编码器总大小
GetImageEncodersSize(&nEncoderNum, &nEncoderInfoSize);
if (nEncoderNum==0 || nEncoderInfoSize==0)
{
return;
}
ImageCodecInfo* pEncoderInfo = NULL;
pEncoderInfo = reinterpret_cast<ImageCodecInfo*>(new(std::nothrow) BYTE[nEncoderInfoSize]);
if (!pEncoderInfo)
{
return;
}
GetImageEncoders(nEncoderNum, nEncoderInfoSize, pEncoderInfo);
for (size_t i=0; i<nEncoderNum; i++)
{
MyTrace(L"【编码器】 Name:%s Ext:%s",
pEncoderInfo[i].CodecName,
pEncoderInfo[i].FilenameExtension);
}
delete [] pEncoderInfo;
}
static void DumpAllDecoders()
{
UINT nDecoderNum = 0; //解码器总个数
UINT nDecoderInfoSize = 0; //解码器总大小
GetImageDecodersSize(&nDecoderNum, &nDecoderInfoSize);
if (nDecoderNum==0 || nDecoderInfoSize==0)
{
return;
}
ImageCodecInfo* pDecoderInfo = NULL;
pDecoderInfo = reinterpret_cast<ImageCodecInfo*>(new(std::nothrow) BYTE[nDecoderInfoSize]);
if (!pDecoderInfo)
{
return;
}
GetImageDecoders(nDecoderNum, nDecoderInfoSize, pDecoderInfo);
for (size_t i=0; i<nDecoderNum; i++)
{
MyTrace(L"【解码器】 Name:%s Ext:%s",
pDecoderInfo[i].CodecName,
pDecoderInfo[i].FilenameExtension);
}
delete [] pDecoderInfo;
}
输出结果可以用debugview查看,在我的电脑上显示如下:
可以看到,基本上常用的都支持了。
2.图像保存
在图像加载时,不需要指定解码器,程序会自动根据对应的文件头来选用对应的解码器解码图像,但是保存图形时,因为我们可以保存各种不同格式的图像,所以必须指定对应的编码器。
GDI+中的解码器使用GUID标识,保存图像时,传入保存名称和GUID即可。但是GUID太难记,一般我们指定图像保存时的MIMIE类型,如下完成MIME到GUDI的转换:
/************保存*********************/
static BOOL GetEncoderClsidFromMime(LPWSTR pszFormat, CLSID *pClsid)
{
if (!pszFormat || !pClsid)
{
return FALSE;
}
UINT nEncoderNum = 0; //编码器总个数
UINT nEncoderInfoSize = 0; //编码器总大小
GetImageEncodersSize(&nEncoderNum, &nEncoderInfoSize);
if (nEncoderNum==0 || nEncoderInfoSize==0)
{
return FALSE;
}
ImageCodecInfo* pEncoderInfo = NULL;
pEncoderInfo = reinterpret_cast<ImageCodecInfo*>(new(std::nothrow) BYTE[nEncoderInfoSize]);
if (!pEncoderInfo)
{
return FALSE;
}
BOOL bFind = FALSE;
GetImageEncoders(nEncoderNum, nEncoderInfoSize, pEncoderInfo);
for (size_t i=0; i<nEncoderNum; i++)
{
if (_wcsicmp(pEncoderInfo[i].MimeType, pszFormat) == 0)
{
*pClsid = pEncoderInfo[i].Clsid;
bFind = TRUE;
break;
}
}
delete [] pEncoderInfo;
return bFind;
}
这样保存图像时,就可以如下实现,传入MIME即可指明保存图像格式:
static BOOL SaveImage(LPWSTR pszFile, Image *pImage, EncoderParameters *pParams=NULL, LPWSTR pszFormat=NULL)
{
if (!pszFile || !pImage)
{
return FALSE;
}
if (!pszFormat)
{
pImage->Save(pszFile, NULL, pParams);
}
else
{
CLSID clsid = {0};
if (!GetEncoderClsidFromMime(pszFormat, &clsid))
{
return FALSE;
}
pImage->Save(pszFile, &clsid, pParams);
}
return TRUE;
}
可以看到,这里我们还可以指定保存图像时的参数,
不同格式支持保存的图像参数也不一样,比如JPG支持保存时指明图像压缩质量,这里也实现了一个函数,用于输出指定MIME格式支持保存时设置的参数,具体的可以参看MSDN,
static void DumpAllParams(Bitmap* pBitmap, LPWSTR pszFormat)
{
EncoderParameters *pParams = NULL;
do
{
if (!pBitmap)
{
break;
}
CLSID clsid = {0};
if (!GetEncoderClsidFromMime(pszFormat, &clsid))
{
break;
}
UINT nSize = 0;
nSize = pBitmap->GetEncoderParameterListSize(&clsid);
if (!nSize)
{
break;
}
pParams = reinterpret_cast<EncoderParameters*>(new(std::nothrow) BYTE[nSize]);
if (!pParams)
{
break;
}
if (Gdiplus::Ok != pBitmap->GetEncoderParameterList(&clsid, nSize, pParams))
{
break;
}
for (size_t i=0; i<pParams->Count; i++)
{
MyTrace(L"【params】 %s", ParamsGuidToString(pParams->Parameter[i].Guid).GetBuffer(0));
}
} while (false);
if (pParams)
{
delete [] pParams;
}
}
比如这里,可如下查看MIME=“image/jpeg”支持的参数:
static void DumpAllParams(Bitmap* pBitmap, LPWSTR pszFormat)
{
EncoderParameters *pParams = NULL;
do
{
if (!pBitmap)
{
break;
}
CLSID clsid = {0};
if (!GetEncoderClsidFromMime(pszFormat, &clsid))
{
break;
}
UINT nSize = 0;
nSize = pBitmap->GetEncoderParameterListSize(&clsid);
if (!nSize)
{
break;
}
pParams = reinterpret_cast<EncoderParameters*>(new(std::nothrow) BYTE[nSize]);
if (!pParams)
{
break;
}
if (Gdiplus::Ok != pBitmap->GetEncoderParameterList(&clsid, nSize, pParams))
{
break;
}
for (size_t i=0; i<pParams->Count; i++)
{
MyTrace(L"【params】 %s", ParamsGuidToString(pParams->Parameter[i].Guid).GetBuffer(0));
}
} while (false);
if (pParams)
{
delete [] pParams;
}
}
结果如下:
这里EncoderQuality即为指明图像保存质量,所以可基于SaveImage如下编写jpg保存函数:
static BOOL SaveToJpeg(LPWSTR pszFile, Image *pImage, ULONG nQuality=100)
{
EncoderParameters params;
params.Count =1;
params.Parameter->Guid = EncoderQuality;
params.Parameter->Type = EncoderParameterValueTypeLong;
params.Parameter->Value = &nQuality;
params.Parameter->NumberOfValues = 1;
return SaveImage(pszFile, pImage, ¶ms, L"image/jpeg");
}
3.图像加载
GDI+支持从文件或从流中加载图像,下面分开说:
GDI+的文件加载很方便,文件路径当做Image/Bitmap的构造函数参数即可,或者FromFile/FromStream,但是原生的GDI+存在一个问题——会锁住文件,很容易验证的一个方便不就是,加载图像的对象还存在时,无法删除图像文件,理论上只要文件加载到内存中,两者就没什么联系了,所以这里是有问题的。如果当前程序不能忽略这个问题,必须自己实现从文件中加载。
所以最终,所有的实现都归为从流中加载,因为要求的流IStream是COM中的概念,所以一般的参数我们使用通用GlobalHeap,如下:
static BOOL LoadFromGlobalHeap(Image** ppImg, HGLOBAL hGlobal, DWORD dwSize)
{
BOOL bRet = FALSE;
IStream *pStream = NULL;
do
{
if (!ppImg)
{
break;
}
if ( CreateStreamOnHGlobal( hGlobal, TRUE, &pStream ) != S_OK )
{
break;
}
bRet = LoadFromStream(ppImg, pStream);
} while (false);
if (pStream)
{
pStream->Release();
}
return bRet;
}
在此,基础上实现 基于内存的加载,这样可以从一段内存中加载实际图像,如下:
static BOOL LoadFromMem(Image** ppImg, LPBYTE pData, DWORD dwSize)
{
BOOL bRet = FALSE;
HGLOBAL hGlobal = NULL;
do
{
if (!ppImg || !pData)
{
break;
}
hGlobal = GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, dwSize);
if (!hGlobal)
{
break;
}
void* pDstData = GlobalLock(hGlobal);
if (!pDstData)
{
break;
}
memcpy_s(pDstData, dwSize, pData, dwSize);
GlobalUnlock(hGlobal);
bRet = LoadFromGlobalHeap(ppImg, hGlobal, dwSize);
} while (false);
if (hGlobal)
{
GlobalFree(hGlobal);
}
return bRet;
}
这样再
从文件中加载图像就很简单了,只需要读入文件数据再加载即可,如下:
static BOOL LoadFromFile(Image** ppImg, LPWSTR pszFile)
{
BOOL bRet = FALSE;
HANDLE hFile = NULL;
HGLOBAL hGlobal = NULL;
do
{
if (!ppImg)
{
break;
}
hFile = ::CreateFile( pszFile,
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if (INVALID_HANDLE_VALUE==hFile)
{
break;
}
LARGE_INTEGER lSize;
lSize.QuadPart = 0;
if (!GetFileSizeEx(hFile, &lSize) || 0==lSize.QuadPart || lSize.HighPart!=0)//只处理小于4G文件
{
break;
}
hGlobal = GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, lSize.LowPart);
if (!hGlobal)
{
break;
}
void* pDstData = GlobalLock(hGlobal);
if (!pDstData)
{
break;
}
DWORD dwReadBytes = 0;
if (!::ReadFile( hFile, pDstData, lSize.LowPart, &dwReadBytes, NULL ) || dwReadBytes!=lSize.LowPart)
{
GlobalUnlock(hGlobal);
break;
}
GlobalUnlock(hGlobal);
bRet = LoadFromGlobalHeap(ppImg, hGlobal, lSize.LowPart);
} while (false);
if (hGlobal)
{
GlobalFree(hGlobal);
}
if (INVALID_HANDLE_VALUE!=hFile)
{
CloseHandle(hFile);
}
return bRet;
}
程序要用的文件和exe分离,通常易于实现动态换肤等功能,但是也容易存在资源和PE文件不匹配的问题,所以这里也实现基于资源ID的加载,思路都是一样,找到对应内存块,直接加载即可:
//修改查找目标字符串"PNG"来支持不同的文件格式
static BOOL LoadFromResource(Image** ppImg, HINSTANCE hInstance, UINT nResourceID)
{
BOOL bRet = FALSE;
do
{
HRSRC hResource = ::FindResource(hInstance, MAKEINTRESOURCE(nResourceID), _T("PNG"));
if (!hResource)
{
break;
}
DWORD dwSize = ::SizeofResource(hInstance, hResource);
if (0 == dwSize)
{
break;
}
//返回HGLOBAL类型只是为兼容,不能在此调用GlobalLock 或 GlobalFree
//不用释放,依赖进程退出释放,参考MSDN
HGLOBAL hGlobal = ::LoadResource(hInstance, hResource);
if(!hGlobal)
{
break;
}
const void* pResourceData = ::LockResource(hGlobal);
if (!pResourceData)
{
break;
}
bRet = LoadFromMem(ppImg, (LPBYTE)pResourceData, dwSize);
}
while(false);
return bRet;
}
如上所有相关代码已封装成一个帮助类CGdiplusCodecHelper,类实现和测试代码 下载链接
部分代码参考书籍《精通GDI+》,很多示例,可以当做手册查询
原创,转载请注明来自http://blog.csdn.net/wenzhou1219