使用纯C/C++编写BMP图像而不使用其他库

62

在我的算法中,我需要创建一个信息输出。我需要将布尔矩阵写入bmp文件中。必须是单色图像,如果该元素上的矩阵为真,则像素为白色。主要问题是bmp头以及如何编写此内容。


1
你可以查看http://qdbmp.sourceforge.net/获取实现细节 :)。 - Marco
也许对于搜索类似概念的访问者有用的是我在这里提出的几乎相关的问题和答案:http://stackoverflow.com/questions/17918978/plot-an-array-into-bitmap-in-c-c-for-thermal-printer - Krista K
除非BMP是必需的,否则在https://dev59.com/dWQn5IYBdhLWcg3wr446中讨论了更简单的格式。 - mwfearnley
我可能稍后会写一篇答案,但请注意,如果您正在编写单色BMP,则没有必要通过每个像素使用多于1位(1 bpp)的空间。 BMP格式允许您指定每个像素的位数,无论是1、2、4、8、16、24还是32,因此对于单色BMP,您只需要每个像素1位(仅两种颜色)。您想要的颜色(在您的情况下为白色和黑色)写在BMP的颜色表中,该表紧随文件头之后(每个条目4个字节,对应于蓝色、绿色和红色分量,然后是0)。 - Gregor Hartl Watters
13个回答

2
如果你在使用上述的C++函数时,发现图像中间出现了奇怪的颜色切换,请确保以二进制模式打开输出流:imgFile.open(filename, std::ios_base::out | std::ios_base::binary);
否则,Windows会在文件中间插入不需要的字符!(我曾经为这个问题苦苦挣扎了几个小时)

参考相关问题: Why does ofstream insert a 0x0D byte before 0x0A?

2
C++答案,灵活的API,为了减少代码量,假设使用小端系统。请注意,这使用bmp本地y轴(底部为0)。
#include <vector>
#include <fstream>

struct image
{   
    image(int width, int height)
    :   w(width), h(height), rgb(w * h * 3)
    {}
    uint8_t & r(int x, int y) { return rgb[(x + y*w)*3 + 2]; }
    uint8_t & g(int x, int y) { return rgb[(x + y*w)*3 + 1]; }
    uint8_t & b(int x, int y) { return rgb[(x + y*w)*3 + 0]; }

    int w, h;
    std::vector<uint8_t> rgb;
};

template<class Stream>
Stream & operator<<(Stream & out, image const& img)
{   
    uint32_t w = img.w, h = img.h;
    uint32_t pad = w * -3 & 3;
    uint32_t total = 54 + 3*w*h + pad*h;
    uint32_t head[13] = {total, 0, 54, 40, w, h, (24<<16)|1};
    char const* rgb = (char const*)img.rgb.data();

    out.write("BM", 2);
    out.write((char*)head, 52);
    for(uint32_t i=0 ; i<h ; i++)
    {   out.write(rgb + (3 * w * i), 3 * w);
        out.write((char*)&pad, pad);
    }
    return out;
}

int main()
{
    image img(100, 100);
    for(int x=0 ; x<100 ; x++)
    {   for(int y=0 ; y<100 ; y++)
        {   img.r(x,y) = x;
            img.g(x,y) = y;
            img.b(x,y) = 100-x;
        }
    }
    std::ofstream("/tmp/out.bmp") << img;
}

0

这段代码使用了一些较新的C++特性。我用它创建了8位和24位bmp文件。目前只能写入bmp文件,也许有一天我们可以读取它们!

我不喜欢所有的移位和容易出错的字节序安全问题。

它可能需要更多的注释,但代码相当直观。据我测试,所谓的运行时检测字节序最终被优化掉了(一段时间以前)。

endian_type.h >> 字节序安全POD类型。

#ifndef ENDIAN_TYPE_H
#define ENDIAN_TYPE_H

#include <algorithm>
#include <type_traits>

namespace endian_type {

template <typename T, bool store_as_big_endian>
struct EndianType {
  using value_type = T;
  static_assert(std::is_fundamental_v<value_type>, 
      "EndianType works for fundamental data types");
  
  EndianType() = default;

