BMP文件格式

  读写BMP格式的图片需要首先了解BMP图片的存储格式。可以参考维基百科上的介绍。
  BMP文件主要有文件头(File Header)、信息头(DIB Header)、调色板(Color Table)和像素阵列(Pixel Array)组成。大部分情况下我们需要用的就是每个像素的数据。
  可以从图中观察到,文件头中的File Offset to PixelArray可以直接得知Pixel Array的位置。信息头中有图像的宽、高和位深度的信息。像素阵列中的数据是一行一行组织的,每行的长度都是4字节的整数倍,如果一行的像素大小不是4字节的整数倍,还会再后面加Padding。

BMP读写

  了解了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
/**
* @file bmpRw.h
* @author Jiandong Qiu (1335521934@qq.com)
* @brief
* @version 0.1
* @date 2022-10-07
*
* @copyright Copyright (c) 2022
*
*/

#ifndef _BMPRW_H_
#define _BMPRW_H_

#ifdef __cplusplus
extern "C" {
#endif

/**
* @brief read bmp file, read data array into pData
* user should allocate space outsize of function
*
* @param fileName bmp file name
* @param pData pre-allocated data pointer
* @return int return 0 for OK
*/
int readBmp(char *fileName, void *pData);

/**
* @brief write bmp file, user should prepare dstInfo and dstHead before
* call this function, this function only support dedicated type of bmp file.
*
* @param fileName bmp file name
* @param pData bmp data
* @return int return 0 for OK
*/
int writeBmp(char *fileName, void *pData);

#ifdef __cplusplus
}
#endif

#endif

  读BMP文件的函数可以通过指定图片名,将图片中的像素数据读到一个指针指向的地址空间中。用户需要确保这个地址空间足够大。
  写BMP文件的函数同样需要由用户指定文件名,然后将一个指针指向的一块连续的数据写到BMP文件中,考虑到实际使用中图片的分辨率一般不会发生变化,所以这个写BMP文件的函数只支持特定的分辨率大小,文件信息头是由固定的常量给出的。根据实际使用情况需要对源文件中的文件信息头dstInfo进行修改。
  下面是源文件的实现:

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
/**
* @file bmpRw.c
* @author Jiandong Qiu (1335521934@qq.com)
* @brief
* @version 0.1
* @date 2022-10-07
*
* @copyright Copyright (c) 2022
*
*/

#include "bmpRw.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>

typedef struct __attribute__((packed)) BITMAPFILEHEADER
{
uint16_t bfType;
uint32_t bfSize;
uint16_t bfReserved1;
uint16_t bfReserved2;
uint32_t bfOffBits;
}BITMAPFILEHEADER;

typedef struct __attribute__((packed)) BITMAPINFOHEADER
{
uint32_t biSize;
uint32_t biWidth;
uint32_t biHeight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
uint32_t biXPelsPerMeter;
uint32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
}BITMAPINFOHEADER;

const BITMAPFILEHEADER dstHead = {
.bfType = 19778,
.bfSize = 766136,
.bfReserved1 = 0,
.bfReserved2 = 0,
.bfOffBits = 54};

const BITMAPINFOHEADER dstInfo = {
.biSize = 40,
.biWidth = 639,
.biHeight = 399,
.biPlanes = 1,
.biBitCount = 24,
.biCompression = 0,
.biSizeImage = 766082,
.biXPelsPerMeter = 3779,
.biYPelsPerMeter = 3779,
.biClrUsed = 0,
.biClrImportant = 0};

/**
* @brief read bmp file, read data array into pData
* user should allocate space outsize of function
*
* @param fileName bmp file name
* @param pData pre-allocated data pointer
* @return int return 0 for OK
*/
int readBmp(char *fileName, void *pData)
{
if(!fileName || !pData){
return -1;
}
int i;
BITMAPFILEHEADER head;
BITMAPINFOHEADER info;
size_t nElemSize;
size_t nPaddingSize;

FILE *fp = fopen(fileName, "rb");
if (fp == NULL){
return -1;
}

fread(&head, sizeof(BITMAPFILEHEADER), 1, fp);
fread(&info, sizeof(BITMAPINFOHEADER), 1, fp);

nElemSize = info.biBitCount / 8;
nPaddingSize = ((info.biWidth * nElemSize + 3) & (size_t)-4) - info.biWidth * nElemSize;

// move fp to data array
fseek(fp, head.bfOffBits, SEEK_SET);
for (i = info.biHeight - 1; i >= 0; --i){
fread(pData + i * info.biWidth * nElemSize, nElemSize, info.biWidth, fp);
// skip padding
fseek(fp, nPaddingSize, SEEK_CUR);
}

fclose(fp);
return 0;
}

/**
* @brief write bmp file, user should prepare dstInfo and dstHead before
* call this function, this function only support dedicated type of bmp file.
*
* @param fileName bmp file name
* @param pData bmp data
* @return int return 0 for OK
*/
int writeBmp(char *fileName, void *pData)
{
if (!fileName || !pData) {
return -1;
}

int i;
size_t nElemSize;
size_t nPaddingSize;
FILE *fp = fopen(fileName, "wb");
if (fp == NULL) {
return -1;
}

fwrite(&dstHead, 1, sizeof(BITMAPFILEHEADER), fp);
fwrite(&dstInfo, 1, sizeof(BITMAPINFOHEADER), fp);

nElemSize = dstInfo.biBitCount / 8;
nPaddingSize = ((dstInfo.biWidth * nElemSize + 3) & (size_t)-4) - dstInfo.biWidth * nElemSize;
uint8_t *pPadding = (uint8_t *)malloc(sizeof(uint8_t) * nPaddingSize);
if(!pPadding)
return -1;
memset(pPadding, 0, sizeof(uint8_t) * nPaddingSize);

for (i = dstInfo.biHeight - 1; i >= 0; --i) {
fwrite(pData + i * dstInfo.biWidth * nElemSize, nElemSize, dstInfo.biWidth, fp);
// add padding
fwrite(pPadding, 1, nPaddingSize, fp);
}

fclose(fp);

free(pPadding);
pPadding = NULL;

return 0;
}

  需要注意的地方:

  • 结构体类型定义需要带上packed属性,这样结构体中的字段才能和真正的BMP文件存储格式对应上;
  • 读写每行像素时,习惯上的第一行,实际上是存在文件中的最后一行,所以在进行读写操作时,循环变量i是从大到小变化;
  • 每行像素有可能会有padding,所以在读写的时候也需要对padding进行处理。
  • 写BMP文件时,要确认文件信息头是否正确。在不知道文件信息头应该是什么样的情况时,可以先读一张同样大小的图片,然后把信息头保存下来。

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
/**
* @file main.c
* @author Jiandong Qiu (1335521934@qq.com)
* @brief
* @version 0.1
* @date 2022-10-07
*
* @copyright Copyright (c) 2022
*
*/

#include "bmpRw.h"
#include <stdint.h>
#include <stdlib.h>

typedef uint8_t Pixel[3];

#define WIDTH (639)
#define HEIGHT (399)

int main()
{
Pixel *pData = (Pixel *)malloc(sizeof(Pixel) * WIDTH * HEIGHT);
if(!pData)
return -1;
readBmp("Test.bmp", pData);
writeBmp("Out.bmp", pData);

return 0;
}

  最后在windows和Ubuntu下分别对639x399的RGB BMP图片进行读写测试。程序能够读取图片数据并正确写回。

Windows MinGW gcc

Ubuntu 环境信息

测试结果: