BMP图片读写接口函数

我很早就学习了BMP位图。印象中,那时应该是在研究AVI视频文件格式时顺便研究的,或者是研究YUV转RGB时顺便研究的。但未写文章出来,我一直以为我的学习只有在发表了文章才算是完结,否则不能算是我做过了这个事。在这里补上当初读、写BMP的函数代码。

头文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* @file bmp_utils.h
* @author Late Lee
* @date 2012-7-2 13:21:53
* @brief
* BMP相关工具函数,目前只针对24位图片测试
*
* 1、在VS003及GCC下编译测试通过;
* 2、解决了BMP图片倒立、偏色、倾斜等问题。
* 3、BMP图像每行数据需要4字节对齐,即一行数据不足4的倍数,以0补。
* 解决此问题方法:设置2个变量:
* width_byte:实际的RGB每一行的字节数
* stride_byte:4字节对齐的每一行的字节数(已对齐时两者相等)
* 保存时,另外开辟一个考虑了4字节对齐的缓冲区,每拷贝一行数据(width_byte),
* 跳过stride_byte个字节,即跳到4字节对齐的下一行。
* 读取时,只读width_byte,并且跳过每行最后补的0。
* 4、图像倒立:读取与保存BMP时,将数据倒过来:
* 读取时,将读到的数据由下往上存放到缓冲区
* 保存时,将数据由下往上拷贝到缓冲区
* 5、偏色:BMP排序为BGR,将RGB数据的G、B调换位置即可。
* 6、倾斜:读取BMP时,未跳过补充的0。
*
* 笔记:
BMP图片结构,基中第1、第2部分占54字节,真彩色图没有第三部分
_______________________________
| BITMAPFILEHEADER |
|_______________________________|
| BITMAPINFOHEADER |
|_______________________________|
| n * RGBQUAD |
|_______________________________|
| image data |
|_______________________________|
*
对于2色位图,用1位表示该象素的颜色(一般0表示黑,1表示白),一个字节可以表示8个象素。调色板:2*4=8
对于16色位图,用4位表示一个象素的颜色,以一个字节可以表示2个象素。调色板:16*4=64
对于256色位图,一个字节表示1个象素。调色板:256*4=1024
对于真彩色图,三个字节表示1个象素。无调色板
* 单色BMP图:调色板占8字节,故头部占用54+8=62字节,后面为像素字节,
注意每行字节需要4字节对齐,
举例:16*16像素单色位图,一行占16/8 = 2字节,需要补2字节。
实际像素字节:16*16/2 = 32字节,补齐字节:2*16 = 32,共64字节
头部共62字节,故该图片总大小为64+62=126字节
*/

#ifndef _BMP_UTILS_H
#define _BMP_UTILS_H

#ifdef __cplusplus
extern "C" {
#endif

#ifdef WIN32
#include <Windows.h>
#else
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef long LONG;

#pragma pack(push)
// 2字节对齐,共14
#pragma pack(2)
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 文件类型, 0x4d42
DWORD bfSize; // 文件总大小
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits; // 实际位图数据偏移
} BITMAPFILEHEADER; //__attribute__ ((packed));

// 40
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本结构体长度
LONG biWidth; // 宽(单位像素)
LONG biHeight; // 高(单位像素)
WORD biPlanes; // 为1
WORD biBitCount; // 像素占用位数 1(2^1=2黑白二色), 4(2^4=16色),8(2^8=256色),24(真彩色),32
DWORD biCompression; // 压缩类型,不压缩:BI_RGB(0)
DWORD biSizeImage; // 位图数据大小,如果是不压缩类型,可以为0
LONG biXPelsPerMeter; // 水平分辨率,单位是每米的象素个数
LONG biYPelsPerMeter; // 垂直分辨率
DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数
DWORD biClrImportant; // 位图显示过程中重要的颜色数
} BITMAPINFOHEADER; //__attribute__ ((aligned(2)));

typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;

