2. GDI+图像的加载和保存

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

猜你喜欢

转载自blog.csdn.net/wenzhou1219/article/details/78266830
今日推荐