BMP文件格式
??读写BMP格式的图片需要首先了解BMP图片的存储格式。可以参考维基百科上的介绍。 ??BMP文件主要有文件头(File Header)、信息头(DIB Header)、调色板(Color Table)和像素阵列(Pixel Array)组成。大部分情况下我们需要用的就是每个像素的数据。 ??可以从图中观察到,文件头中的File Offset to PixelArray可以直接得知Pixel Array的位置。信息头中有图像的宽、高和位深度的信息。像素阵列中的数据是一行一行组织的,每行的长度都是4字节的整数倍,如果一行的像素大小不是4字节的整数倍,还会再后面加Padding。
BMP读写
??了解了BMP文件的格式后,就可以据此编写相应的读写函数了。我准备写两个函数分别负责读和写。如下面的头文件所示:
#ifndef _BMPRW_H_
#define _BMPRW_H_
#ifdef __cplusplus
extern "C" {
#endif
int readBmp(char *fileName, void *pData);
int writeBmp(char *fileName, void *pData);
#ifdef __cplusplus
}
#endif
#endif
??读BMP文件的函数可以通过指定图片名,将图片中的像素数据读到一个指针指向的地址空间中。用户需要确保这个地址空间足够大。 ??写BMP文件的函数同样需要由用户指定文件名,然后将一个指针指向的一块连续的数据写到BMP文件中,考虑到实际使用中图片的分辨率一般不会发生变化,所以这个写BMP文件的函数只支持特定的分辨率大小,文件信息头是由固定的常量给出的。根据实际使用情况需要对源文件中的文件信息头dstInfo进行修改。 ??下面是源文件的实现:
#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};
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;
fseek(fp, head.bfOffBits, SEEK_SET);
for (i = info.biHeight - 1; i >= 0; --i){
fread(pData + i * info.biWidth * nElemSize, nElemSize, info.biWidth, fp);
fseek(fp, nPaddingSize, SEEK_CUR);
}
fclose(fp);
return 0;
}
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);
fwrite(pPadding, 1, nPaddingSize, fp);
}
fclose(fp);
free(pPadding);
pPadding = NULL;
return 0;
}
??需要注意的地方:
- 结构体类型定义需要带上packed属性,这样结构体中的字段才能和真正的BMP文件存储格式对应上;
- 读写每行像素时,习惯上的第一行,实际上是存在文件中的最后一行,所以在进行读写操作时,循环变量i是从大到小变化;
- 每行像素有可能会有padding,所以在读写的时候也需要对padding进行处理。
- 写BMP文件时,要确认文件信息头是否正确。在不知道文件信息头应该是什么样的情况时,可以先读一张同样大小的图片,然后把信息头保存下来。
BMP读写测试
#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 环境信息 测试结果:
|