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


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


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

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

总结

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


扩展

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


                
  • 14
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
以下是一个简单的生成 BMP 文件的 C 语言代码,它可以生成一个 24 位色的 BMP 文件。 ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #pragma pack(push, 1) typedef struct { unsigned char signature[2]; unsigned int size; unsigned int reserved; unsigned int offset; } BMPHeader; typedef struct { unsigned int size; int width; int height; unsigned short planes; unsigned short bitsPerPixel; unsigned int compression; unsigned int imageSize; int xResolution; int yResolution; unsigned int colorsUsed; unsigned int importantColors; } BMPInfoHeader; #pragma pack(pop) void writeBMP(char* filename, unsigned char* data, int width, int height) { BMPHeader header; BMPInfoHeader infoHeader; FILE* file = fopen(filename, "wb"); int rowSize = (width * 3 + 3) & ~3; int pixelArraySize = rowSize * height; header.signature[0] = 'B'; header.signature[1] = 'M'; header.size = sizeof(BMPHeader) + sizeof(BMPInfoHeader) + pixelArraySize; header.reserved = 0; header.offset = sizeof(BMPHeader) + sizeof(BMPInfoHeader); infoHeader.size = sizeof(BMPInfoHeader); infoHeader.width = width; infoHeader.height = height; infoHeader.planes = 1; infoHeader.bitsPerPixel = 24; infoHeader.compression = 0; infoHeader.imageSize = pixelArraySize; infoHeader.xResolution = 0; infoHeader.yResolution = 0; infoHeader.colorsUsed = 0; infoHeader.importantColors = 0; fwrite(&header, sizeof(header), 1, file); fwrite(&infoHeader, sizeof(infoHeader), 1, file); for (int y = height - 1; y >= 0; y--) { int rowOffset = y * width * 3; for (int x = 0; x < width; x++) { int pixelOffset = rowOffset + x * 3; unsigned char b = data[pixelOffset]; unsigned char g = data[pixelOffset + 1]; unsigned char r = data[pixelOffset + 2]; fputc(b, file); fputc(g, file); fputc(r, file); } for (int i = 0; i < rowSize - width * 3; i++) fputc(0, file); } fclose(file); } int main() { int width = 256; int height = 256; unsigned char* data = (unsigned char*)malloc(width * height * 3); for (int y = 0; y < height; y++) { int rowOffset = y * width * 3; for (int x = 0; x < width; x++) { int pixelOffset = rowOffset + x * 3; data[pixelOffset] = x & 0xFF; data[pixelOffset + 1] = y & 0xFF; data[pixelOffset + 2] = (x + y) & 0xFF; } } writeBMP("output.bmp", data, width, height); free(data); return 0; } ``` 这个代码生成了一个 256x256 像素的 BMP 文件,每个像素的颜色由其 x 和 y 坐标计算得出。你可以根据需要修改这个代码来生成其他大小、颜色和内容的 BMP 文件
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值