C/C++读写BMP文件

BMP文件格式

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

BMP读写

  了解了BMP文件的格式后,就可以据此编写相应的读写函数了。我准备写两个函数分别负责读和写。如下面的头文件所示:

/**
 * @file bmpRw.h
 * @author Jiandong Qiu ([email protected])
 * @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进行修改。
  下面是源文件的实现:

/**
 * @file bmpRw.c
 * @author Jiandong Qiu ([email protected])
 * @brief 
 * @version 0.1
 * @date 2022-10-07
 * 
 * @copyright Copyright (c) 2022
 * 
 */

#include "bmpRw.h"
#include 
#include 
#include 
#include 
#include 

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读写测试

/**
 * @file main.c
 * @author Jiandong Qiu ([email protected])
 * @brief 
 * @version 0.1
 * @date 2022-10-07
 * 
 * @copyright Copyright (c) 2022
 * 
 */

#include "bmpRw.h"
#include 
#include 

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 环境信息
C/C++读写BMP文件_第2张图片
测试结果:
C/C++读写BMP文件_第3张图片

你可能感兴趣的:(日常调试记录,c++,c语言,bmp)