见招拆招《Windows程序设计》(十三)
相关的例子:下载>>> 作者:Zoologist 于2009-3-17上传 

与设备无关的位图

 

在上一期我们了解到Windows GDI位图对象(也称为与设备相关的位图,或DDB)有许多程序设计用途。但是我并没有展示把这些位图储存到磁盘文件或把它们加载内存的方法。这是以前在Windows中使用的方法,现在根本不用了。因为位图的位格式相当依赖于设备,所以DDB不适用于图像交换。DDB内没有色彩对照表来指定位图的位与色彩之间的联系。DDB只有在Windows开机到关机的生命期内被建立和清除时才有意义。

在Windows 3.0中发表了与设备无关的位图(DIB),提供了适用于交换的图像文件格式。正如您所知的,像.GIF或.JPEG之类的其它图像文件格式在Internet上比DIB文件更常见。这主要是因为.GIF和.JPEG格式进行了压缩,明显地减少了下载的时间。尽管有一个用于DIB的压缩方案,但极少使用。DIB内的位图几乎都没有被压缩。如果您想在程序中操作位图,这实际上是一个优点。DIB不像.GIF和.JPEG文件,Windows API直接支持DIB。如果在内存中有DIB,您就可以提供指向该DIB的指标作为某些函数的参数,来显示DIB或把DIB转化为DDB。

DIB 文件格式

有意思的是,DIB格式并不是源自于Windows。它首先定义在OS/2的1.1版中,该操作系统最初由IBM和Microsoft在八十年代中期开始开发。OS/2 1.1在1988年发布,并且是第一个包含了类似Windows的图形使用者接口的OS/2版本,该图形使用者接口被称之为「Presentation Manager(PM)」。「Presentation Manager」包含了定义位图格式的「图形程序接口」(GPI)。

额外提一句:前几天我还遇到了一个OS2的问题,安装了一个OS/2 wrap,如果以当时的眼光看,这样的设计是非常优秀的。不过因为商业上的原因(应该是被MS忽悠了)和一些其他的原因(比如:IBM的软件用起来总感觉是把客户当作开发人员,操作起来非常复杂),OS/2最终没能发展下去。

然后在Windows 3.0中(发布于1990)使用了OS/2位图格式,这时称之为DIB。Windows 3.0也包含了原始DIB格式的变体,并在Windows下成为标准。在Windows 95(以及Windows NT 4.0)和Windows 98(以及Windows NT 5.0)下也定义了一些其它的增强能力,我会在本章讨论它们。

DIB首先作为一种文件格式,它的扩展名为.BMP,在极少情况下为.DIB。Windows应用程序使用的位图图像被当做DIB文件建立,并作为只读资源储存在程序的可执行文件中。图标和鼠标光标也是形式稍有不同的DIB文件。

程序能将DIB文件减去前14个字节加载连续的内存块中。这时就可以称它为「packed DIB(packed-DIB)格式的位图」。在Windows下执行的应用程序能使用packed DIB格式,通过Windows剪贴簿来交换图像或建立画刷。程序也可以完全存取DIB的内容并以任意方式修改DIB。

程序也能在内存中建立自己的DIB然后把它们存入文件。程序使用GDI函数呼叫就能「绘制」这些DIB内的图像,也能在程序中利用别的内存DIB直接设定和操作图素位。

在内存中加载了DIB后,程序也能通过几个Windows API函数呼叫来使用DIB数据。与DIB相关的API呼叫是很少的,并且主要与显示器或打印机页面上显示DIB相关,还与转换GDI位图对象有关。

除了这些内容以外,还有许多应用程序需要完成的DIB任务,而这些任务Windows操作系统并不支持。例如,程序可能存取了24位DIB并且想把它转化为带有最佳化的256色调色盘的8位DIB,而Windows不会为您执行这些操作。但是在本章和下一章将向您显示Windows API之外的操作DIB的方式。

OS/2样式的DIB

先不要陷入太多的细节,让我们看一下与首先在OS/2 1.1中出现的位图格式兼容的Windows DIB格式。

DIB文件有四个主要部分:

您可以把前两部分看成是C的数据结构,把第三部分看成是数据结构的数组。在Windows表头文件WINGDI.H中说明了这些结构(对于汇编语言用户,可以在Windows.inc中找到对应定义)。在内存中的packed DIB格式内有三个部分:

除了没有文件表头外,其它部分与储存在文件内的DIB相同。

DIB文件(不是内存中的packed DIB)以定义为如下结构的14个字节的文件表头开始:

 

typedef struct tagBITMAPFILEHEADER  // bmfh
        
{
        
           WORD          bfType ;        // signature word "BM" or 0x4D42
        
           DWORD         bfSize ;        // entire size of file
        
           WORD          bfReserved1 ;   // must be zero
        
           WORD          bfReserved2 ;   // must be zero
        
           DWORD        bfOffsetBits ;  // offset in file of DIB pixel bits
        
}
        
BITMAPFILEHEADER, * PBITMAPFILEHEADER ;
        

在WINGDI.H内定义的结构可能与这不完全相同,但在功能上是相同的。第一个注释(就是文字「bmfh」)指出了给这种数据型态的数据变量命名时推荐的缩写。如果在我的程序内看到了名为pbmfh的变量,这可能是一个指向BITMAPFILEHEADER型态结构的指针或指向PBITMAPFILEHEADER型态变量的指针。

结构的长度为14字节,它以两个字母「BM」开头以指明是位图文件。这是一个WORD值0x4D42。紧跟在「BM」后的DWORD以字节为单位指出了包括文件表头在内的文件大小。下两个WORD字段设定为0。(在与DIB文件格式相似的鼠标光标文件内,这两个字段指出光标的「热点(hot spot)」)。结构还包含一个DWORD字段,它指出了文件中图素位开始位置的字节偏移量。此数值来自DIB信息表头中的信息,为了使用的方便提供在这里。

在OS/2样式的DIB内,BITMAPFILEHEADER结构后紧跟了BITMAPCOREHEADER结构,它提供了关于DIB图像的基本信息。紧缩的DIB(Packed DIB)开始于BITMAPCOREHEADER:

typedef struct tagBITMAPCOREHEADER  // bmch
        
{
        
           DWORD         bcSize ;              // size of the structure = 12
        
           WORD          bcWidth ;             // width of image in pixels
        
           WORD          bcHeight ;           // height of image in pixels
        
           WORD          bcPlanes ;            // = 1
        
           WORD          bcBitCount ;          // bits per pixel (1, 4, 8, or 24)
        
}
        
BITMAPCOREHEADER, * PBITMAPCOREHEADER ;
        

「core(核心)」用在这里看起来有点奇特,它是指这种格式是其它由它所衍生的位图格式的基础。

BITMAPCOREHEADER结构中的bcSize字段指出了数据结构的大小,在这种情况下是12字节。

bcWidth和bcHeight字段包含了以图素为单位的位图大小。尽管这些字段使用WORD意味着一个DIB可能为65,535图素高和宽,但是我们几乎不会用到那么大的单位。

bcPlanes字段的值始终是1。这个字段是我们在上一章中遇到的早期Windows GDI位图对象的残留物。

bcBitCount字段指出了每图素的位数。对于OS/2样式的DIB,这可能是1、4、8或24。DIB图像中的颜色数等于2 ^ bmch.bcBitCount,或用C的语法表示为:

1 << bmch.bcBitCount
        
c语言中 A << B,意思是“将A左移B位”,反之是“右移”。上面的表达式用汇编的语言描述就是:

mov eax,1

mov ecx,bmch.bcBitCount

shl eax,ecx

这样,bcBitCount字段等于:

当我提到「8位DIB」时,就是说每图素占8位的DIB。

对于前三种情况(也就是位数为1、4和8时),BITMAPCOREHEADER后紧跟色彩对照表,24位DIB没有色彩对照表。色彩对照表是一个3字节RGBTRIPLE结构的数组,数组中的每个元素代表图像中的每种颜色:

typedef struct tagRGBTRIPLE  // rgbt
        
{
        
           BYTE rgbtBlue ;       // blue level
        
           BYTE rgbtGreen ;      // green level
        
           BYTE rgbtRed ;        // red level
        
}
        
RGBTRIPLE ;
        

这样排列色彩对照表以便DIB中最重要的颜色首先显示,我们将在下一期中说明原因。

WINGDI.H表头文件也定义了下面的结构:

typedef struct tagBITMAPCOREINFO  // bmci
        
{
        
           BITMAPCOREHEADER      bmciHeader ;                  // core-header structure
        
           RGBTRIPLE             bmciColors[1] ;               // color table array
        
}
        
BITMAPCOREINFO, * PBITMAPCOREINFO ;
        

这个结构把信息表头与色彩对照表结合起来。虽然在这个结构中RGBTRIPLE结构的数量等于1,但在DIB文件内您绝对不会发现只有一个RGBTRIPLE。根据每个图素的位数,色彩对照表的大小始终是2、16或256个RGBTRIPLE结构。如果需要为8位DIB配置PBITMAPCOREINFO结构,您可以这样做:

pbmci = malloc (sizeof (BITMAPCOREINFO) + 255 * sizeof (RGBTRIPLE)) ;
        

然后可以这样存取RGBTRIPLE结构:

pbmci->bmciColors[i]
        

因为RGBTRIPLE结构的长度是3字节,许多RGBTRIPLE结构可能在DIB中以奇数地址开始。然而,因为在DIB文件内始终有偶数个的RGBTRIPLE结构,所以紧跟在色彩对照表数组后的数据块总是以WORD地址边界开始。

紧跟在色彩对照表(24位DIB中是信息表头)后的数据是图素位本身。

由下而上

像大多数位图格式一样,DIB中的图素位是以水平行组织的,用显示器硬件的术语称作「扫描线」。行数等于BITMAPCOREHEADER结构的bcHeight字段。然而,与大多数位图格式不同的是,DIB从图像的底行开始,往上表示图像。

在此应定义一些术语,当我们说「顶行」和「底行」时,指的是当其正确显示在显示器或打印机的页面上时出现在虚拟图像的顶部和底部。就好像肖像的顶行是头发,底行是下巴,在DIB文件中的「第一行」指的是DIB文件的色彩对照表后的图素行,「最后行」指的是文件最末端的图素行。

因此,在DIB中,图像的底行是文件的第一行,图像的顶行是文件的最后一行。这称之为由下而上的组织。因为这种组织和直觉相反,您可能会问:为什么要这么做?

好,现在我们回到OS/2的Presentation Manager。IBM的人认为PM内的坐标系统-包括窗口、图形和位图-应该是一致的。这引起了争论:大多数人,包括在全画面文字方式下编程和窗口环境下工作的程序写作者认为应使用垂直坐标在屏幕上向下增加的坐标。然而,计算机图形程序写作者认为应使用解析几何的数学方法进行视频显示,这是一个垂直坐标在空间中向上增加的直角(或笛卡尔)坐标系。

简而言之,数学方法赢了。PM内的所有事物都以左下角为原点(包括窗口坐标),因此DIB也就有了那种方式。

DIB图素位

DIB文件的最后部分(在大多数情况下是DIB文件的主体)由实际的DIB的图素字节成。图素位是由从图像的底行开始并沿着图像向上增长的水平行组织的。

DIB中的行数等于BITMAPCOREHEADER结构的bcHeight字段。每一行的图素数等于该结构的bcWidth字段。每一行从最左边的图素开始,直到图像的右边。每个图素的位数可以从bcBitCount字段取得,为1、4、8或24。

以字节为单位的每行长度始终是4的倍数。行的长度可以计算为:

RowLength = 4 * ((bmch.bcWidth * bmch.bcBitCount + 31) / 32) ;
        

或者在C内用更有效的方法:

RowLength = ((bmch.bcWidth * bmch.bcBitCount + 31) & ~31) >> 3 ;
        

如果需要,可通过在右边补充行(通常是用零)来完成长度。图素数据的总字节数等于RowLength和bmch.bcHeight的乘积。

要了解图素编码的方式,让我们分别考虑四种情况。在下面的图表中,每个字节的位显示在框内并且编了号,7表示最高位,0表示最低位。图素也从行的最左端从0开始编号。

对于每图素1位的DIB,每字节对应为8图素。最左边的图素是第一个字节的最高位:


 

每个图素可以是0或1。0表示该图素的颜色由色彩对照表中第一个RGBTRIPLE项目给出。1表示图素的颜色由色彩对照表的第二个项目给出。

