15.1 DIB 文件格式

来源:互联网 发布:php获取时间轴 编辑:程序博客网 时间:2024/05/23 19:06

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P569

        有趣的是,DIB 格式并不是从 Windows 起源的。它最早是在微软和 IBM 于上世纪 80 年代中期开始合作开发的操作系统 OS/2 1.1 版本中定义的。OS/2 1.1 于 1988 年发布,是第一个拥有像 Windows 一样的图形用户界面(叫 Presentation Manager,PM)的 OS/2 版本。PM 包含一套图形编程接口,其中定义了位图的格式。

        OS/2 的这种位图格式后来被用到 Windows 3.0(1990 年发布)中,从此被称作 DIB。Windows 3.0 还包含了最初 DIB 格式的一个变形,它最后成为了 Windows 环境下的标准。Windows 95(和 Windows NT 4.0)以及后来的 Windows 98(和 Windows NT 5.0)又在此基础之上做了一些改进,我会在本章的稍后部分讨论。

        最好是先把 DIB 想象为一种文件格式。DIB 文件的扩展名是 BMP,在极个别情况下也可以是 DIB。Windows 应用程序中用到的位图(例如一个按钮的图像)一般都是作为 DIB 文件存储在可执行文件的只读资源中。图标和鼠标指针也是 DIB 文件,只不过有一些微小的不同。

        程序可以把 DIB 文件除去开始的 14 个字节外,整个载入到一块连续的内存区域中这有时也被称为“紧凑 DIB 格式的位图”(packed-DIB format)。Windows 下的应用程序可以用这种格式创建画刷或者通过 Windows 剪贴板来交换图像。程序还拥有对该 DIB 内容的完全控制,可以对此 DIB 进行任意修改。

        程序也可以在内存中创建自己的 DIB,然后把它们存到文件中。这些 DIB 中的图像可以用 GDI 函数绘制。程序也可以直接对像素操作,在此过程中可以使用其他基于内存的 DIB。

        一个 DIB 被载入到内存后,程序可以对该 DIB 数据调用一些 Windows API 函数,这些函数我会在本章中讨论。与 DIB 相关的 API 调用并不多,主要都是用来在屏幕或者打印机上显示图像,还有一些是用来在 DIB 和 GDI 位图对象之间进行互相转换。

        尽管有以上提到的这些功能,但还有很多很多和 DIB 相关的任务 Windows 操作系统并不支持。例如,一个程序可以存取一个 24 位的 DIB,现在它想把这个 DIB 转换成一个拥有最佳匹配的 256 色调色板的 8 位 DIB。Windows 并不能替你做到这一点。而本章和第 16 章会演示怎样完成一些 Windows API 并不支持的 DIB 相关任务。

15.1.1  OS/2 风格的 DIB

        在尚未涉及太多细节之前,先来看一下和最早在 OS/2 1.1 版本中引入的位图文件相兼容的 Windows DIB 文件格式。

        DIB 文件有四个主要部分

  • 文件头
  • 信息头
  • RGB 颜色表(有时可能没有)
  • 位图的像素位

        可以把前两部分看做是 C 数据结构,把第三部分看做一个数组结构的数组。这些数据结构定义在 Windows 的头文件 WINGDI.H 中。在内存中紧凑格式的 DIB 有三个部分:

  • 信息头
  • RGB 颜色表(有时可能没有)
  • 位图的像素位

它和存在与文件中的 DIB 一模一样,唯一的区别在于没有文件头。

        DIB 文件——不是内存中紧凑格式 DIB——以一个 14 字节的文件头开始,该文件头的定义如下:

typedef struct tagBITMAPFILEHEADER    // bmfh{    WORD  bfType;          // 文件签名,是 "BM" 或者 0x4D42    DWORD bfSize;          // 整个文件的长度    WORD  bfReserved1;    // 必须是 0    WORD  bfReserved2;    // 必须是 0    DWORD bfOffsetBits;   // 到位图像素位的位移}BITMAPFILEHEADER, * PBITMAPFILEHEADER;
这也许和在 WINGDI.H 中的定义不完全一样,例如,注释是我加的,但是二者功能上时一样的。第一个注释,"bmfh",是我推荐使用的这个数据结构的缩写名称。如果你在我的程序中看到一个变量叫做 pbmfh,那就是一个指向 BITMAPFILEHEADER 类型的结构的指针,或者是一个 PBITMAPFILEHEADER 类型的变量。

        这个结构有 14 字节长。它以两个字母 "BM" 打头,说明这是一个位图文件。如果用十六进制的 WORD 值来表示,它的数值就是 0X4D42。“BM”之后是一个 DWORD,记录了整个文件的大小,以字节为单位,该大小包含了文件头的长度。在这之后的两个 WORD 字段必须设置为零。(在鼠标指针文件中,采用非常相似于 DIB 的文件格式,而这两个字段表示指针的“热点”位置。)这个结构的最后一个字段是一个 DWORD,它指示了图像的像素位在文件中的起始位置。这个值可以从 DIB 信息头中推导出来,这里提供它只是为了方便。

        在 OS/2 风格的 DIB 中,紧接着 BITMAPFILEHEADER 结构之后是一个 BITMAPCOREHEADER 结构,它提供了 DIB 图像的基本信息。紧凑格式的 DIB 就是这个 BITMAPCOREHEADER 结构开头,如下所示:

typedef struct tagBITMAPCOREHEADER    // bmch{    DWORD bcSize;        // 结构大小 = 12    WORD  bcWidth;       // 以像素计的图像的宽度    WORD  bcHeight;      // 以像素计的图像的高度    WORD  bcPlanes;      // = 1    WORD  bcBitCount;    // 每个像素的位数 (1, 4, 8 or 24)}BITMAPCOREHEADER, * PBITMAPCOREHEADER;
对于这个结构,其名称中的单词“core”(核心)听起来有一些奇怪,事实上也正是如此。它其实表面这是其他位图格式的基础(所以叫 “core”),其他的格式都由此衍生而来。

        在 BITMAPCOREHEADER 结构中的 bcSize 字段表明了这个结构本身的大小,在这里,它是 12 字节。

        bcWidth 和 bcHeight 两个字段按像素为单位,给出了位图的大小。尽管这两个字段是 WORD 类型,所以能表示有 65535 个像素宽或者高的 DIB,但是你可能很少会遇到那样大的图像。

        bcPlanes 字段总是 1。从它开始被定义一直到现在始终是 1。它是我们在第 14 章遇到的早期 Windows GDI 位图对象的遗留产物。

        bcBitCount 字段指明每一个像素的位数。对 OS/2 风格的 DIB 来说,它可以是 1, 4, 8 或者 24。DIB 中的颜色数等于 2^(bmch.bcBitCount),或者,用 C 语言的语法,就是:

1 << bmch.bcBitCount
因此,bcBitCount 字段就等于:

  • 1 对应于双色 DIB
  • 4 对应于 16 色 DIB
  • 8 对应于 256 色 DIB
  • 24 对应于全彩 DIB

在提到“一个 8 位 DIB”时,我指的是一个 DIB 的每个像素有 8 位。

        对于前三种情形(位数为 1、4 和 8),BITMAPCOREHEADER 后面是一个颜色表24 位 DIB 没有颜色表颜色表是一个数组,数组中的元素是一个 3 字节长的 RGBTRIPLE 结构,对应于图像中的每一种颜色:

typedef struct tagRGBTRIPLE     // rgbt{    BYTE rbgtBlue;      // 蓝色值    BYTE rbgtGreen;     // 绿色值    BYTE rgbtRed;       // 红色值}RGBTRIPLE;
一般都推荐把图像中最重要的颜色排在颜色表的前面。我们在第 16 章会看到原因。

        WINGDI.H 头文件中还定义了下面的结构:

typedef struct tagBITMAPCOREINFO    // bmci{    BITMAPCOREHEADER bmciHeader;             // 基本信息头    RGBTRIPLE        bmciColors[1];          // 颜色表数组}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 来说,就是信息头之后的数据)就是像素位本身了

15.1.2  自下而上存储!

        像大多数位图格式一样,DIB 中的像素位是以水平的行来排列的在视频显示硬件的术语中,也叫“扫描线”。图像中的行数等于 BITMAPCOREHEADER 结构中 bcHeight 字段的值。但是和其他大多数位图格式不同的是,DIB 是从图像的最下面一行开始,然后逐渐向上来存储整个图像的

        这里先解释一些术语。当我说“顶行”“底行”时,我指的是视觉上图像的顶部和底部,就像在显示器或打印页上正确显示时的样子。一幅人头肖像画的顶行是头发;底行是下巴。当我说“第一行”时,我指的是 DIB 文件中直接跟在颜色表中之后的像素行。当我说“最后一行”时,我指的是在文件最后的像素行。

        因此,在 DIB 中,图像的底行是文件的第一行,图像的顶行是文件的最后一行。这是一种由下往上的组织方式。由于这种组织方式不直观,你可能会问为什么要这样安排。

        一切都要从 OS/2 的 Presentation Manager 说起。在 IBM,有人认为在 PM 中的所有坐标系都应该保持一致,包括窗口、图形以及位图。这引发了一场辩论:多数人,包括哪些与整个屏幕的文本或窗口环境打交道的程序员,认为垂直坐标应该随着屏幕向下增加。然而,那些发烧级的计算机图形程序员更愿意从解析几何的角度来看图形显示系统。这就牵涉到了一个直角(或笛卡尔)坐标系,其中垂直坐标是随着空间向上而增加的。

        简而言之,数学家赢了。在 PM 中,所有坐标系都以左下角为源点,包括窗口坐标。这就是 DIB 为什么这样安排的原因

15.1.3  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;

如果需要的话,每一行的结尾要补充一些字节(一般用 0)来达到这个长度。所有像素数据所占用的字节数就是 RowLength 和 bmch.bcHeight 的乘积。

        要想知道像素是怎么编码的,让我们分别看一下这四种情况。在下面的图中,每一个字节的二进制位显示在格子中,7 代表最高位,0 代表最低位。像素也从 0 开始编号,从一行中最左边的像素开始。

        对于每像素 1 位的 DIB 来说,每个字节表示了 8 个像素。最左边的像素就是第一个字节的最高位

每一个像素数值不是 0 就是 1。0 代表这个像素的颜色是由颜色表第一个 RGBTRIPLE 字段指定。1 代表这个像素的颜色是由颜色表第二个 RGBTRIPLE 字段指定。

        对于每像素 4 位的 DIB 来说,每个字节表示了 2 个像素。最左边的像素就是第一个字节的高 4 位,以此类推:

每一个 4 位像素的值在 0 到 15 之间。这个数值就是颜色表中 16 个元素的下标。

        对于每像素 8 位的 DIB 来说,每个字节表示了 1 个像素:


每一个像素(字节)的数值在 0 到 255 之间。同样,这个数值就是颜色表中 256 个元素的下标。

        对于每像素 24 位的 DIB 来说,每个像素需要 3 个字节来代表红、绿和蓝色的数值。每一行像素基本上就是一个 RGBTRIPLE 结构的数组,只是可能在每一行的结尾添加一些 0 字节以保证每行的字节数都是 4 的倍数。

再次说明,每像素 24 位的 DIB 没有颜色表。

15.1.4  Windows 扩展 DIB

        现在我们已经掌握了在 Windows 3.0 中引入的和 OS/2 兼容的 DIB 格式,让我们来看一下同时在 Windows 3.0 中引入的扩展版本的 DIB 格式吧。

        这个格式的 DIB 和前面提到的格式一样,也是以一个 BITMAPFILEHEADER 结构开头,但是,接在后面的是一个 BITMAPINFOHEADER 结构,而不是 BITMAPCOREHEADER 结构:

typedef struct tagBITMAPINFOHEADER    // bmih{    DWORD biSize;           // 结构大小 == 40    LONG  biWidth;          // 以像素计的图像的宽度    LONG  biHeight;         // 以像素计的图像的高度    WORD  biPlanes;         // = 1    WORD  biBitCount;       // 每个像素的位数 (1, 4, 8, 16, 24 或 32)    DWORD biCompression;    // 压缩编码    DWORD biSizeImage;      // 图像的字节数    LONG  biXPelsPerMeter;  // 水平分辨率    LONG  biYPelsPerMeter;  // 垂直分辨率    DWORD biClrUsed;        // 用到的颜色数    DWORD biClrImportant;   // 重要颜色的数目}BITMAPINFOHEADER, * PBITMAPINFOHEADER;

        你可以从这个结构的第一个字段来区别 OS/2 兼容的 DIB 和 Windows DIB: 前者是 12,后者是 40

        你可能注意到了,这个结构有六个新字段,但是 BITMAPINFOHEADER 结构并不是简单的 BITMAPCOREHEADER 结构再加上一些新东西。再仔细看一下:在 BITMAPCOREHEADER 结构中 bcWidth 和 bcHeight 字段是 16 位的 WORD 型数值。而在这个结构中,它们是 32 位的 LONG 型数值。这个讨厌的小改动肯定会让你发疯

        还有一个改动:对于 1 位、4 位 和 8 位的 DIB,如果用 BITMAPINFOHEADER 结构的话,颜色表不再是一个 RGBTRIPLE 结构的数组。接在 BITMAPINFOHEADER 结构后面的是一个 RGBQUAD 结构的数组:

typedef struct tagRGBQUAD  // rgb{    BYTE rgbBlue;         // 蓝色值    BYTE rgbGreen;        // 绿色值    BYTE rgbRed;          // 红色值    BYTE rbgReserved;     // = 0}RGBQUAD;
除了第 4 个字段总是设置为 0 以外,这个结构几乎和 RGBTRIPLE 结构一样。在 WINGDI.H 头文件中,还定义了下面的结构:

typedef struct tagBITMAPINFO    // bmi{    BITMAPINFOHEADER  bmiHeader;    // 信息头    RGBQUAD           bmiColors[1]; // 颜色表数组}BITMAPINFO, * PBITMAPINFO;
请注意,BITMAPINFO 结构从一个 32 位地址边界开始,每一个 RGBQUAD 字段也都从 32 位地址边界开始,这是因为 BITMAPINFOHEADER 结构是 40 个字节长。这保证了32 位的处理器可以很有效地对颜色表数据进行寻址

        尽管 BITMAPINFOHEADER 最初是在 Windows 3.0 中定义的,但有些字段在 Windows 95 和 Windows NT 4.0 中又被重新定义了,并被带到了 Windows 98 和 Windows NT 5.0 里。例如,现在的文档中注明:“如果 biHeight 是负数,那么这个 DIB 就是从上到下的,原点为左上角。”知道这个确实不错,但如果 1990 年最初定义这个 DIB 格式的时候有人就这么决定就更好了。我的建议是不要从上到下的 DIB。因为那些不知道这个“功能”的程序在遇到一个负的 biHeight 字段时会崩溃或者出错。例如,微软 Word 97 中包括的 Microsoft Photo Editor 就会在遇到一个从上到下的 DIB 时报告“非法图像高度”(Word 97 自己倒是没有这个问题)。

        biPlanes 字段仍然是 1,但是 biBitCount 字段现在除了可以是1、4、8、24 之外,还可以是 16 或者 32。这也是在 Windows 95 和 Windows NT 4.0 中的新功能。我一会儿就会讨论到这两个新格式是怎么工作的。

        让我先跳过 biCompression 和 biSizeImage 字段。我很快就会对此展开讨论。

        biXPelsPerMeter 和 biYPelsPerMeter 字段指示图像在真实世界中的大小,以一个拙劣的每米像素数作为单位。(“Pel”,Picture Element,是 IBM 对像素的称呼。)Windows 内部并不用这个信息。但是,应用程序可以用它来显示 DIB 真正的大小。有些 DIB 是从不具备正方形像素的设备中得来的,这两个字段对这些 DIB 也是有用的。在绝大多数 DIB 里,这两个字段都是 0,说明图像没有一个推荐的真实世界的大小。72 DPI(每英寸点数)的分辨率(一般用在显示器上,但是真正的分辨率要取决于显示器的大小)大约等于每米 2835 个像素,而一般打印机的 300 DPI 分辨率大约等于每米 11811 个像素。

        biClrUsed 字段是一个非常重要的字段,因为它影响到颜色表中元素的数目。对 4 位和 8 位的 DIB 来说,这说明颜色表中可能会有少于 16 或 256 个元素。这是一种减少 DIB 大小的办法,尽管并没有减少多少。例如,假如一个 DIB 图像中只有 64 级灰度,biClrUsed 字段就会设置为 64,颜色表中就会只有 64 个 RGBQUAD 结构,而不是 256 个。每个像素的数值就会是 0x00 到 0x3F。这个 DIB 仍然需要用 1 个字节来表示一个像素,但是每个字节的最高两位为 0。如果 biClrUsed 字段为 0,则说明颜色表中包括了由 biBitCount 字段指明的全部数目的元素。

        从 Windows 95 开始,biClrUsed 字段对于 16、24 和 32 位的 DIB 来说也可以不是 0。在这种情况下,颜色表不再被 Windows 用来解释像素位数据,而是被用作在一个 256 色的显示器上显示这个 DIB 时所用的调色板。你可能还记得在 OS/2 兼容的格式中,一个 24 位的 DIB 没有颜色表。在 Windows 3.0 中,扩展的 DIB 格式也是如此。但是在 Windows 95 中,24 位的 DIB 也可以有一个颜色表,其大小由 biClrUsed 字段指明。

        简单总结如下:

  • 1 位 DIB 中,biClrUsed 总是 0 或 2.颜色表总是有 2 个元素。
  • 4 位 DIB 中,如果 biClrUsed 是 0 或 16,那么颜色表有 16 个元素。如果 biClrUsed 的数值是 2~15,那么它指明了颜色表的元素数,每一个像素的最大值为这个数减 1。
  • 8 位 DIB 中,如果 biClrUsed 是 0 或 256,那么颜色表有 256 个元素。如果 biClrUsed 的数值是 2 ~255,那么它指明了颜色表的元素数,每一个像素的最大值为这个数减 1。
  • 16、24 和 32 位 DIB 中,biClrUsed 一般为 0。如果不是,那么它指明了颜色表中的元素数。应用程序可以用这个颜色表为该 DIB 在 256 色的显示适配器上设置调色板

        另一个警告:根据较早的 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。

