C语言生成BMP文件

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 
#include 
#include 

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 data
		u4_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 together
		if(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 struct
		st_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->FileSize
		0x00, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0x28, 0x00, // 10 BB->OffBits
		0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xDD, 0xDD, // 18 CC->Width
		0xDD, 0xDD, 0x01, 0x00, 0xEE, 0xEE, 0x00, 0x00, // 22 DD->Height
		0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, // 28 EE->BitCount
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 34 FF->ImageSize
		0x00, 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 info
		vd_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 head
		fwrite(u1_tp_bitmap_header, sizeof(u1_tp_bitmap_header), 1L, file_bitmap);
		
		// write bitmap image data, bottom to top
		u4_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->32
		st_tp_bitmap = st_g_CreateBitmap(20, 10, 16);
		if(st_tp_bitmap == NULL)
			break;

		// draw pixels on bitmap
		vd_SetBitmapPixel(st_tp_bitmap, 0, 0, BMP_RGB24(255, 0, 0)); // red
		vd_SetBitmapPixel(st_tp_bitmap, 1, 1, BMP_RGB24(0, 255, 0)); // green
		vd_SetBitmapPixel(st_tp_bitmap, 2, 2, BMP_RGB24(0, 0, 255)); // blue

		// save to file
		vd_g_SaveBitmap(st_tp_bitmap, "test.bmp");
		
	} while(FALSE);
	
	if(st_tp_bitmap)
		vd_g_FreeBitmap(st_tp_bitmap);
	
	return 0;
}


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

C语言生成BMP文件_第1张图片


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

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

总结

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


扩展

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

 
 

你可能感兴趣的:(C语言技巧,BMP文件技巧,嵌入式开发)