C语言生成BMP文件

来源:互联网 发布:票务网站源码 编辑:程序博客网 时间:2024/05/16 01:49

C 语言生成BMP 文件

针对这个话题其实可以分解为两个议题,一个是 BMP 文件的格式,一个是 C语言如何操作文件。

BMP 文件格式

BMP 是微软在 windows 系统中使用的一种位图图像格式,主要包含调色板图像和直接色图像两大类。

文件格式由文件头、信息头、调色板数据、图像数据四个部分构成。文件头区域包含文件的标识、文件大小和图像数据区偏移量等字段。信息头区域则包含图像宽度、高度、像素格式等信息。所有数据一般按小端字节序来存储,且数据块一般组织成4字节对齐。


图像数据区也不例外,按每行图像的数据字节,按4字节对齐。图像数据按行倒序存放,先存储最后一行图像数据,然后依次存放,直到第一行数据。这样设计,可能是为了从文件尾部往前读的时候,能够直接顺序读出图像数据吧。

备注:相较于windows画图程序,Photoshop保存的BMP文件,其图像数据区末尾多出两个0x00字节(图像数据区大小字段也大了2),可能是为了保证整个文件大小是4字节对齐。


使用调色板的位图图像,在其调色区域存储实际的颜色值,而在图像数据区域存储对调色板的索引值。根据调色板的数量,可以分为单色图像、16色图像和256色图像。使用直接色的位图图像,没有调色板区域,在图像数据区直接存储每行图像的每个像素的RGB颜色数据。根据颜色数据的格式,分为 BGR555、BGR888和 BGRA8888等格式。


本文中只是直接色的格式进行了说明和文件生成。

C 语言如何操作文件

在 C 语言的标准库中,有两大类文件操作接口,一类是比较原始的 open/close/seek/read/write 等接口,直接与系统调用相关联;一类是面向流的 fopen/fclose/fseek/fread/fwrite等接口,会在内部维护文件数据缓冲区,从而更加有效的进行系统调用。本文中采用面向流的文件接口进行文件数据读写。


下面是对这个问题的代码实现。

头文件和类型定义

#include <stdio.h>#include <stdlib.h>#include <string.h>typedef unsigned char  U1;typedef unsigned short U2;typedef unsigned long  U4;typedef unsigned char  BOOL;#define TRUE  (0xFFu)#define FALSE (0x00u)


字节序列化工具函数

以下函数用于将整型无符号数值,按小端字节序存储到字节序列中。
void vd_SerializeLittleEndianU2(U1 * u1_ap_serial, U2 u2_a_value){do{if(u1_ap_serial == NULL)break;u1_ap_serial[0] = (U1)u2_a_value;u1_ap_serial[1] = (U1)(u2_a_value >> 8);} while(FALSE);}void vd_SerializeLittleEndianU3(U1 * u1_ap_serial, U4 u4_a_value){do{if(u1_ap_serial == NULL)break;u1_ap_serial[0] = (U1)u4_a_value;u1_ap_serial[1] = (U1)(u4_a_value >> 8);u1_ap_serial[2] = (U1)(u4_a_value >> 16);} while(FALSE);}void vd_SerializeLittleEndianU4(U1 * u1_ap_serial, U4 u4_a_value){do{if(u1_ap_serial == NULL)break;u1_ap_serial[0] = (U1)u4_a_value;u1_ap_serial[1] = (U1)(u4_a_value >> 8);u1_ap_serial[2] = (U1)(u4_a_value >> 16);u1_ap_serial[3] = (U1)(u4_a_value >> 24);} while(FALSE);}

颜色转换辅助宏

下面的宏用来定义颜色数据,按 RGB 颜色的每个分量为0~255的颜色值,定义到32位整型类型。以及将32位颜色值,转换为16位的 RGB555颜色值。
// AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB// 76543210765432107654321076543210#define BMP_RGBA32(r,g,b,a)  (U4)( ((U4)(U1)(r)<<16) | ((U4)(U1)(g)<<8) | (U4)(U1)(b) | ((U4)(U1)(a)<<24) )#define BMP_RGB24(r,g,b)     (U4)( ((U4)(U1)(r)<<16) | ((U4)(U1)(g)<<8) | (U4)(U1)(b) )// XRRRRRGGGGGBBBBB// 0432104321043210#define BMP_RGBA32TOBMP16(c) (U2)( (((U4)(c)>>9) & 0x7C00u) | (((U4)(c)>>6) & 0x03E0u) | (((U4)(c)>>3) & 0x001F) )


内存中的图像数据