typedef struct tagBITMAPINFO{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO; // __attribute__ ((aligned(2)));

#pragma pack(pop)

#endif

#undef ALIGN
#define ALIGN(x, n) (((x)+(n)-1)&~((n)-1))

/**
* RGB互换R、B顺序
*
* @param[IN] rgb_buffer RGB缓冲区
* @param[IN] len 缓冲区大小
*
* @return none
*
* @note
* 缓冲区数据可以是RGB,也可以是BGR,该函数只是将B、G进行互换
*/
void swap_rgb(unsigned char* rgb_buffer, int len);

/**
* 分析BMP文件头部
*
* @param[IN] bmp_file BMP图片文件名称
*
* @return
* 0: 成功
* -1: 文件不存在或不是BMP文件
*/
int analyse_bmp_file(const char* bmp_file);

/**
* 读取BMP图片文件
*
* @param[IN] bmp_file BMP图片文件名称
*
* @param[OUT] rgb_buffer RGB数据(实际为BGR)
* @param[OUT] size RGB数据大小
* @param[OUT] width 图片宽
* @param[OUT] height 图片高
*
* @return
* 0:成功
* -1:读取文件失败,或不是BMP文件,或申请内存失败
* @note
* rgb_buffer为二级指针,内存由该函数分配,需要自行释放
* rgb_buffer数据排列顺序为BGR,因此,处理时可能需要转换成RGB顺序
*/
int read_bmp_file(const char* bmp_file, unsigned char** rgb_buffer,
int* size, int* width, int* height);

int read_bmp_file_1(const char* bmp_file, unsigned char** rgb_buffer, int* rgb_size,
unsigned char** palette_buf, int* palette_len,
int* width, int* height);
/**
* 保存BMP文件
*
* @param[IN] bmp_file BMP图片文件名称
*
* @param[IN] rgb_buffer RGB数据(实际为BGR)
* @param[IN] width 图片宽
* @param[IN] height 图片高
*
* @return
* 0:成功
* -1:打开文件失败
* @note
* BMP图片颜色分量实际为BGR,因此,需要事先将rgb_buffer数据排列顺序转换成BGR。
*/
int write_bmp_file(const char* bmp_file, unsigned char* rgb_buffer, int width, int height);

int write_bmp_file_1(const char* bmp_file, unsigned char* rgb_buffer,
unsigned char* palette_buf, int* palette_len,
int width, int height);
#ifdef __cplusplus
};
#endif

#endif /* _BMP_UTILS_H */

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <limits.h>
#include "bmp_utils.h"
//#include "debug.h"

// 注:只针对24位图片
int analyse_bmp_file(const char* bmp_file)
{
#if 0
FILE* fp;
BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
int rgb_size1 = 0;
int rgb_size2 = 0;
int width = 0;
int height = 0;
int padding = 0;
int stride_byte = 0;
int color_num = 0;
int paltette_len = 0;

char* palette = NULL;

fp = fopen(bmp_file, "rb");
if (fp == NULL)
{
printf("open file %s failed.\n", bmp_file);
return -1;
}

fread(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);

if (bmpHeader.bfType != (('M' << 8) | 'B'))
{
printf("Sorry, not bmp picture.\n");
return -1;
}

width = bmpInfo.biWidth;
height = (int)fabs((double)bmpInfo.biHeight);

switch(bmpInfo.biBitCount)
{
case 1:
color_num = 2;
break;
case 4:
color_num = 16;
break;
case 8:
color_num = 256;
break;
case 24:
default:
color_num = 0;
break;
}

stride_byte = ALIGN(width*bmpInfo.biBitCount/8, 4);
padding = stride_byte - width*bmpInfo.biBitCount/8;
paltette_len = color_num * sizeof(RGBQUAD);

rgb_size1 = bmpHeader.bfSize - sizeof(BITMAPFILEHEADER) - sizeof(BITMAPINFOHEADER) - paltette_len;
rgb_size2 = stride_byte*height;
// 打印结构体中每个成员
printf("file name: %s\n", bmp_file);
printf("file type: %c%c %x\n", (bmpHeader.bfType)>>8, (bmpHeader.bfType)&0xff, bmpHeader.bfType);
printf("file size: %d(B) = %0.2f(KB) = %0.2f(MB)\n", bmpHeader.bfSize, (float)bmpHeader.bfSize/1024.00, (float)bmpHeader.bfSize/1024.00/1024.00);
printf("offset of image data: %d\n", bmpHeader.bfOffBits);
//////////////////////////////////

printf("biSize: %d\n", bmpInfo.biSize);
printf("width: %d\n", bmpInfo.biWidth);
printf("height: %d\n", bmpInfo.biHeight);
printf("Plane: %d\n", bmpInfo.biPlanes);
printf("BitCount: %d\n", bmpInfo.biBitCount);
printf("biCompression: %d\n", bmpInfo.biCompression);
printf("biSizeImage: %d\n", bmpInfo.biSizeImage);
printf("XPelsPerMeter: %d\n", bmpInfo.biXPelsPerMeter);
printf("YPelsPerMeter: %d\n", bmpInfo.biYPelsPerMeter);
printf("biClrUsed: %d\n", bmpInfo.biClrUsed);
printf("biClrImportant: %d\n", bmpInfo.biClrImportant);

printf("width*3: %d stride byte: %d padding: %d\n", width*3, stride_byte, padding);

printf("rgb buffer size: %d %d\n", rgb_size1,rgb_size2);

if (color_num != 0)
{
palette = (char *)malloc(paltette_len * sizeof(char));
fread(palette, paltette_len, 1, fp);
printf("palette:\n");
//dump(palette, paltette_len);
}
#endif
return 0;
}

