前几天写程序需要将DIB(设备无关位图)格式的图片在WPF 程序中显示出来,在网上查了一些资料和代码,记录下来以便以后参考。
设备有关位图主要是显卡在显存中保存的图片格式,显卡就是从显存里面将要显示的图片数据直接打印到显示器上,这样不仅节省了系统内存的空间,还大大节省了从内存把图片数据搬到显存的时间—因为显卡芯片可以实现一些指令,直接操作显存中的图片。所以设备有关位图的格式不是固定的,是由显卡制造商自由决定的,这也就意味着我们没有办法直接操作DDB格式图片的每一个像素。
如果保存在硬盘的图片格式是以DDB格式保存的话,那就意味着你使用一个显卡保存的图片不能在使用不同型号的显卡的机器上打开—因为不同显卡制造商使用的DDB的格式不一样。为了解决这个问题,在很久以前,微软的GDI系统就提供了DIB(设备无关位图),相当于一种中间格式,在显示DIB图片的之前,GDI先将DIB位图转换成显卡能够理解的DDB格式—当然这一系列的操作都是由显卡的指令完成的。
在WPF和Winform的世界里,大家针对图片的编程相对来说都比较舒服,因为.NET Framework提供了简化的API帮你隐藏了这些细节,所有你需要做的就是:
WPF:
var image = new Image();
image.Source = new BitmapImage(new Uri("some image.jpg"));
Winform:
var image = System.Drawing.Image.FromFile("some image.jpg");
恩,看起来很美,但问题是,因为微软的函数库实在是太多了,强大如.NET这种类库也有考虑不周的时候,WPF并没有公开的API直接从DIB格式的图片中生成BitmapImage实例。为什么有的时候我们需要直接操作DIB格式的图片呢?例如你需要从剪贴板里面拿一个图片出来,而把图片放置到剪贴板的程序有可能并不是.NET程序,而是古老的OLE程序或者Win32程序,这些老古董经常喜欢在剪贴板里面放两个格式的图片,一个是DIB格式的,另外一个可能是支持IDataObject的接口—这个接口怎么实现又是仁者见仁,智者见智的事情。这个时候,你就发现你必须要处理DIB格式的图片;另外一种情况就是拖拉操作,因为拖拉操作中数据源和接受数据的程序有可能不是同一个程序,例如,你从一个OLE程序里面拖拉一个图片到WPF程序里。
根据MSDN,DIB图片的格式如下图所示:
下面的代码是我从网上搜到的关于如何从DIB格式生成BitmapImage实例的代码:
private System.Drawing.Bitmap CreateBitmapFromDib(Stream dib) { BinaryReader reader = new BinaryReader(dib);
int headerSize = reader.ReadInt32(); int pixelSize = (int)dib.Length - headerSize; int fileSize = 14 + headerSize + pixelSize;
MemoryStream bmp = new MemoryStream(fileSize); BinaryWriter writer = new BinaryWriter(bmp);
// 1. 把位图的一些元数据写进去,下面这几次Write相当于填写Win32的 // BITMAPFILEHEADER结构 writer.Write((byte)'B'); writer.Write((byte)'M'); writer.Write(fileSize); writer.Write((int)0); writer.Write(14 + headerSize);
// 2. 把DIB位图中的像素矩阵拷贝出来到我们指定的MemoryStream里。 // 因为我们要从MemoryStream里面生成System.Drawing.Bitmap对象 // 然后再颇为曲折地从Bitmap对象生成WPF的BitmapImage对象 dib.Position = 0; byte[] data = new byte[(int)dib.Length]; dib.Read(data, 0, (int)dib.Length); writer.Write(data, 0, (int)data.Length);
// 3. 生成Bitmap对象—这个是Winform里面的Bitmap对象 bmp.Position = 0; return new System.Drawing.Bitmap(bmp); } |
上面那一段代码也相当于下面的C++代码,从MFC的实例代码里面找到的:
HDIB WINAPI ReadDIBFile(CFile& file) { BITMAPFILEHEADER bmfHeader; UINT nBitsSize; HDIB hDIB; LPSTR pDIB;
/* * get length of DIB in bytes for use when reading */
nBitsSize = (UINT)file.GetLength();
/* * Go read the DIB file header and check if it's valid. */ if (file.Read((LPSTR)&bmfHeader, sizeof(bmfHeader)) != sizeof(bmfHeader)) return NULL;
if (bmfHeader.bfType != DIB_HEADER_MARKER) return NULL;
/* * Allocate memory for DIB */ hDIB = (HDIB) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, (DWORD)nBitsSize); if (hDIB == 0) { return NULL; } pDIB = (LPSTR) ::GlobalLock((HGLOBAL) hDIB);
/* * Go read the bits. */ if (file.Read(pDIB, nBitsSize - sizeof(BITMAPFILEHEADER)) != nBitsSize - sizeof(BITMAPFILEHEADER) ) { ::GlobalUnlock((HGLOBAL) hDIB); ::GlobalFree((HGLOBAL) hDIB); return NULL; } ::GlobalUnlock((HGLOBAL) hDIB); return hDIB; } |
得到System.Drawing.Bitmap实例以后,就可以通过下面的函数来创建BitmapImage对象了:
private BitmapSource BitmapToImageSource(System.Drawing.Bitmap bitmap) { BitmapSource destination; IntPtr hBitmap = bitmap.GetHbitmap(); BitmapSizeOptions sizeOptions = BitmapSizeOptions.FromEmptyOptions(); destination = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, sizeOptions); destination.Freeze(); return destination; } |