15.1.5  现实情况

        如果遇到一个由别人或程序创建的 DIB,你可以期待发现什么呢?

        尽管 OS/2 风格的 DIB 在 Windows 3.0  发布时是很常见的,但最近几年它们已经变得非常罕见了。有些程序在要快速编写一些 DIB 程序时干脆就把它们忽略不计了。你能遇到的 4 位 DIB 很可能是由 Windows 的画图程序在 16 色的显示适配器上创建的,其中的颜色表就是那些显示器上的标准 16 色。

        你能找到的最常见的 DIB 可能就是 8 位的了(译注:这是本书英文版成书时的情况。现在,随着数码硬件的普及,最常见的 DIB 已经是 24 位的“真彩”的图像了)。8 位 DIB 图像可以分为两类:灰度 DIB 和调色板化的彩色 DIB。可惜,文件头中没有任何信息可以告诉你具体是哪一种。

        有些灰度 DIB 的 biClrUsed 字段等于 64,说明颜色表中有 64 个元素。这些元素一般都以灰度递增的顺序排列。也就是说,颜色表开始的 RGB 值是 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[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 的任何一个值。当然了,如果它是 2,就没有什么必要了(因为这样的一个 8 位 DIB 可以重新编码为 1 位的 DIB)。同样,如果它小于 16,这个 DIB 就可以重新编码为 4 位 DIB。不管是哪种情况,颜色表中的元素个数一定要等于 biClrUsed(或者在 biClrUsed 为 0 时等于 256),而且每个像素的数值不能大于颜色表元素个数减一,因为像素的数值就是颜色表数组的下标。对于 biClrUsed 为 64 的 8 位 DIB 来说,像素的数值是 0x00 到 0x3F。

        有一件很重要的事情值得记住当一个 8 位 DIB 的颜色表中的元素全部是灰度(也就是说,红、绿、蓝的级别一样),而且灰度的等级是均匀的升高时(像我刚才描述的情况),那么像素的值就代表了灰度的比例。例如,biClrUsed 是 64,那么一个值为 0 的像素是黑色,一个值为 0x20 的像素为 50% 灰色,值为 3F 的像素值为白色。

        这一点对某些图像处理的任务很重要,因为你可以完全忽略颜色表而只和像素值打交道。这太有用了,以至于如果给我一个机会回到过去对 BITMAPINFOHEADER 结构作一个修改的话,我就会在其中加上一个标志位,说明这个 DIB 是灰度的,没有颜色表,每个像素的值直接代表灰度等级。

        调色板化的 8 位彩色 DIB 一般都会用到整个颜色表,于是其 biClrUsed 字段值为 0 或者 256。然而,你也可能遇到更少颜色的情况——例如,236 色。这和如下事实有关:程序一般只能改变 Windows 调色板中 236 个元素的值以准确显示这个 DIB。我会在第 16 章讨论到这个问题。

        biXPelsPerMeter 和 biYPelsPerMeter 值不为 0 的情况很少见另一种极少见的情况是 biClrImportant 不等于 0 或者不等于 biClrUsed

15.1. 6  DIB 压缩

        前面我推迟了对 BITMAPINFOHEADER 结构中 biCompression 和 biSizeImage 字段的讨论。现在是时候来检视它们了。

        biCompression 字段可以是 4 个常数值之一:BI_RGB,BI_RLE8,BI_RLE4 或 BI_BITFIELDS,它们在 WINGDI.H 头文件中定义为 0 到 3。这个字段有两个意义:对 4 位和 8 位的 DIB 来说,它说明像素的值以一种行程长度编码(run-length encoding, RLE)压缩;对 16 位或 32 位 DIB 来说,它说明是否使用颜色遮罩(color mask)来对像素位进行编码。第二个功能是在 Windows 95 中被引入的。

        让我们来先看一下行程长度编码(RLE)压缩。

  • 对 1 位 DIB 来说,biCompression 字段总是 BI_RGB。
  • 对 4 位 DIB 来说,biCompression 字段可以是 BI_RGB 或 BI_RLE4。
  • 对 8 位 DIB 来说,biCompression 字段可以是 BI_RGB 或 BI_RLE8。
  • 对 24 位 DIB 来说,biCompression 字段总是 BI_RGB。

        如果这个值是 BI_RGB,那么像素位就和 OS/2 兼容的 DIB 一样存储。否则的话,像素位就是用行程长度编码来压缩的。

        行程长度编码(RLE)是最简单的数据压缩方法之一它基于这样一个知识:DIB 图像中经常在一行中会有一连串相同的像素。行程长度编码通过记录下重复像素的值和它重复的次数来节省空间。DIB 使用的行程长度编码算法有时还超出这个范围,它可以用来定义一个稀疏的矩形 DIB 图像。也就是说,矩形中有些区域是没有定义的。这可以用来绘制非矩形的图像。

        对 8 位 DIB 进行行程长度编码从概念上来说是最简单的,那就让我们从这里开始吧。下表有助于理解在 biCompression 字段等于 BI_RLE8 时像素是怎么编码的。

字节 1字节 2字节 3字节 4含  义 00 00   一行的结尾 00 01   图像的结尾 00 02 dx dy 移动到(x+dx, y+dy) 00 n = 03 到 FF   使用接下来的 n 个像素 n = 01 到 FF 像素   重复像素 n 次        在对一个压缩的 DIB 解码时,要把 DIB 字节数据成对看,就像上表中的“字节 1”和“字节 2”一样。这个表格是按照字节的值从小到大排列的,但是从底向上地讨论更方便一些。

        如果第一个字节不为 0(如表中最后一行所示),那么它就是行程长度编码的重复因子它表示后面的像素数据就重复这么多次。例如,以下这对字节:

0x05 0x27
就被解码为如下像素值:
0x27 0x27 0x27 0x27 0x27

        DIB 中也会有很多不重复的像素值。这种情况由表格的倒数第二行来处理。它表明其后跟随的若干像素就应该直接使用。例如,以下序列:

0x00 0x06 0x45 0x32 0x77 0x34 0x59 0x90
就被解码为如下像素值:

0x45 0x32 0x77 0x34 0x59 0x90

        这样的序列总是与两个字节的边界对齐的。如果第二个字节是奇数,那么在这个序列的结尾就会有一个不同的多余字节。例如,以下序列:

0x00 0x05 0x45 0x32 0x77 0x34 0x59 0x00
就被解码为如下像素值:

0x45 0x32 0x77 0x34 0x59

        行程长度编码就是这么工作的。很明显,如果 DIB 图像中没有重复像素,用这种压缩技术反而会增加 DIB 文件的大小

        表格的前三行指出了一个矩形 DIB 图像中有些部分是可以没有定义的。现在,假设要写一个 DIB 解压缩程序。在解压缩过程中,会维护一对从(0, 0)开始的数值(y, x)。每次解码出一个像素,便使 x 加 1。每结束一行的解码,你会把 y 加 1 并且把 x 重置为 0。

        在遇到一个 0x00 字节,后跟一个 0x02 字节时,应该读入后面紧跟的两个字节然后把它们作为无符号整数加到现在的 x 和 y 值上。在遇到一个 0x00 字节后面跟着一个 0x00 字节时,这一行的解码就结束了。应该把 x 设置为 0 并把 y 加 1。在遇到一个 0x00 字节后面跟着一个 0x01 字节时,整个图像的解码就算完成了。这样的代码可以让 DIB 中包括未定义的区域。有时候这会很有用,例如说定义一个非矩形的图像,或者制作数字动画或电影的时候(因为每一帧中会包含很多在上一帧中已有的信息,这些信息没有必要再重新编码)。

        对 4 位 DIB 来说,编码的过程基本上是一样的,但是会复杂一些,因为像素和字节不是一一对应的

        如果第一个字节不是 0,那么它就是重复因子 n。第二个字节包括两个像素,它们会在解码后的序列中交替重复至 n 个像素。例如,以下这对数据:

0x07 0x35
就被解码为如下像素值:

0x35 0x35 0x35 0x3?
其中的问号表面这个像素还是未知的。如果前面 0x07 0x35 数据对之后是以下这对数据:

0x05 0x24
那么整个解码后的序列将如下所示:

0x35 0x35 0x35 0x32 0x42 0x42

        如果一对数据的第一个字节是 0x00 而第二个字节为 0x03 或更大时,解码就要使用由第二个字节指定的数目的像素。例如,下面的序列:

0x00 0x05 0x23 0x57 0x10 0x00
就被解码为:

0x23 0x57 0x1?
注意,编码序列必须填补成偶数个字节。

        只要 biCompression 字段是 BI_RLE4 或者 BI_RLE8,biSizeImage 字段就会存储以字节为单位的 DIB 像素数据大小。如果 biCompression 字段是 BI_RGB,biSizeImage 通常为 0,但是它也可以设置成 biHeight 和每一行字节数的乘积,就像我们在本章前面计算过的一样。

        现在的文档中注明:“从上到下的 DIB 不能被压缩。所谓从上到下的 DIB 指的是那些 biHeight 字段为负值的图像。

15.1.7  颜色遮罩

        biCompression 字段还可以用在 Windows 95 新加入的 16 位和 32 位的 DIB 中。在这样的 DIB 中,biCompression 字段可以是 BI_RGB 或者 BI_BITFIELDS(定义值为 3)。

        让我们先来回顾一下 24 位 DIB 的像素格式,其 biCompression 字段总是 BI_RGB:

每一行基本上就是一个 RGBTRIPLE 结构的数组,只是在每一行结束时有可能会有一些填充的数据以使每行的字节数都是 4 的倍数

        对一个 biCompression 为 BI_RGB 的 16 位 DIB 来说,每个像素需要两个字节。颜色编码如下:

每个颜色用 5 位来表示。对每行的第一个像素来说,蓝色值是第一个字节的最低 5 位。绿色值需要用到两个字节中的二进制位:它的最高两位是第二个字节的最低两位,它的低三位是第一个字节的最高三位。红色值是第二个字节的 2 到 6 位。第二个字节的最高位为 0。

        在用一个 16 位的 WORD 型整数来存取像素值时,这就非常清楚了。因为一个多字节的数值存储总是把最低位的字节存在前面,这个像素的数值就会是下面这样:

        假如有一个 16 位的像素储存在 wPixel 变量中,可以按照下面的公式来计算红、绿、蓝色的值:

Red     = ((0x7C00 & wPixel) >> 10) << 3;Greeen  = ((0x03E0 & wPixel) >> 5) << 3;Blue    = ((0x001F & wPixel) >> 0) << 3;

        首先,这个像素要和一个遮罩作一个按位与操作,结果再向右位移。对红色来说,右移 10 位,绿色右移 5 位,蓝色右移 0 位。我会把这些位移值叫做“右移”值。这样产生的颜色值在 0x00~0x1F 之间。这个值还需要左移三位以使其颜色值在 0x00 到 0xF8 之间。我会把这些位移值称为“左移”值。

        还有一点要记住:如果一个 16 位 DIB 的像素宽度是奇数,那么每一行就会有 2 个字节填充在最后以使每一行的字节数是 4 的倍数。

        对 32 位的 DIB 来说,如果 biCompression 等于 BI_RGB,那么每一个像素就需要 4 字节。蓝色值是第一个字节,绿色是第二个,红色是第三个,第四个字节为 0。换言之,像素就是一个 RGBQUAD 结构的数组。因为每个像素都是 4 字节,所以每一行的结尾永远不需要填充。

        如果把每个像素视为一个 32 位的双字,则会是下面这样:

        或者,如果 dwPixel 是一个 32 位的双字,则像下面这样:

Red    =  ((0x00FF0000 & dwPixel) >> 16) << 0;Green  =  ((0x0000FF00 & dwPixel) >> 8) << 0;Blue   =  ((0x000000ff & dwPixel) >> 0) << 0;
左移位全部是 0,这是因为颜色值已经可以到最大的 0xFF 了。注意,这个 32 位值和 GDI 函数中用于指定 RGB 颜色的 32 位 COLORREF 并不一样。在 COLORREF 值中,红色在最低字节

        到现在为止,我们已经介绍了 16 位和 32 位 DIB 当 biCompression 字段为 BI_RGB 时的默认情况。如果 biCompression 字段是 BI_BITFIELDS,那么在 BITMAPINFOHEADER 结构之后紧跟着的是 3 个 32 位的颜色遮罩,第一个用于红色,第二个用于绿色,第三个用于蓝色。可以用 C 语言中的按位与操作符(&)来把这些遮罩加到 16 位或 32 位的像素值上。然后可以右移来得到最终结果。可惜,右移的位数必须从遮罩中得到。仔细想一下,颜色遮罩的规则应该是很明显的:每一个颜色遮罩中 1 的位必须是连续的,而且 3 个遮罩中为 1 的位不能互相重叠。

        让我们来看一个例子吧。假设有一个 16 位的 DIB,而且 biCompression 字段为 BI_BITFIELDS。检查一下 BITMAPINFOHEADER 结构之后的 3 个 32 位数:

0x0000F8000x000007E00X0000001F

        注意,只有在最低的 16 位中才可以设置为 1,因为这时一个 16 位的 DIB。把这三个值保存到变量 dwMask[0]、dwMask[1]和 dwMask[2]中。现在,需要写两个函数来从这些遮罩中计算出右移和左移的位数:

int MaskToRShift (DWORD dwMask){int iShift ;if ( dwMask == 0)return 0 ;for ( iShift = 0 ; !(dwMask & 1) ; iShift++)dwMask >>= 1 ;return iShift ;}int MaskToLShift (DWORD dwMask){    int iShift ;        if ( dwMask == 0)        return 0 ;        while (!(dwMask & 1))        dwMask >>= 1 ;        for (iShift = 0 ; dwMask & 1 ; iShift++)        dwMask >>= 1 ;        return 8 - iShift ;}
然后调用 MaskToRShift 函数 3 次来得到右移的位数:

iRShift[0] = MaskToRShift (dwMask[0]) ;iRShift[1] = MaskToRShift (dwMask[1]) ;iRShift[2] = MaskToRShift (dwMask[2]) ;
分别得到 11、5 和 0。然后可以类似地调用 MaskToLShift:

iLShift[0] = MaskToLShift (dwMask[0]) ;iLShift[1] = MaskToLShift (dwMask[1]) ;iLShift[2] = MaskToLShift (dwMask[2]) ;
分别得到 3、2 和 3。现在便可以从像素值从得到颜色值了:

Red   = ((dwMask[0] & wPixel) >> iRShift[0]) << iLShift[0] ;Green = ((dwMask[1] & wPixel) >> iRShift[1]) << iLShift[1] ;Blue  = ((dwMask[2] & wPixel) >> iRShift[2]) << iLShift[2] ;
对 32 位 DIB 来说,过程一样,只是颜色遮罩可以大于 16 位 DIB 所能允许的最大值 0x0000FFFF。

        注意,不管是 16 位还是 32 位的 DIB,红、绿、蓝的颜色值都可以大于 255。事实上,在 32 位 DIB 中,如果两个颜色的遮罩都是 0,那么第三个可以是 0xFFFFFFFF,也就是一个 32 位的颜色值!当然,这在某种意义上说有些可笑,所以我不会太为此而担心。

        与 Windows NT 不同,Windows 95 和 Windows 98 对颜色遮罩有更严格的规定。允许的数值如下表所示。

 16 位 DIB16 位 DIB32 位 DIB 红色遮罩 0x00007C00 0x0000F800 0x00FF0000 绿色遮罩 0x000003E0 0x000007E0 0x0000FF00 蓝色遮罩 0x0000001F 0x0000001F 0x000000FF 简写 5-5-5 5-6-5 8-8-8        换言之,当 biCompression 是 BI_RGB 时,可以用默认的两套遮罩,再加上我在上面例子中所用的那一套遮罩。表的最下一行给出了一个简写形式,指明了每个像素种红、绿和蓝色所用的位数。

15.1.8  版本 4 的文件头

        我们还远未结束呢。正如我提到的,Windows 95 改变了最早的 BITMAPINFOHEADER 结构中有些字段的含义,而且还包括了一个扩展的信息头,叫做 BITMAPV4HEADER。当你意识到 Windows 95 可能会被命名为 Windows 4.0,而且 Windows NT 4.0 也支持这个结构时,你就会明白为什么这个结构会有这样的名称。

typedef struct{    DWORD       bV4Size ;          // 结构的大小 = 120    LONG        bV4Width ;         // 以像素为单位的图像宽度    LONG        bV4Height ;        // 以像素为单位的图像高度    WORD        bV4Planes ;        // = 1    WORD        bV4BitCount ;      // 每像素位数 (1, 4, 8, 16, 24, or 32)    DWORD       bV4Compression ;   // 压缩编码    DWORD       bV4SizeImage ;     // 图像字节数    LONG        bV4XPelsPerMeter ; // 水平分辨率    LONG        bV4YPelsPerMeter ; // 垂直分辨率    DWORD       bV4ClrUsed ;       // 用到的颜色数    DWORD       bV4ClrImportant ;  // 重要的颜色数    DWORD       bV4RedMask ;       // 红色遮罩    DWORD       bV4GreenMask ;     // 绿色遮罩    DWORD       bV4BlueMask ;      // 蓝色遮罩    DWORD       bV4AlphaMask ;     // 阿尔法遮罩    DWORD       bV4CSType ;        // 色彩空间类型    CIEXYZTRIPLE bV4Endpoints ;    // XYZ 值    DWORD       bV4GammaRed ;      // 红色伽玛值    DWORD       bV4GammaGreen ;    // 绿色伽玛值    DWORD       bV4GammaBlue ;     // 蓝色伽玛值}BITMAPV4HEADER, * PBITMAPV4HEADER ;

        请注意,前面的 11 个字段和 BITMAPINFOHEADER 结构一样。最后 5 个字段用于支持 Windows 95 和 Windows NT 4.0 的颜色匹配(color-matching)技术。如果用不到 BITMAPV4HEADER 结构的最后 4 个字段,便应该用 BITMAPINFOHEADER(或 BITMAPV5HEADER)。

        bV4RedMask、bV4GreenMask 和 bV4BlueMask 只适用于 bV4Compression 字段为 BI_BITFIELDS 的 16 位或 32 位 DIB。它们和 BITMAPINFOHEADER 结构之后的颜色遮罩的功能一样,而且事实上也出现在 DIB 文件的同样位置,只不过这里它们是明确命名的字段而已。而 bV4AlphaMask 字段就我所知没有被用到。

        BITMAPV4HEADER 结构的其余字段牵涉到 Windows 的图像颜色管理系统(Windows Image Color Management),这恐怕超出了本书的范围。但是,一些背景知识也许可以帮你入门。

        用 RGB 来表示颜色有一个缺陷,就是它和显示器、彩色照相机和彩色扫描仪的技术密切相关。如果一个颜色的 RGB 值为(255, 0, 0),它仅仅意味着在一个阴极射线管中的红色电子枪应该施加最大的电压。一个为(128, 0, 0)的 RGB 值意味着应该施加一半的电压。但是不同的显示器反应却是可以不一样的。更重要的是,打印机用的颜色模式是不一样的,它是由青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Black)混合成的。这个颜色模式也称为CMY(cyan-magenta-yellow)和CMYK(cyan-magenta-yellow-black)。有数学公式可以把 RGB 值转换成 CMY 和 CMYK 值但是没法保证打印出来的颜色会和显示出来的颜色一样。图像颜色管理系统就是要把颜色和一个设备无关的标准联系起来。

        颜色现象和可见光的波长有关,其范围在 380nm(蓝色)到 780nm(红色)之间。我们能够看见的光都是由在可见光光谱内的不同数量、不同波长的光组成的。1931 年,国际照明委员会(Commission Internationale de l´Eclairage,CIE)发布了一个科学的定量颜色的方法。这个方法使用了三个颜色匹配函数(称为x, y, z)来描述一般人对不同光的波长的反应,其精简形式(每隔 5nm 一个值)出版在 CIE 刊物 15.2-1986,《比色法,第 2 版》,表 2.1 中。

        一个颜色的频谱(Spectrum, S)是一组值,表明了每个波长的强度。如果知道了频谱,那么前面提到的三个颜色匹配函数就可以用来计算出 X, Y 和 Z:

这三个值称为大 X、大 Y 和大 Z。y 颜色匹配函数相当于人眼对可见光谱内的光线强度的反应。(它看起来像一个铃铛状的曲线,在 380nm 和 780nm 时为 0。)因为它代表了光线的总体强度,所以 Y 被称为 CIE 亮度(CIE Luminance)

        如果要用 BITMAPV5HEADER 结构,bV4CSType 字段就必须设置为 LCS_CALIBRATED_RGB,其值定义为 0。接下来的 4 个字段必须设置为合理的值。

        CIEXYZTRIPLE 结构定义如下:

typedef struct tagCIEXYZTRIPLE{    CIEXYZ ciexyzRed ;    CIEXYZ ciexyzGreen ;    CIEXYZ ciexyzBlue ;}CIEXYZTRIPLE, * LPCIEXYZTRIPLE ;
其中的 CIEXYZ 结构如下:

typedef struct tagCIEXYZ{    FXPT2DOT30 ciexyzX ;    FXPT2DOT30 ciexyzY ;    FXPT2DOT30 ciexyzZ ;}CIEXYZ, * LPCIEXYZ ;
它的三个字段定义为 FXPT2DOT30 类型也就是说它们是定点(fixed-point)数值,整数部分是 2 位而小数部分为 30 位。所以,0x40000000 就表示 1.0, 0x48000000 是 1.125。最大值是 0xFFFFFFFF,比 4.0 小一点点。

        bV4Endpoints 字段提供了三个 X、Y 和 Z 的值,对应于 RGB 颜色的 (255, 0, 0),(0, 255, 0)和(0, 0, 255)。这三个值应该在创建 DIB 时,由创建它的应用程序加入进来,用以指明独立于设备的 RGB 颜色的含义。

        BITMAPV4HEADER 剩下的三个字段是“伽玛”值(Gamma)。伽玛(小写希腊字母 γ)指的是颜色值的非线性属性。在 DIB 中,红、绿、蓝色的范围是 0 到 255。在显示适配器上,这三个数值被转换成三个模拟电压输出到显示器上。这个电压决定了每个像素的强度。但是,由于阴极射线管中电子枪发射出的电子的特点,像素的强度(intensity)I 和电压(voltage)V 的关系并不是线性的,而是

其中,ε 是显示器的黑电平(Black Level),由显示器的亮度控制器决定(最好为 0)指数 γ 由显示器的对比度或图像控制器决定对绝大多数显示器来说,γ 大约是 2.5

        为了补偿这种非线性关系,摄像机传统上都要包括一个“伽玛校正”的功能。进入摄像机的光线要被一个 0.45 的指数修改。这意味着显示器的伽玛值大约是 2.2(1/0.45)。(更高的伽玛值会增加对比度,但通常这不是我们想要的,因为背景光会降低对比度。)

        实际上,显示器的这个非线性特征比它看起来要恰当得多这是因为人对光的反应也是非线性的。我前面提到 Y 被称为 CIE 亮度。这是对光的一个线性度量。CIE 也定义了一个接近人感知的光强数值 L*(英语发音为 “ell star”),它可以用下面的公式从 Y 计算出来:

其中,Yn 是白电平(White Level)。公式的第一部分是一个线性关系。总体来说,人对光的感知与光亮度的立方根是线性关系,这是由第二个公式表示的。L* 的范围从 0 到 100。L* 的每个整数级的变化,一般被认为是人能察觉的最小光亮变化

        在程序中,我们最好是根据人对光的感应而不是线性光亮来调节光的强度。这样我们可以把像素位数降至一个合理的水平并且可以减少模拟电路中的噪音。

        让我们来看一看整个过程吧。像素值(P)的范围是从 0 到 255。这个数值被线性地转换成电压,我们可以假设它被规范化到 0.0 至 1.0 之间。假设显示器的黑电平被设置为 0,则该像素点的强度如下:

其中,γ 大约为 2.5。人的感应光亮(L*)基于这个强度的立方根,而且范围从 0 至 100,那么近似地:

那个指数约为 0.85 左右。如果指数是 1,那么 CIE 亮度就和像素值完美匹配了,尽管事实并非如此,但它还是比用像素值代表线性光亮更接近一些。

        BITMAPV4HEADER 的最后三个字段为创建 DIB 的程序提供了一个方式来指明像素值应该具有的伽玛值。这个值有 16 位整数部分和 16 位小数部分。例如,0x10000 是 1.0。如果 DIB 是一个由真实世界拍摄出的图像,这个伽玛值很可能由拍摄硬件隐含指定,并且大概是 2.2(编码为 0x23333)如果 DIB 是由程序算法生成的,这个程序一般会用一个指数函数把所有线性光亮转换成 CIE 光亮。该指数的倒数就是 DIB 中的伽玛值

15.1.9  版本 5 的头文件

        为 Windows 98 和 Windows NT 5.0 写的程序可以使用一个新的 BITMAPV5HEADER 结构,如下所示:

typedef struct{    DWORD       bV5Size;             // 结构的大小= 120    LONG        bV5Width;            // 以像素为单位的图像宽度    LONG        bV5Height;           // 以像素为单位的图像高度    WORD        bV5Planes;           // = 1    WORD        bV5BitCount;         // 每像素位数(1, 4, 8, 16, 24 或 32)    DWORD       bV5Compression;      // 压缩编码    DWORD       bV5SizeImage;        // 图像字节数    LONG        bV5XPelsPerMeter;    // 水平分辨率    LONG        bV5YPelsPerMeter;    // 垂直分辨率    DWORD       bV5ClrUsed;          // 用到的颜色数    DWORD       bV5ClrImpportant;    // 重要颜色数    DWORD       bV5RedMask;          // 红色遮罩    DWORD       bV5GreenMask;        // 绿色遮罩    DWORD       bV5BlueMask;         // 蓝色遮罩    DWORD       bV5AlphaMask;        // 阿尔法遮罩    CIEXYZTRIPLE bV5Endpoints;       // XYZ 值    DWORD       bV5GammaRed;         // 红色伽玛值    DWORD       bV5GammaGreen;       // 绿色伽玛值    DWORD       bV5GammaBlue;        // 蓝色伽玛值    DWORD       bV5Intent;           // 渲染意图    DWORD       bV5ProfileData;      // 颜色配置数据或文件名    DWORD       bV5ProfileSize;      // 内嵌数据或文件名的大小    DWORD       bV5Reserved;}BITMAPV5HEADER, * PBITMAPV5HEADER
它有 4 个新字段,实际上只用到三个。这些字段支持由国际色彩协会(International Color Consortium,ICC,由 Adobe、Agfa、Apple、Kodak、Microsoft、Silicon Graphics 和 Sun Microsystems 等创立)提出的一个叫 ICC 颜色配置文件格式规范(ICC Profile Format Specification)的提案。可以从 http://www.icc.org 得到这个提案的副本。简单地讲,每个输入设备(扫描器或照相机)、输出设备(打印机或胶片记录器)和显示设备(显示器)都和一个颜色配置文件(profile)联系在一起。这个颜色配置文件把设备相关的颜色值(一般是 RGB 或 CMYK)与一个设备无关的颜色规范(最终基于 CIE 的 XYZ 值)联系在一起。颜色配置文件的文件扩展名是 .ICM(Image Color Management, 图像颜色管理)。颜色配置文件可以内嵌在 DIB 文件中或者链接到 DIB 文件中以表明 DIB 是怎么被创建出来的。可以从 MSDN 文档的“/Platform SDK/Graphics and Multimedia Service/Color Management”(译注:MSDN Library/Win32 and COM Development/ Graphics and Multimedia/Windows Color System)部分进一步了解 Windows 图像颜色管理

        BITMAPV5HEADER 中的 bV5CSType 字段可以采用几个不同的数值。如果它是 LCS_CALIBRATED_RGB,则它就是和 BITMAPV4HEADER 结构兼容的。相应的 bV5Endpoints 字段和伽玛字段必须是合理值。

        如果 bV5CSType 字段是 LCS_sRGB,则所有剩余的字段都不需要设置。这意味着这个 DIB 的色彩空间是微软和惠普提出的“标准”RGB色彩空间。这个标准色彩空间的目标是要提供一定的设备无关性,不要颜色配置文件,特别适用于因特网。http://www.color.org/contrib/sRGB.html 有它的详细文档。

        如果 bV5CSType 字段是 LCS_WINDOWS_COLOR_SPACE,那么所有剩余字段也都不需要设置。Windows 会使用显示位图的 API 函数所用到的色彩空间。

        如果 bV5CSType 字段是 PROFILE_EMBEDDED,那么说明 DIB 文件中包含一个 ICC 颜色配置文件。如果该字段是 PROFILE_LINKED,那么说明 DIB 文件
中包含一个 ICC 颜色配置文件的完整文件名。无论是这两种情况的哪一种,bV5ProfileData 都是从 BITMAPV5HEADER 的开头到颜色配置文件数据或文件名开始的位移量。bV5ProfileSize 字段给出了颜色配置文件数据或文件名的大小。不需要设置 bV5Endpoints 和伽玛字段。

15.1.10  显示 DIB 信息

        现在是时候来看一下代码了。我们还没有足够的知识来显示 DIB,但是我们至少可以从文件头结构中得到 DIB 的信息并把这些信息显示出来。DIBHEADS 程序就是为此设计的。

/*-------------------------------------------------DIBHEADS.C -- Displays DIB Header Information     (c) Charles Petzold, 1998--------------------------------------------------*/#include <windows.h>#include "resource.h"LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);TCHAR szAppName[] = TEXT("DibHeads");int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,PSTR szCmdLine, int iCmdShow){HACCEL hAccel;HWND hwnd;MSG msg;WNDCLASS wndclass;wndclass.style = CS_HREDRAW | CS_VREDRAW;wndclass.lpfnWndProc = WndProc;wndclass.cbClsExtra = 0;wndclass.cbWndExtra = 0;wndclass.hInstance = hInstance;wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);wndclass.lpszMenuName = szAppName;wndclass.lpszClassName = szAppName;if (!RegisterClass(&wndclass)){MessageBox(NULL, TEXT("This program requires Windows NT!"),szAppName, MB_ICONERROR);return 0;}hwnd = CreateWindow(szAppName, TEXT("DIB Headers"),WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL, hInstance, NULL);ShowWindow(hwnd, iCmdShow);UpdateWindow(hwnd);hAccel = LoadAccelerators(hInstance, szAppName);while (GetMessage(&msg, NULL, 0, 0)){if (!TranslateAccelerator(hwnd, hAccel, &msg)){TranslateMessage(&msg);DispatchMessage(&msg);}}return msg.wParam;}void Printf(HWND hwnd, TCHAR * szFormat, ...){TCHARszBuffer[1024];va_list pArgList;va_start(pArgList, szFormat);wvsprintf(szBuffer, szFormat, pArgList);va_end(pArgList);SendMessage(hwnd, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);SendMessage(hwnd, EM_REPLACESEL, FALSE, (LPARAM)szBuffer);SendMessage(hwnd, EM_SCROLLCARET, 0, 0);}void DisplayDibHeaders(HWND hwnd, TCHAR * szFileName){static TCHAR* szInfoName[] = { TEXT("BITMAPCOREHEADER"),   TEXT("BITMAPINFOHEADER"),   TEXT("BITMAPV4HEADER"),   TEXT("BITMAPV5HEADER") };static TCHAR* szCompression[] = { TEXT("BI_RGB"), TEXT("BI_RLE8"),  TEXT("BI_RLE4"),  TEXT("BI_BITFIELDS"),  TEXT("unknown") };BITMAPCOREHEADER * pbmch;BITMAPFILEHEADER * pbmfh;BITMAPV5HEADER * pbmih;BOOL bSuccess;DWORD dwFileSize, dwHighSize, dwBytesRead;HANDLE hFile;int i;PBYTE pFile;TCHAR * szV;// Display the file namePrintf(hwnd, TEXT("File: %s\r\n\r\n"), szFileName);// Open the filehFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL,OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);if (hFile == INVALID_HANDLE_VALUE){Printf(hwnd, TEXT("Cannot open file.\r\n\r\n"));return;}// Get the size of the filedwFileSize = GetFileSize(hFile, &dwHighSize);if (dwHighSize){Printf(hwnd, TEXT("Cannot deal with > 4G files.\r\n\r\n"));CloseHandle(hFile);return;}// Allocate memory for the filepFile = (PBYTE)malloc(dwFileSize);if (!pFile){Printf(hwnd, TEXT("Cannot allocate memory.\r\n\r\n"));CloseHandle(hFile);return;}// Read the fileSetCursor(LoadCursor(NULL, IDC_WAIT));ShowCursor(TRUE);bSuccess = ReadFile(hFile, pFile, dwFileSize, &dwBytesRead, NULL);ShowCursor(FALSE);SetCursor(LoadCursor(NULL, IDC_ARROW));if (!bSuccess || (dwBytesRead != dwFileSize)){Printf(hwnd, TEXT("Could not read file.\r\n\r\n"));CloseHandle(hFile);free(pFile);return;}// Close the fileCloseHandle(hFile);// Display file sizePrintf(hwnd, TEXT("File size = %u bytes\r\n\r\n"), dwFileSize);// Display BITMAPFILEHEADER structurepbmfh = (BITMAPFILEHEADER *)pFile;Printf(hwnd, TEXT("BITMAPFILEHEADER\r\n"));Printf(hwnd, TEXT("\t.bfType = 0x%X\r\n"), pbmfh->bfType);Printf(hwnd, TEXT("\t.bfSize = %u\r\n"), pbmfh->bfSize);Printf(hwnd, TEXT("\t.bfReserved1 = %u\r\n"), pbmfh->bfReserved1);Printf(hwnd, TEXT("\t.bfReserved2 = %u\r\n"), pbmfh->bfReserved2);Printf(hwnd, TEXT("\t.bfOffBits = %u\r\n\r\n"), pbmfh->bfOffBits);// Determine which information structure we havepbmih = (BITMAPV5HEADER *)(pFile + sizeof(BITMAPFILEHEADER));switch (pbmih->bV5Size){case sizeof(BITMAPCOREHEADER) : i = 0; szV = TEXT(""); break;case sizeof(BITMAPINFOHEADER) : i = 1; szV = TEXT("i"); break;case sizeof(BITMAPV4HEADER) :i = 2; szV = TEXT("V4"); break;case sizeof(BITMAPV5HEADER) :i = 3; szV = TEXT("V5"); break;default:Printf(hwnd, TEXT("Unknown header size of %u.\r\n\r\n"), pbmih->bV5Size);free(pFile);return;}Printf(hwnd, TEXT("%s\r\n"), szInfoName[i]);// Display the BITMAPCOREHEADER fieldsif (pbmih->bV5Size == sizeof(BITMAPCOREHEADER)){pbmch = (BITMAPCOREHEADER *)pbmih;Printf(hwnd, TEXT("\t.bcSize = %u\r\n"), pbmch->bcSize);Printf(hwnd, TEXT("\t.bcWidth = %u\r\n"), pbmch->bcWidth);Printf(hwnd, TEXT("\t.bcHeight = %u\r\n"), pbmch->bcHeight);Printf(hwnd, TEXT("\t.bcPlanes = %u\r\n"), pbmch->bcPlanes);Printf(hwnd, TEXT("\t.bcBitCount = %u\r\n\r\n"), pbmch->bcBitCount);free(pFile);return;}// Display the BITMAPINFOHEADER fieldsPrintf(hwnd, TEXT("\t.b%sSize = %u\r\n"), szV, pbmih->bV5Size);Printf(hwnd, TEXT("\t.b%sWidth = %i\r\n"), szV, pbmih->bV5Width);Printf(hwnd, TEXT("\t.b%sHeight = %i\r\n"), szV, pbmih->bV5Height);Printf(hwnd, TEXT("\t.b%sPlanes = %u\r\n"), szV, pbmih->bV5Planes);Printf(hwnd, TEXT("\t.b%sBitCount=%u\r\n"), szV, pbmih->bV5BitCount);Printf(hwnd, TEXT("\t.b%sCompression = %s\r\n"), szV,szCompression[min(4, pbmih->bV5Compression)]);Printf(hwnd, TEXT("\t.b%sSizeImage= %u\r\n"), szV,pbmih->bV5SizeImage);Printf(hwnd, TEXT("\t.b%sXPelsPerMeter = %i\r\n"), szV,pbmih->bV5XPelsPerMeter);Printf(hwnd, TEXT("\t.b%sYPelsPerMeter = %i\r\n"), szV,pbmih->bV5YPelsPerMeter);Printf(hwnd, TEXT("\t.b%sClrUsed = %i\r\n"), szV,pbmih->bV5ClrUsed);Printf(hwnd, TEXT("\t.b%sClrImportant = %i\r\n\r\n"), szV,pbmih->bV5ClrImportant);if (pbmih->bV5Size == sizeof(BITMAPINFOHEADER)){if (pbmih->bV5Compression == BI_BITFIELDS){Printf(hwnd, TEXT("Red Mask = %08X\r\n"),pbmih->bV5RedMask);Printf(hwnd, TEXT("Green Mask = %08X\r\n"),pbmih->bV5GreenMask);Printf(hwnd, TEXT("Blue Mask = %08X\r\n\r\n"),pbmih->bV5BlueMask);}free(pFile);return;}// Display additional BITMAPV4HEADER fieldsPrintf(hwnd, TEXT("\t.b%sRedMask = %08X\r\n"), szV,pbmih->bV5RedMask);Printf(hwnd, TEXT("\t.b%sGreenMask = %08X\r\n"), szV,pbmih->bV5GreenMask);Printf(hwnd, TEXT("\t.b%sBlueMask = %08X\r\n"), szV,pbmih->bV5BlueMask);Printf(hwnd, TEXT("\t.b%sAlphaMask = %08X\r\n"), szV,pbmih->bV5AlphaMask);Printf(hwnd, TEXT("\t.b%sCSType = %u\r\n"), szV,pbmih->bV5CSType);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzRed.ciexyzX = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzX);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzRed.ciexyzY = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzY);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzRed.ciexyzZ = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzZ);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzGreen.ciexyzX = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzGreen.ciexyzX);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzGreen.ciexyzY = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzGreen.ciexyzY);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzGreen.ciexyzZ = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzGreen.ciexyzZ);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzBlue.ciexyzX = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzBlue.ciexyzX);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzBlue.ciexyzY = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzBlue.ciexyzY);Printf(hwnd, TEXT("\t.b%sEndpoints.ciexyzBlue.ciexyzZ = %08X\r\n"),szV, pbmih->bV5Endpoints.ciexyzBlue.ciexyzZ);Printf(hwnd, TEXT("\t.b%sGammaRed = %08X\r\n"), szV,pbmih->bV5GammaRed);Printf(hwnd, TEXT("\t.b%sGammaGreen = %08X\r\n"), szV,pbmih->bV5GammaGreen);Printf(hwnd, TEXT("\t.b%sGammaBlue = %08X\r\n\r\n"), szV,pbmih->bV5GammaBlue);if (pbmih->bV5Size == sizeof(BITMAPV4HEADER)){free(pFile);return;}// Display additional BITMAPV5HEADER fieldsPrintf(hwnd, TEXT("\t.b%sIntent = %u\r\n"), szV,pbmih->bV5Intent);Printf(hwnd, TEXT("\t.b%sProfileData = %u\r\n"), szV,pbmih->bV5ProfileData);Printf(hwnd, TEXT("\t.b%sProfileSize = %u\r\n"), szV,pbmih->bV5ProfileSize);Printf(hwnd, TEXT("\t.b%sReserved = %u\r\n\r\n"), szV,pbmih->bV5Reserved);free(pFile);return;}LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){static HWNDhwndEdit;static OPENFILENAME ofn;static TCHARszFileName[MAX_PATH], szTitleName[MAX_PATH];static TCHARszFilter[] = TEXT("Bitmap Files (*.BMP)\0*.bmp\0") TEXT("All Files (*.*)\0*.*\0\0");switch (message){case WM_CREATE:hwndEdit = CreateWindow(TEXT("edit"), NULL,WS_CHILD | WS_VISIBLE | WS_BORDER |WS_VSCROLL | WS_HSCROLL |ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY,0, 0, 0, 0, hwnd, (HMENU)1,((LPCREATESTRUCT)lParam)->hInstance, NULL);ofn.lStructSize = sizeof(OPENFILENAME);ofn.hwndOwner = hwnd;ofn.hInstance = NULL;ofn.lpstrFilter = szFilter;ofn.lpstrCustomFilter = NULL;ofn.nMaxCustFilter = 0;ofn.nFilterIndex = 0;ofn.lpstrFile = szFileName;ofn.nMaxFile = MAX_PATH;ofn.lpstrFileTitle = szTitleName;ofn.nMaxFileTitle = MAX_PATH;ofn.lpstrInitialDir = NULL;ofn.lpstrTitle = NULL;ofn.Flags = 0;ofn.nFileOffset = 0;ofn.nFileExtension = 0;ofn.lpstrDefExt = TEXT("bmp");ofn.lCustData = 0;ofn.lpfnHook = NULL;ofn.lpTemplateName = NULL;return 0;case WM_SIZE:MoveWindow(hwndEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);return 0;case WM_COMMAND:switch (LOWORD(wParam)){case IDM_FILE_OPEN:if (GetOpenFileName(&ofn))DisplayDibHeaders(hwndEdit, szFileName);return 0;}break;case WM_DESTROY:PostQuitMessage(0);return 0;}return DefWindowProc(hwnd, message, wParam, lParam);}
DIBHEADS.RC (excerpts)// Microsoft Visual C++ generated resource script.//#include "resource.h"///////////////////////////////////////////////////////////////////////////////// Menu//DIBHEADS MENUBEGIN    POPUP "&File"    BEGIN        MENUITEM "&Open\tCtrl+O",                IDM_FILE_OPEN    ENDEND///////////////////////////////////////////////////////////////////////////////// Accelerator//DIBHEADS ACCELERATORSBEGIN    "O",            IDM_FILE_OPEN,          VIRTKEY, CONTROL, NOINVERTEND
RESOURCE.H (excerpts)// Microsoft Visual C++ 生成的包含文件。// 供 DIBHeads.rc 使用//#define IDM_FILE_OPEN                   40001

        这个程序有一个很短的 WndProc 函数,它创建了一个只读的编辑窗口,这个只读窗口填满了它的客户区,该函数还处理菜单的 File Open(打开文件)命令。它使用 GetOpenFileName 函数调用标准的 File Open 对话框,然后调用 DisplayDibHeaders 这个函数。这个函数把整个 DIB 文件读入内存,然后一个字段一个字段地显示所有文件头信息。

0 0