int read_bmp_file(const char* bmp_file, unsigned char** rgb_buffer,
int* size, int* width, int* height)
{
int ret = 0;
FILE* fp;
BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
int tmp_width = 0;
int tmp_height = 0;
int rgb_size = 0;
int stride_byte = 0; // 每行占用字节数(4字节对齐)
int width_byte = 0; // 每行真正有效字节数
int padding = 0; // 需要对齐的字节数
unsigned char* tmp_buf = 0;
int color_num = 0;
int palette_len = 0;
int i = 0;

fp = fopen(bmp_file, "rb");
if (fp == NULL)
{
printf("open file %s failed.\n", bmp_file);
return -1;
}

ret = fread(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
if (ret != sizeof(BITMAPFILEHEADER))
{
printf("read BITMAPFILEHEADER failed.\n");
return -1;
}

ret = fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);
if (ret != sizeof(BITMAPINFOHEADER))
{
printf("read BITMAPINFOHEADER failed read: %d %d.\n", ret, sizeof(BITMAPINFOHEADER));
return -1;
}

if (bmpHeader.bfType != (('M' << 8) | 'B'))
{
printf("Sorry, not bmp picture.\n");
return -1;
}
tmp_width = bmpInfo.biWidth;
tmp_height = (int)fabs((double)bmpInfo.biHeight); // 预防高为负数的情况

// 真正RGB数据大小
rgb_size = tmp_width * tmp_height * bmpInfo.biBitCount/8;

*width = tmp_width;
*height = tmp_height;
*size = rgb_size;
/**
* 每行占用字节数,与下式结果相同
* stride_byte = (width * bmpInfo.biBitCount/8+3)/4*4;
*/
stride_byte = ALIGN(tmp_width*bmpInfo.biBitCount/8, 4);
width_byte = tmp_width * bmpInfo.biBitCount/8;

/**
* 补齐字节,与下式结果相同
* padding = (4 - width * 3 % 4) % 4;
* 实现未使用
*/
padding = stride_byte - width_byte;

// 判断调色板
switch(bmpInfo.biBitCount)
{
case 1:
color_num = 2;
break;
case 4:
color_num = 16;
break;
case 8:
color_num = 256;
break;
case 24:
default:
color_num = 0;
break;
}
// todo:读取调色板
palette_len = color_num * sizeof (RGBQUAD);

// 计算偏移量与实际偏移量比较,如不等,颜色数出错
if (bmpHeader.bfOffBits != sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + palette_len)
{
return -1;
}

printf("debug--:\nfile size: %d rgb size: %d %d stride byte: %d padding: %d BitCount: %d\n",
(int)bmpHeader.bfSize, rgb_size, stride_byte*tmp_height, stride_byte, padding, bmpInfo.biBitCount);

if (color_num != 0)
{
// 跳到图像数据处
fseek(fp, palette_len, SEEK_CUR);
}

// 申请合适的内存
*rgb_buffer = (unsigned char *)malloc(sizeof(char) * rgb_size);
if (*rgb_buffer == NULL)
{
return -1;
}
// 将读取的数据倒着存放到缓冲区(即BMP图像第一行数据放到缓冲区最后一行,等等),
// 这样图像才是正常的,否则图像是倒立的
tmp_buf = *rgb_buffer + rgb_size;
for (i = 0; i < tmp_height; i++)
{
tmp_buf -= width_byte;
ret = fread(tmp_buf, 1, width_byte, fp);
if (ret != width_byte)
{
free(*rgb_buffer);
return -1;
}
fseek(fp, padding, SEEK_CUR);
}

#if 0
// 顺序读文件,读到的图像是倒立的
unsigned char* tmp_buf = *rgb_buffer;
size_t readByte = 0;
for (int i = 0; i < tmp_height; i++)
{
readByte += fread(tmp_buf, 1, width_byte, fp);
fseek(fp, padding, SEEK_CUR);
tmp_buf += width_byte;
}
#endif
return 0;
}