内存中的图像数据的定义和创建,其中的一个技巧是将结构体和图像数据区,在一次malloc 中动态分配的,这样释放内存时也减少了判断。

typedef struct {U4 u4_image_width;U4 u4_image_height;U4 u4_widthbyte;U4 u4_image_size;U2 u2_bitcount;U2 u2_palette_size;U1* u1_p_image_data;} ST_BITMAP;ST_BITMAP * st_g_CreateBitmap(U4 u4_a_width, U4 u4_a_height, U2 u2_a_bitcount){BOOL b_t_success = FALSE;ST_BITMAP * st_tp_bitmap = NULL;U4 u4_t_widthbyte = 0;U4 u4_t_imagesize = 0;do{if((u4_a_width == 0) || (u4_a_width > 4096))break;if((u4_a_height == 0) || (u4_a_height > 4096))break;if((u2_a_bitcount != 16) && (u2_a_bitcount != 24) && (u2_a_bitcount != 32))break;// 4-byte aligned bytes for a line of image datau4_t_widthbyte = (u4_a_width * u2_a_bitcount + 31) / 32 * 4;u4_t_imagesize = (u4_t_widthbyte * u4_a_height);st_tp_bitmap = malloc(sizeof(ST_BITMAP) + u4_t_imagesize); // alloc togetherif(st_tp_bitmap == NULL)break;memset(st_tp_bitmap, 0, sizeof(ST_BITMAP) + u4_t_imagesize);st_tp_bitmap->u4_image_width = u4_a_width;st_tp_bitmap->u4_image_height = u4_a_height;st_tp_bitmap->u4_widthbyte = u4_t_widthbyte;st_tp_bitmap->u4_image_size = u4_t_imagesize;st_tp_bitmap->u2_bitcount = u2_a_bitcount;st_tp_bitmap->u2_palette_size = 0;// pointer to the address next to the structst_tp_bitmap->u1_p_image_data = (U1 *)st_tp_bitmap + sizeof(ST_BITMAP);b_t_success = TRUE;} while(FALSE);return st_tp_bitmap;}


作为安全处理,在释放内存之前,先用 memset 清除了原来结构体中的数据,如果使用方在内存被释放后,继续访问该内存数据,出现 NULL 指针崩溃。

void vd_g_FreeBitmap(ST_BITMAP * st_ap_bitmap){BOOL b_t_success = FALSE;do{if(st_ap_bitmap == NULL)break;memset(st_ap_bitmap, 0, sizeof(ST_BITMAP));free(st_ap_bitmap);b_t_success = TRUE;} while(FALSE);}


保存图像数据到BMP文件

下面将内存中的图像数据保存到文件,其中的一个技巧是将文件头和信息头,预先放入了字节数组,除了 AA、BB直到 FF 的数据之外,其他的数据对于当前版本的 BMP 来说,都可以用默认值,其字段不需要特别处理。需要注意的是位图的图像数据是按图像行倒序的,所以写入到文件时,依次写入最后一行到第一行。如果要做读取 BMP 文件时,也需要注意这个问题。

void vd_g_SaveBitmap(const ST_BITMAP * st_ap_bitmap, const char * sz_ap_path){BOOL b_t_success = FALSE;U1 u1_tp_bitmap_header[] ={0x42, 0x4D, 0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0x00, //  2 AA->FileSize0x00, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0x28, 0x00, // 10 BB->OffBits0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xDD, 0xDD, // 18 CC->Width0xDD, 0xDD, 0x01, 0x00, 0xEE, 0xEE, 0x00, 0x00, // 22 DD->Height0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, // 28 EE->BitCount0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 34 FF->ImageSize0x00, 0x00, 0x00, 0x00, 0x00, 0x00};FILE *file_bitmap = NULL;U4 u4_t_y;U4 u4_t_pixel_offset = 0;do{if(st_ap_bitmap == NULL)break;if((st_ap_bitmap->u2_bitcount != 16) && (st_ap_bitmap->u2_bitcount != 24) && (st_ap_bitmap->u2_bitcount != 32))break;if(sz_ap_path == NULL)break;file_bitmap = fopen(sz_ap_path, "wb");if(file_bitmap == NULL)break;// set bitmap head infovd_SerializeLittleEndianU4(&u1_tp_bitmap_header[2], sizeof(u1_tp_bitmap_header) + st_ap_bitmap->u4_image_size);vd_SerializeLittleEndianU4(&u1_tp_bitmap_header[10], sizeof(u1_tp_bitmap_header));vd_SerializeLittleEndianU4(&u1_tp_bitmap_header[18], st_ap_bitmap->u4_image_width);vd_SerializeLittleEndianU4(&u1_tp_bitmap_header[22], st_ap_bitmap->u4_image_height);vd_SerializeLittleEndianU2(&u1_tp_bitmap_header[28], st_ap_bitmap->u2_bitcount);vd_SerializeLittleEndianU4(&u1_tp_bitmap_header[34], st_ap_bitmap->u4_image_size);// write bitmap file headfwrite(u1_tp_bitmap_header, sizeof(u1_tp_bitmap_header), 1L, file_bitmap);// write bitmap image data, bottom to topu4_t_pixel_offset = st_ap_bitmap->u4_image_height * st_ap_bitmap->u4_widthbyte;for(u4_t_y = 0; u4_t_y < st_ap_bitmap->u4_image_height; u4_t_y++){u4_t_pixel_offset -= st_ap_bitmap->u4_widthbyte;fwrite(&st_ap_bitmap->u1_p_image_data[u4_t_pixel_offset], st_ap_bitmap->u4_widthbyte, 1L, file_bitmap);}b_t_success = TRUE;} while(0);if(file_bitmap)fclose(file_bitmap);}


以图像数据为画布画点

设置内存中的图像数据,这里是简单的写入一个像素。可以在此基础上,结合计算机图形学的算法,进行画直线、画圆、椭圆、填充等。

void vd_SetBitmapPixel(ST_BITMAP * st_ap_bitmap, U4 u4_a_x, U4 u4_a_y, U4 u4_a_color){U4 u4_t_pixel_offset = 0;U2 u2_t_color = 0;do{if(st_ap_bitmap == NULL)break;if(u4_a_x >= st_ap_bitmap->u4_image_width)break;if(u4_a_y >= st_ap_bitmap->u4_image_height)break;u4_t_pixel_offset = u4_a_y * st_ap_bitmap->u4_widthbyte + u4_a_x * st_ap_bitmap->u2_bitcount / 8;switch(st_ap_bitmap->u2_bitcount){case 16:u2_t_color = BMP_RGBA32TOBMP16(u4_a_color);vd_SerializeLittleEndianU2(&st_ap_bitmap->u1_p_image_data[u4_t_pixel_offset], u2_t_color);break;case 24:vd_SerializeLittleEndianU3(&st_ap_bitmap->u1_p_image_data[u4_t_pixel_offset], u4_a_color);break;case 32:vd_SerializeLittleEndianU4(&st_ap_bitmap->u1_p_image_data[u4_t_pixel_offset], u4_a_color);break;default:break;}} while(FALSE);}


综合应用

最后以对函数的实际使用来举例,先创建位图,然后写入数据,最后保存成BMP图像文件。

int main(){ST_BITMAP * st_tp_bitmap = NULL;do{// create bitmap, size:20x10 format:RGB555->16// also support format RGB888->24 and RGBA8888->32st_tp_bitmap = st_g_CreateBitmap(20, 10, 16);if(st_tp_bitmap == NULL)break;// draw pixels on bitmapvd_SetBitmapPixel(st_tp_bitmap, 0, 0, BMP_RGB24(255, 0, 0)); // redvd_SetBitmapPixel(st_tp_bitmap, 1, 1, BMP_RGB24(0, 255, 0)); // greenvd_SetBitmapPixel(st_tp_bitmap, 2, 2, BMP_RGB24(0, 0, 255)); // blue// save to filevd_g_SaveBitmap(st_tp_bitmap, "test.bmp");} while(FALSE);if(st_tp_bitmap)vd_g_FreeBitmap(st_tp_bitmap);return 0;}


保存的图像文件如下所示,黑色的背景上,按红、绿、蓝填充了三个像素。


使用十六进制编辑器查看文件

我们还可以使用十六进制编辑器来查看这个文件,可以看到文件有42 4D 开头,也就是 BM的ASCII 编码,实际上也是作为图像格式的识别码而出现的。黑色折线条上方的是文件头和信息头,黑色线条下方的是图像数据区域。因为是生成的16位的直接色图像文件,没有并没有调色板数据。

总结

对于 BMP 位图格式而言,相对是比较简单的格式,可以用上面的代码进行处理。
代码主要演示了,在内存中的图像结构体定义和内存分配、释放,图像数据的修改,以及图像文件的生成。


扩展

对于 ICO 图标文件、ANI 图标动画文件,可以在一个文件中包含多个位图以及掩码图案,格式会复杂一些。而对于 PNG、JPG 这样的格式,则需要借助 libpng、libjpeg 和 zlib 等开源库代码进行读写了。

 
原创粉丝点击