针对这个话题其实可以分解为两个议题,一个是 BMP 文件的格式,一个是 C语言如何操作文件。
BMP 是微软在 windows 系统中使用的一种位图图像格式,主要包含调色板图像和直接色图像两大类。
文件格式由文件头、信息头、调色板数据、图像数据四个部分构成。文件头区域包含文件的标识、文件大小和图像数据区偏移量等字段。信息头区域则包含图像宽度、高度、像素格式等信息。所有数据一般按小端字节序来存储,且数据块一般组织成4字节对齐。
图像数据区也不例外,按每行图像的数据字节,按4字节对齐。图像数据按行倒序存放,先存储最后一行图像数据,然后依次存放,直到第一行数据。这样设计,可能是为了从文件尾部往前读的时候,能够直接顺序读出图像数据吧。
备注:相较于windows画图程序,Photoshop保存的BMP文件,其图像数据区末尾多出两个0x00字节(图像数据区大小字段也大了2),可能是为了保证整个文件大小是4字节对齐。
使用调色板的位图图像,在其调色区域存储实际的颜色值,而在图像数据区域存储对调色板的索引值。根据调色板的数量,可以分为单色图像、16色图像和256色图像。使用直接色的位图图像,没有调色板区域,在图像数据区直接存储每行图像的每个像素的RGB颜色数据。根据颜色数据的格式,分为 BGR555、BGR888和 BGRA8888等格式。
本文中只是直接色的格式进行了说明和文件生成。
在 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);
}
// 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);
}
下面将内存中的图像数据保存到文件,其中的一个技巧是将文件头和信息头,预先放入了字节数组,除了 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;
}
保存的图像文件如下所示,黑色的背景上,按红、绿、蓝填充了三个像素。