int write_bmp_file(const char* bmp_file, unsigned char* rgb_buffer, int width, int height)
{
#define BPP 24 // 目前只考虑24色位图

BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
FILE* fp = NULL;
int offset = 0;
int stride_byte = 0; // 每行占用字节数(4字节对齐)
int width_byte = 0; // 每行真正有效字节数
int rgb_size = 0;
int padding = 0;
unsigned char* tmp_buf = NULL;
int i = 0;

fp = fopen(bmp_file, "wb");
if (fp == NULL)
{
printf("open %s failed\n", bmp_file);
return -1;
}

offset = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER); //54字节
// 4字节对齐 ((width * 24 + 31) / 32) * 4
// 如已经对齐,则rowStride与实际宽一致,如不对齐rowStride会比宽大一些
// stride_byte = ((width * 24 + 31) >> 5) << 2;
stride_byte = ALIGN(width*BPP/8, 4);
width_byte = width*BPP/8;
rgb_size = stride_byte * height; // 已考虑对齐

bmpHeader.bfType = ('M' << 8) | 'B';
bmpHeader.bfSize = offset + rgb_size; // BMP文件总大小
bmpHeader.bfReserved1 = 0;
bmpHeader.bfReserved2 = 0;
bmpHeader.bfOffBits = offset;

bmpInfo.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.biWidth = width;
bmpInfo.biHeight = height;
bmpInfo.biPlanes = 1;
bmpInfo.biBitCount = BPP;
bmpInfo.biCompression = 0;
bmpInfo.biSizeImage = rgb_size;
bmpInfo.biXPelsPerMeter = 0;
bmpInfo.biYPelsPerMeter = 0;
bmpInfo.biClrUsed = 0;
bmpInfo.biClrImportant = 0;

// 需要填充字节,BMP要求每一行数据必须4字节对齐,不足以0补。
//padding = (4 - width * 3 % 4) % 4;
// 实际未使用到
padding = stride_byte - width_byte;

printf("debug--:\nwidth: %d height: %d padding: %d rgb_size: %d, stride_byte: %d\n",
width, height, padding, rgb_size, stride_byte);
tmp_buf = (unsigned char *)malloc(sizeof(char) * rgb_size);
if (tmp_buf == NULL)
{
return -1;
}
memset(tmp_buf, '\0', sizeof(char) * rgb_size);
// 倒着拷贝到缓冲区
for (i = 0; i < height; i++)
{
// 每一行的实际数据为width * 3(R、G、B)
memcpy(tmp_buf + i * stride_byte, rgb_buffer + (height - i - 1) * width_byte, width_byte);
}

fwrite(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
fwrite(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);
fwrite(tmp_buf, 1, rgb_size, fp);

free(tmp_buf);

return 0;
}

// rgb --> bgr or
// bgr --> rgb
void swap_rgb(unsigned char* rgb_buffer, int len)
{
int i = 0;
for (i = 0; i < len; i += 3)
{
unsigned char tmp;
tmp = rgb_buffer[i];
rgb_buffer[i] = rgb_buffer[i + 2];
rgb_buffer[i + 1] = rgb_buffer[i + 1];
rgb_buffer[i + 2] = tmp;
}
}

说明: 最后写的那个RGB交换函数,是因为当初未了解libjpeg解压jpeg可以选择RGB的格式,以为只有是RGB,但BMP却只认BGR,因此就自己写了个R、B交换函数。但libjpeg本身是支持BGR的格式的。
看了几年前自己写的代码,觉得能写Doxygen风格的注释很不容易,希望自己能坚持下去,而不用理会其它的影响。

李迟 2015.7.9