  EndianType(const value_type& value)
    : value{ convert_to(value) } {}

  struct TypeAsBytes {
    unsigned char value[sizeof(value_type)];
  };

  static constexpr bool is_big_endian() {
    union { int ival; char cval; } uval;
    uval.ival = 1;
    return 0 == uval.cval;
  }

  static TypeAsBytes convert_to(const value_type& ivalue) {
    TypeAsBytes ovalue;
    const unsigned char* p_ivalue = (const unsigned char*)&ivalue;
    if (store_as_big_endian != is_big_endian()) {
      std::reverse_copy(p_ivalue, p_ivalue + sizeof(value_type), ovalue.value);
    } else {
      std::copy(p_ivalue, p_ivalue + sizeof(value_type), ovalue.value);
    }
    return ovalue;
  }

  static value_type convert_from(const TypeAsBytes& ivalue) {
    value_type ovalue;
    unsigned char* p_ovalue = (unsigned char*) &ovalue;
    const unsigned char* p_ivalue = (const unsigned char*)&ivalue;
    if (store_as_big_endian != is_big_endian()) {
      std::reverse_copy(p_ivalue, p_ivalue + sizeof(value_type), p_ovalue);
    }
    else {
      std::copy(p_ivalue, p_ivalue + sizeof(value_type), p_ovalue);
    }
    return ovalue;
  }

  value_type get() const {
    return convert_from(value);
  }

  EndianType& set(const value_type& ivalue) {
    value = convert_to(ivalue);
    return *this;
  }

  operator value_type() const {
    return get();
  }

  EndianType& operator=(const value_type& ivalue) {
    set(ivalue);
    return *this;
  }

private:
  TypeAsBytes value;
};

template <typename T>
using BigEndian = EndianType<T, true>;

template <typename T>
using LittleEndian = EndianType<T, false>;

}  // namespace endian_type
#endif  // ENDIAN_TYPE_H

以下内容包含write_bmp函数。
bmp_writer.h >> BMP写入器头文件
#ifndef BMP_WRITER
#define BMP_WRITER

#include "endian_type.h"

#include <cctype>
#include <vector>
#include <fstream>

namespace bmp_writer {

template <typename T>
using LittleEndian = endian_type::LittleEndian<T>;

struct Header {
  char magic[2]{ 'B', 'M' };
  LittleEndian<std::uint32_t> size;
  LittleEndian<std::uint16_t> app_data1;
  LittleEndian<std::uint16_t> app_data2;
  LittleEndian<std::uint32_t> offset;
};

struct Info {
  LittleEndian<std::uint32_t> info_size{ 40 };
  LittleEndian<std::uint32_t> width;
  LittleEndian<std::uint32_t> height;
  LittleEndian<std::uint16_t> count_colour_planes{ 1 };
  LittleEndian<std::uint16_t> bits_per_pixel;
  LittleEndian<std::uint32_t> compression{};
  LittleEndian<std::uint32_t> image_bytes_size;
  LittleEndian<std::uint32_t> resolution_horizontal{ 2835 };
  LittleEndian<std::uint32_t> resolution_vertical{ 2835 };
  LittleEndian<std::uint32_t> count_pallete_entries{ 0 };
  LittleEndian<std::uint32_t> important_colours{ 0 };
};

template <std::size_t count>
class Palette {
public:
  static constexpr std::uint32_t NUM_CHANNELS = 4;
  using Entry = std::uint8_t[NUM_CHANNELS];
private:
  Palette() {
    for (auto i = 0; i < count; ++i) {
      auto& entry = table[i];
      for (auto j = 0; j < NUM_CHANNELS - 1; ++j) {
        entry[j] = i;
      }
    }
  }

  Palette(const Palette&) = delete;
  Palette(const Palette&&) = delete;
  Palette& operator=(const Palette&) = delete;
  Palette& operator=(const Palette&&) = delete;

public:
  static const Palette& get() {
    static const Palette palette;
    return palette;
  }

  Entry table[count];
};


static_assert(sizeof(Info) == 40, "");

template <typename T>
void write_bmp(
  std::ofstream& out,
  std::uint32_t width,
  std::uint32_t height,
  std::uint16_t count_colour_planes,
  const T* data,
  std::uint32_t data_size
) {

  auto& palette = Palette<256>::get();

  Header header;

  Info info;
  info.width = width;
  info.height = height;
  //info.count_colour_planes = count_colour_planes;

  const std::uint32_t t_per_pixel = data_size / (width * height);
  info.bits_per_pixel = std::uint16_t(sizeof(T) * 8 * t_per_pixel);
  const std::uint32_t row_len = width * sizeof(T) * t_per_pixel;
  // Round row up to next multiple of 4.
  const std::uint32_t padded_row_len = (row_len + 3) & ~3u;
  const std::uint32_t data_size_bytes = padded_row_len * height;
  info.image_bytes_size = data_size_bytes;

  if (count_colour_planes == 1) {
    header.offset = sizeof(Info) + sizeof(Header) + sizeof(palette);
  } else {
    header.offset = sizeof(Info) + sizeof(Header);
  }
  header.size = header.offset + height * padded_row_len;

  out.write(reinterpret_cast<const char*>(&header), sizeof(header));
  out.write(reinterpret_cast<const char*>(&info), sizeof(info));

  if (count_colour_planes == 1) {
    out.write(reinterpret_cast<const char*>(&palette), sizeof(palette));
  }

  const char padding[3] = {};
  for (int i = height; i > 0;) {
    --i;
    const char* p_row =
      reinterpret_cast<const char*>(data + i * width);
    out.write(p_row, row_len);
    if (padded_row_len != row_len) {
      out.write(padding, padded_row_len - row_len);
    }
  }

};


template <typename T>
void write_bmp(
  std::ofstream& out,
  std::uint32_t width,
  std::uint32_t height,
  std::uint16_t count_colour_planes,
  const std::vector<T>& data
) {
  write_bmp(out, width, height, count_colour_planes,
    &*data.cbegin(), data.size());
}

template <typename T>
void write_bmp(
  const std::string& outfilename,
  std::uint32_t width,
  std::uint32_t height,
  std::uint16_t count_colour_planes,
  const std::vector<T>& data
) {

  std::ofstream out{ outfilename, std::ios_base::binary };
  if (!out) {
    throw std::runtime_error("Failed to open: " + outfilename);
  }

  write_bmp(out, width, height, count_colour_planes,
    &*data.begin(), static_cast<std::uint32_t>(data.size()));

  out.close();
}

}  // namespace

#endif // BMP_WRITER

还有一个使用示例:

#include "bmp_writer.h"

struct PixelType {
  PixelType(std::uint8_t r, std::uint8_t g, std::uint8_t b)
    : c{ b, g, r } {}

  PixelType(std::uint32_t c)
    : c{ (c >> 16) & 0xffu, (c >> 8) & 0xffu, c & 0xffu } {}

  PixelType() = default;

  std::uint8_t c[3] = {};
};

void bmp_writer_test1() {
  const int size_x = 20;
  const int size_y = 10;
  std::vector<PixelType> data(size_x * size_y);

  // Write some pixels.
  data[2] = PixelType(0xff0000);   // red
  data[10] = PixelType(0x00ff00);  // green

  bmp_writer::write_bmp(
    "test_bmp_writer1.bmp",
    std::uint32_t(size_x),
    std::uint32_t(size_y),
    std::uint16_t(sizeof(PixelType)),
    data
  );
}

void bmp_writer_test2() {
  const int size_x = 20;
  const int size_y = 10;
  PixelType data[size_x * size_y];

  // Write some pixels.
  data[15] = PixelType(0xff, 0, 0);  // red
  data[17] = PixelType(0, 0xff, 0);  // green

  std::ofstream out{ "test_bmp_writer2.bmp", std::ios_base::binary };
  if (!out) {
    throw std::runtime_error("Failed to open: "  "test_bmp_writer2.bmp");
  }
  bmp_writer::write_bmp(
    out,
    std::uint32_t(size_x),
    std::uint32_t(size_y),
    std::uint16_t(sizeof(PixelType)),
    data,
    sizeof(data) / sizeof PixelType
  );
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接