BMP 图片格式及代码实现
介绍
BMP(Bitmap-File)图形文件是 Windows 采用的图形文件格式,以 RGB 为基础存储图像数据。这种格式特点是包含图像信息较丰富,几乎不进行压缩,但因此占用磁盘空间也会过大。
文件结构
位图文件可看成由4个部分组成:图像文件头、图像信息头、调色板和图像数据。
图像文件头与图像信息头是每个文件都会有的,文件结构表可参考下表:
偏移量 | 字段说明 | 占用空间 | 值说明 | |
---|---|---|---|---|
图像文件头 | 0000h | 位图类型 | 2 bytes | ‘BM’ :Windows 3.1x,95,NT,… ‘BA’:OS/2 Bitmap Array ‘CI’ :OS/2 Color Icon ‘CP’ :OS/2 Color Pointer ‘IC’ :OS/2 Icon ‘PT’ :OS/2 Pointe 除 ‘BM’ 外其它可不考虑 |
0002h | 整个文件大小 | 4 bytes | ||
0006h | 保留 | 4 bytes | 保留位,必须设置为0 | |
000Ah | 位图数据距离文件开始的偏移量 | 4 bytes | ||
图像信息头 | 000Eh | 位图信息头长度 | 4 bytes | 28h - Windows 3.1x,95,NT,… 0Ch - OS/2 1.x F0h - OS/2 2.x |
0012h | 位图宽度 | 4 bytes | 单位:像素 | |
0016h | 位图高度 | 4 bytes | 单位:像素 | |
001Ah | 位图位面数 | 2 bytes | 该值总是 1 | |
001Ch | 每个像素位数 | 2 bytes | 1 - 单色位图(实际上可有两种颜色,缺省情况下是黑色和白色。你可以自己定义这两种颜色) 4 - 16 色位图 8 - 256 色位图 16 - 16bit 高彩色位图 24 - 24bit真彩色位图 32 - 32bit 增强型真彩色位图 其中,1, 4, 8为索引模式,需要有调色板 | |
001Eh | 压缩方式 | 4 bytes | 0 - 不压缩 (使用BI_RGB表示) 1 - RLE 8-使用8位RLE压缩方式(用BI_RLE8表示) 2 - RLE 4-使用4位RLE压缩方式(用BI_RLE4表示) 3 – Bitfields-位域存放方式(用BI_BITFIELDS表示) | |
0022h | 位图图像数据的大小 | 4 bytes | 该数必须是 4 的倍数 | |
0026h | 水平分辨率 | 4 bytes | 单位:像素/米 | |
002Ah | 垂直分辨率 | 4 bytes | 单位:像素/米 | |
002Eh | 位图使用的颜色数 | 4 bytes | ||
0032h | 指定重要的颜色数 | 4 bytes | 当该域的值等于颜色数时(或者等于0时),表示所有颜色都一样重要 |
图像文件头
图像文件头只有四个字段,文件类型,文件大小,保留字段和文件图像数据的偏移量。
代码定义:
#define uint8 unsigned char
#define uint16 unsigned short
#define uint32 unsigned int
//文件头
typedef struct {
uint16 bfHeader; //文件类型,'BM',其它可忽略
uint32 bfSize; //文件大小
uint32 bfReserved; //保留字段 ,必须设置为0
uint32 bfOffset; //从文件开始到位图数据的偏移量
}__attribute__((packed)) bmpFileHeader;
文件类型用于判定图像格式。
文件大小记录整个文件字节数。
文件图像数据的偏移量可以用来定位图像数据的位置。
图像信息头
图像信息头包含了图像的一些信息,包括图像宽度,图像长度,每像素使用的位数,压缩方法等信息。
代码实现:
//图像信息头
typedef struct {
uint32 biSize; //位图信息头长度
uint32 biWidth; //位图宽度(像素)
uint32 biHeight; //位图长度(像素)
uint16 biPlanes; //位图位面数,该值总是为1
uint16 biBitsPerPixel; //每个像素的位数
uint32 biCompression;
uint32 biImgSize; //位图数据大小
uint32 biHResolution; //水平分辨率(像素/米)
uint32 biVResolution; //垂直分辨率(像素/米)
uint32 biUsedColors; //位图使用的颜色数,如果为0,则颜色数为2的biBitsPerPixel次方
uint32 biImportantColors; //指定重要的颜色数,如果为0,则代表所有颜色一样重要
}__attribute__((packed)) bmpImgHeader;
其中:
1). 图像信息头长度记录了信息头的长度,一般为 0x28h,但在一些早期系统或新的 BMP 文件格式中,其信息头结构变化较大,因此最好从文件中读取该信息头大小信息。
2). 位图宽度,指位图中每行包含多少像素。位图是以行为单位存储的,每行占用的字节数为(位图宽度 * 每个像素的位数 + 7) / 8 。需要注意的是,bmp 图片在存储时会进行行对齐,即每行按四字节对齐,当每行占用的字节数不够四的倍数时,会在每行后面补齐缺少的字节数,以 0 填充。例:一个宽度为 1001 像素的单色位图,每个像素位数为 1,则每行占用的字节数为 (1001 * 1 + 7) / 8 = 126 字节,此时每行不够四字节的倍数,则添加两个字节的 0,补到 128 字节。
获取每行字节数:
//width为每行的宽度,单位为像素,type为每像素占用的位数类型,参见 4)
static uint32 bmpGetLineAlignBytes(uint32 width, eBitsPerPixel type)
{
uint32 lineBytes = ((width * type + 31) / 32) * 4;
return lineBytes;
}
3). 位图高度,一般指位图有多少行。注意:当值为正时表示位图从下往上存储,左下角是起点。为负时,表示从上往下存储,左上角是起点。
4). 每像素占用的位数,当像素位数为 1, 4, 8 时,类型分别为 RGB1, RGB4, RGB8,值为调色板的索引(见认识RGB)。类型定义:
//每像素占用的位数类型
typedef enum {
E_RGB1 = 1,
E_RGB4 = 4,
E_RGB8 = 8,
E_RGB16 = 16,
E_RGB24 = 24,
E_RGB32 = 32
}eBitsPerPixel;
5). 压缩方法。压缩方法有无压缩,BI_RLE8, BI_RLE4 和 BI_BITFIELDS 压缩方法,这里只关注无压缩的方式。其它压缩方式可参见位图压缩
//压缩方式
#define BI_RGB 0 //BI_RGB:没有压缩;
#define BI_RLE8 1 //BI_RLE8:每个像素 8 比特的RLE压缩编码,压缩格式由 2 字节组成(重复像素计数和颜色索引);
#define BI_RLE4 2 //BI_RLE4:每个像素 4 比特的RLE压缩编码,压缩格式由 2 字节组成
#define BI_BITFIELDS 3 //BI_BITFIELDS:每个像素的比特由指定的掩码决定。
调色板
图像信息头中每个像素位数为 1, 4, 8 时,图像中需要有调色板数据。调色板数据是一个 RGB 颜色数组,每个颜色包含红绿蓝分量及一个保留字节,数组包含的颜色个数为 21,24,28。
调色板数据定义:
typedef struct {
uint8 blue; //蓝
uint8 green; //绿
uint8 red; //红
uint8 reserved; //保留
}colorPanelMeta;
需要调色版与不带调色板的图像文件头对比:
24 位位图不需要调色板,文件头中图像数据偏移量一般为 0x36h,而需要调色板的图像数据偏移量会比 0x36h 大一个存储调色板的数据:
256 色位图需要调色板,调色板占用空间为 28 * 4 bytes = 0x400h,所以图像数据偏移量为0x436h:
图像数据
图像数据以行来存储,每行按四字节对齐,位图高度就是图像有多少行。
读取 bmp 文件
结合之前定义的数据结构来读取 bmp 文件:
#define BMP_OK (0)
#define BMP_ERR (1)
#define BMP_FILE_ERR (2)
//图像头信息组合
typedef struct {
bmpFileHeader fileHeader;
bmpImgHeader imgHeader;
colorPanelMeta colorPanel[256];
uint32 metaCount;
}__attribute__((packed)) bmpHeader;
读取文件头:
//初始化调色板
static uint32 bmpInitColorPanel(colorPanelMeta *panelTable, uint32 *panelCount, uint16 bitsPerPixel, uint32 imgHeaderSize, FILE *fp)
{
uint32 offset = 0;
if (bitsPerPixel >= E_RGB16){
*panelCount = 0;
return 0;
}
fseek(fp, sizeof(bmpFileHeader) + imgHeaderSize, SEEK_SET);
*panelCount = (bitsPerPixel == E_RGB1 ? 2 : (bitsPerPixel == E_RGB4 ? 16 : 256));
colorPanelMeta *p = panelTable;
for (uint32 i = 0; i < *panelCount; i++) {
offset += fread(p, 1, sizeof(colorPanelMeta), fp);
p ++; //= sizeof(colorPanelMeta);
}
return offset;
}
uint32 bmpReadHeader(bmpHeader *header, FILE *fp)
{
uint32 offset = 0;
//置文件指针到文件开头,读取图像文件头和图像信息头
rewind(fp);
offset = fread(header, 1, sizeof(bmpFileHeader) + sizeof(bmpImgHeader), fp);
//读取调色板信息
header->metaCount = 0;
offset += bmpInitColorPanel(&header->colorPanel[0], &header->metaCount, header->imgHeader.biBitsPerPixel, header->imgHeader.biSize, fp);
return offset;
}
单/16/256 色 Bitmap 文件转 24 位 Bitmap 文件
根据 BMP 文件结构可以实现单色,16 色,256 色 bmp 文件转为 24 位 bmp 文件方法:
//buffer为每行的数据,pixelIndex为当前行的第 pixelIndex 个像素
//函数用于获取第 pixelIndex 像素在调色板中的索引。
static uint32 bmpGetColorPanelIndex(uint8 *buffer, uint32 pixelIndex, eBitsPerPixel type)
{
uint32 index = (pixelIndex + (8 / type - 1)) / (8 / type);
uint8 input = buffer[index];
uint8 unit = 0x01;
switch (type) {
case E_RGB1:
unit = 0x01;
break;
case E_RGB4:
unit = 0x0f;
break;
case E_RGB8:
unit = 0xff;
break;
default:
break;
}
uint32 shift = (8 - ((pixelIndex % (8 / type)) * type)) % 8;
return ((input >> shift) & unit);
}
//将调色板像素转为 RGB24 像素
static inline uint32 colorChangeRGB24(colorPanelMeta *meta, uint8 *output)
{
output[0] = meta->blue;
output[1] = meta->green;
output[2] = meta->red;
return 3;
}
uint32 bmpFileTransformRGB24(char *srcFile, char *destFile)
{
if (srcFile == NULL || destFile == NULL)
return BMP_FILE_ERR;
FILE *srcFp = fopen(srcFile, "rb");
FILE *dstFp = fopen(destFile, "wb");
if (srcFp == NULL || dstFp == NULL)
return BMP_FILE_ERR;
bmpHeader srcHeader;
bmpReadHeader(&srcHeader, srcFp);
if (srcHeader.imgHeader.biBitsPerPixel >= E_RGB16) {
fclose(dstFp);
fclose(srcFp);
return BMP_OK;
}
uint32 fileSize = 0;
uint32 imgSize = 0;
bmpHeader dstHeader;
dstHeader.fileHeader = srcHeader.fileHeader;
dstHeader.imgHeader = srcHeader.imgHeader;
dstHeader.fileHeader.bfOffset = sizeof(bmpFileHeader) + sizeof(bmpImgHeader);
dstHeader.imgHeader.biSize = sizeof(bmpImgHeader);
dstHeader.imgHeader.biBitsPerPixel = E_RGB24;
dstHeader.imgHeader.biCompression = BI_RGB;
fileSize = sizeof(bmpFileHeader) + sizeof(bmpImgHeader);
//先写入文件头用于占位,后续再修改文件头相关信息
fwrite(&dstHeader, 1, fileSize, dstFp);
fseek(srcFp, srcHeader.fileHeader.bfOffset, SEEK_SET);
uint32 srcLineBytes = bmpGetLineAlignBytes(srcHeader.imgHeader.biWidth, srcHeader.imgHeader.biBitsPerPixel);
uint8 buffer[srcLineBytes];
uint32 dstLineBytes = bmpGetLineAlignBytes(srcHeader.imgHeader.biWidth, E_RGB24);
uint8 *writeBuf = malloc(dstLineBytes);
if (writeBuf == NULL) {
fclose(dstFp);
fclose(srcFp);
return BMP_OK;
}
//每次读取一行,写入新文件,注意bmp文件每行会按四字节对齐,不够四字节以0补齐
while (!feof(srcFp)) {
uint32 readBytes = fread(buffer, 1, srcLineBytes, srcFp);
if (readBytes < srcLineBytes) {
break;
}
uint32 bufOffSize = 0;
for (uint32 i = 0; i < srcHeader.imgHeader.biWidth; i++) {
uint32 panelIndex = bmpGetColorPanelIndex(buffer, i, srcHeader.imgHeader.biBitsPerPixel);
if (panelIndex >= srcHeader.metaCount)
printf("panelIndex error:%d:%d\n", i, panelIndex);
bufOffSize += colorChangeRGB24(&srcHeader.colorPanel[panelIndex], writeBuf + bufOffSize);
}
while (bufOffSize < dstLineBytes){
*(writeBuf + bufOffSize) = 0;
bufOffSize++;
}
fwrite(writeBuf, 1, bufOffSize, dstFp);
imgSize += bufOffSize;
}
free(writeBuf);
fileSize += imgSize;
dstHeader.fileHeader.bfSize = fileSize;
//dstHeader.imgHeader.biHeight = -dstHeader.imgHeader.biHeight;
dstHeader.imgHeader.biImgSize = imgSize;
//将文件定位到文件头,修改文件头
rewind(dstFp);
fwrite(&dstHeader, 1, sizeof(bmpFileHeader) + sizeof(bmpImgHeader), dstFp);
fclose(dstFp);
fclose(srcFp);
return BMP_OK;
}
参考:
百度百科-BMP格式
音视频入门-03-RGB转成BMP图片
BMP文件格式及RlE压缩算法
Windows 应用开发-位图