对于每图素4位的DIB,每个字节对应两个图素。最左边的图素是第一个字节的高4位,以此类推:  

每图素4位的值的范围从0到15。此值是指向色彩对照表中16个项目的索引。

对于每图素8位的DIB,每个字节为1个图素:


 

字节的值从0到255。同样,这也是指向色彩对照表中256个项目的索引。

对于每图素24位的DIB,每个图素需要3个字节来代表红、绿和蓝的颜色值。图素位的每一行,基本上就是RGBTRIPLE结构的数组,可能需要在每行的末端补0以便该行为4字节的倍数:


 

每图素24位的DIB没有色彩对照表。

扩展的Windows DIB

现在我们掌握了Windows 3.0中介绍的与OS/2兼容的DIB,同时也看一看Windows中DIB的扩展版本。

这种DIB形式跟前面的格式一样,以BITMAPFILEHEADER结构开始,但是接着是BITMAPINFOHEADER结构,而不是BITMAPCOREHEADER结构:

typedef struct tagBITMAPINFOHEADER  // bmih
        
{
        
           DWORD biSize ;              // size of the structure = 40
        
          LONG  biWidth ;             // width of the image in pixels
        
           LONG  biHeight ;            // height of the image in pixels
        
           WORD  biPlanes ;            // = 1
        
           WORD  biBitCount ;          // bits per pixel (1, 4, 8, 16, 24, or 32)
        
           DWORD biCompression ;       // compression code
        
           DWORD biSizeImage ;         // number of bytes in image
        
           LONG  biXPelsPerMeter ;     // horizontal resolution
        
           LONG  biYPelsPerMeter ;     // vertical resolution
        
           DWORD biClrUsed ;           // number of colors used
        
           DWORD biClrImportant ;      // number of important colors
        
}
        
BITMAPINFOHEADER, * PBITMAPINFOHEADER ;
        

您可以通过检查结构的第一字段区分与OS/2兼容的DIB和Windows DIB,前者为12,后者为40。

您将注意到,在这个结构内有六个附加的字段,但是BITMAPINFOHEADER不是简单地由BITMAPCOREHEADER加上一些新字段而成。仔细看一下:在BITMAPCOREHEADER结构中,bcWidth和bcHeight字段是16位WORD值;而在BITMAPINFOHEADER结构中它们是32位LONG值。这是一个令人讨厌的小变化,当心它会给您带来麻烦。

另一个变化是:对于使用BITMAPINFOHEADER结构的1位、4位和8位DIB,色彩对照表不是RGBTRIPLE结构的数组。相反,BITMAPINFOHEADER结构紧跟着一个RGBQUAD结构的数组:

typedef struct tagRGBQUAD  // rgb
        
{
        
           BYTE rgbBlue ;     // blue level
        
           BYTE rgbGreen ;    // green level
        
           BYTE rgbRed ;      // red level
        
           BYTE rgbReserved ; // = 0
        
}
        
RGBQUAD ;
        

除了包括总是设定为0的第四个字段外,与RGBTRIPLE结构相同。 WINGDI.H表头文件也定义了以下结构:

typedef struct tagBITMAPINFO              // bmi
        
{
        
           BITMAPINFOHEADER bmiHeader ;      // info-header structure
        
           RGBQUAD                      bmiColors[1] ;  // color table array
        
}
        
BITMAPINFO, * PBITMAPINFO ;
        

注意,如果BITMAPINFO结构以32位的地址边界开始,因为BITMAPINFOHEADER结构的长度是40字节,所以RGBQUAD数组内的每一个项目也以32位边界开始。这样就确保通过32位微处理器能更有效地对色彩对照表数据寻址。

尽管BITMAPINFOHEADER最初是在Windows 3.0中定义的,但是许多字段在Windows 95和Windows NT 4.0中又重新定义了,并且被带入Windows 98和Windows NT 5.0中。比如现在的文件中说:「如果biHeight是负数,则位图是由上而下的DIB,原点在左上角」。这很好,但是在1990年刚开始定义DIB格式时,如果有人做了这个决定,那会更好。我的建议是避免建立由上而下的DIB。有一些程序在编写时没有考虑这种新「特性」,在遇到负的biHeight字段时会当掉。还有如Microsoft Word 97带有的Microsoft Photo Editor在遇到由上而下的DIB时会报告「图像高度不合法」(虽然Word 97本身不会出错)。

biPlanes字段始终是1,但biBitCount字段现在可以是16或32以及1、4、8或24。这也是在Windows 95和Windows NT 4.0中的新特性。一会儿我将介绍这些附加格式工作的方式。

现在让我们先跳过biCompression和biSizeImage字段,一会儿再讨论它们。

biXPelsPerMeter和biYPelsPerMeter字段以每公尺多少图素这种笨拙的单位指出图像的实际尺寸。(「pel」--picture element(图像元素)--是IBM对图素的称呼。)Windows在内部不使用此类信息。然而,应用程序能够利用它以准确的大小显示DIB。如果DIB来源于没有方图素的设备,这些字段是很有用的。在大多数DIB内,这些字段设定为0,这表示没有建议的实际大小。每英寸72点的分辨率(有时用于视频显示器,尽管实际分辨率依赖于显示器的大小)大约相当于每公尺2835个图素,300 DPI的普通打印机的分辨率是每公尺11,811个图素。

biClrUsed是非常重要的字段,因为它影响色彩对照表中项目的数量。对于4位和8位DIB,它能分别指出色彩对照表中包含了小于16或256个项目。虽然并不常用,但这是一种缩小DIB大小的方法。例如,假设DIB图像仅包括64个灰阶,biClrUsed字段设定为64,并且色彩对照表为256个字节大小的色彩对照表包含了64个RGBQUAD结构。图素值的范围从0x00到0x3F。DIB仍然每图素需要1字节,但每个图素字节的高2位为零。如果biClrUsed字段设定为0,意味着色彩对照表包含了由biBitCount字段表示的全部项目数。

从Windows 95开始,biClrUsed字段对于16位、24位或32位DIB可以为非零。在这些情况下,Windows不使用色彩对照表解释图素位。相反地,它指出DIB中色彩对照表的大小,程序使用该信息来设定调色盘在256色视频显示器上显示DIB。您可能想起在OS/2兼容格式中,24位DIB没有色彩对照表。在Windows 3.0中的扩展格式中,也与这一样。而在Windows 95中,24位DIB有色彩对照表,biClrUsed字段指出了它的大小。

总结如下:

另一个警告:原先使用早期DIB文件编写的程序不支持24位DIB中的色彩对照表,如果在程序使用24位DIB的色彩对照表的话,就要冒一定的风险。

biClrImportant字段实际上没有biClrUsed字段重要,它通常被设定为0以指出色彩对照表中所有的颜色都是重要的,或者它与biClrUsed有相同的值。两种方法意味着同一件事,如果它被设定为0与biClrUsed之间的值,就意味着DIB图像能仅仅通过色彩对照表中第一个biClrImportant项目合理地取得。当在256色显示卡上并排显示两个或更多8位DIB时,这是很有用的。

对于1位、4位、8位和24位的DIB,图素位的组织和OS/2兼容的DIB是相同的,一会儿我将讨论16位和32位DIB。

真实检查

当遇到一个由其它程序或别人建立的DIB时,您希望从中发现什么内容呢?

尽管在Windows3.0首次推出时,OS/2样式的DIB已经很普遍了,但最近这种格式却已经很少出现了。许多程序写作者在实际编写快速DIB例程时忽略了它们。您遇到的任何4位DIB可能是Windows的「画图」程序使用16色显示器建立的,在这些显示器上色彩对照表具有标准的16种颜色。

最普遍的DIB可能是每图素8位。8位DIB分为两类:灰阶DIB和混色DIB。不幸的是,表头信息中并没有指出8位DIB的型态。

许多灰阶DIB有一个等于64的biClrUsed字段,指出色彩对照表中的64个项目。这些项目通常以上升的灰阶层排列,也就是说色彩对照表以00-00-00、04-04-04、08-08-08、0C-0C-0C的RGB值开始,并包括F0-F0-F0、F4-F4-F4、F8-F8-F8和FC-FC-FC的RGB值。此类色彩对照表可用下列公式计算:

rgb[i].rgbRed = rgb[i].rgbGreen = rgb[i].rgbBlue = i * 256 / 64 ;
        

在这里rgb是RGBQUAD结构的数组,i的范围从0到63。灰阶色彩对照表可用下列公式计算:

rgb[i].rgbRed = rgb[i].rgbGreen = rgb[i].rgbBlue = i * 255 / 63 ;
        

因而此表以FF-FF-FF结尾。

实际上使用哪个计算公式并没有什么区别。许多视频显示卡和显示器没有比6位更大的色彩精确度。第一个公式承认了这个事实。然而当产生小于64的灰阶时-可能是16或32(在此情况下公式的除数分别是15和31)-使用第二个公式更适合,因为它确保了色彩对照表的最后一个项目是FF-FF-FF,也就是白色。

当某些8位灰阶DIB在色彩对照表内有64个项目时,其它灰阶的DIB会有256个项目。biClrUsed字段实际上可以为0(指出色彩对照表中有256个项目)或者从2到256的数。当然,biClrUsed值是2的话就没有任何意义(因为这样的8位DIB能当作1位DIB被重新编码)或者小于或等于16的值也没意义(因为它能当作4位DIB被重新编码)。任何情况下,色彩对照表中的项目数必须与biClrUsed字段相同(如果biClrUsed是0,则是256),并且图素值不能超过色彩对照表项目数减1的值。这是因为图素值是指向色彩对照表数组的索引。对于biClrUsed值为64的8位DIB,图素值的范围从0x00到0x3F。

在这里应记住一件重要的事情:当8位DIB具有由整个灰阶组成的色彩对照表(也就是说,当红色、绿色和蓝色程度相等时),或当这些灰阶层在色彩对照表中递增(像上面描述的那样)时,图素值自身就代表了灰色的程度。也就是说,如果biClrUsed是64,那么0x00图素值为黑色,0x20的图素值是50%的灰阶,0x3F的图素值为白色。

这对于一些图像处理作业是很重要的,因为您可以完全忽略色彩对照表,仅需处理图素值。这是很有用的,如果让我回溯时光去对BITMAPINFOHEADER结构做一个简单的更改,我会添加一个旗标指出DIB映像是不是灰阶的,如果是,DIB就没有色彩对照表,并且图素值直接代表灰阶。

混色的8位DIB一般使用整个色彩对照表,它的biClrUsed字段为0或256。然而您也可能遇到较小的颜色数,如236。我们应承认一个事实:程序通常只能在Windows颜色面内更改236个项目以正确显示这些DIB,我将在后面一期讨论这些内容。

biXPelsPerMeter和biYPelsPerMeter很少为非零值,biClrImportant字段不为0或biClrUsed值的情况也很少。

DIB压缩

前面我没有讨论BITMAPINFOHEADER中的biCompression和biSizeImage字段,现在我们讨论一下这些值。

biCompression字段可以为四个常数之一,它们是:BI_RGB、BI_RLE8、BI_RLE4或BI_BITFIELDS。它们定义在WINGDI.H表头文件中,值分别为0到3。此字段有两个用途:对于4位和8位DIB,它指出图素位被用一种运行长度(run-length)编码方式压缩了。对于16位和32位DIB,它指出了颜色屏蔽(color masking)是否用于对图素位进行编码。这两个特性都是在Windows 95中发表的。

首先让我们看一下RLE压缩:

如果值是BI_RGB,图素位储存的方式和OS/2兼容的DIB一样,否则就使用运行长度编码压缩图素位。

运行长度编码(RLE)是一种最简单的数据压缩形式,它是根据DIB映射在一列内经常有相同的图素字符串这个事实进行的。RLE通过对重复图素的值及重复的次数编码来节省空间,而用于DIB的RLE方案只定义了很少的矩形DIB图像,也就是说,矩形的某些区域是未定义的,这能被用于表示非矩形图像。

8位DIB的运行长度编码在概念上更简单一些,因此让我们从这里入手。表15-1会帮助您理解当biCompression字段等于BI_RGB8时,图素位的编码方式。

表15-1

字节1

字节2

字节3

字节4

含义

00

00

行尾

00

01

映射尾

00

02

dx

dy