diff --git a/Exec/main.cpp b/Exec/main.cpp index 877f742..38b0e8a 100644 --- a/Exec/main.cpp +++ b/Exec/main.cpp @@ -1,9 +1,26 @@ #include +#include +#include using namespace Leonetienne::BmpPP; int main() { - BMP bmp({800, 600}); + + BMP bmp({800, 600}, Colormode::RGB); + + for (int x = 0; x < 800; x++) + for (int y = 0; y < 600; y++) { + + bmp.SetPixel({x, y}, + (std::uint8_t)( ((double)x / 800.0) * 255.0), + (std::uint8_t)((1.0 - ((double)x / 800.0)) * 255.0), + (std::uint8_t)((1.0 - ((double)y / 800.0)) * 255.0), + 255 + ); + } + + if (!bmp.Write("test.bmp")) + std::cerr << "What the hell" << std::endl; return 0; } diff --git a/Src/BMP.cpp b/Src/BMP.cpp index 6ad450a..39682d5 100644 --- a/Src/BMP.cpp +++ b/Src/BMP.cpp @@ -1,11 +1,15 @@ #include "BMP.h" #include +#include +#include +#include "BmpHeader.h" namespace Leonetienne::BmpPP { BMP::BMP(const Eule::Vector2i &size, const Colormode& colormode) : - size { size } + size { size }, + colormode { colormode } { pixelBuffer.clear(); @@ -19,11 +23,177 @@ namespace Leonetienne::BmpPP { switch (colormode) { case Colormode::RGB: return 3; + case Colormode::RGBA: return 4; - - // Unreachable } + + // Unreachable + return -1; + } + + bool BMP::Write(const std::string &filename) const { + + // Create the bmp header + BmpHeader bmpHeader; + + // Populate file header + bmpHeader.fileHeader.addressPixelBuffer = + BmpHeader::FileHeader::NBYTES + + BmpHeader::DibHeader::NBYTES + + 2; // We have to put two padding-bytes behind the headers. + // The pixel data must start at an address that is a multiple of 4. + + // The size of the bmp file is not yet known... + + // Populate dib header + bmpHeader.dibHeader.imageWidth = size.x; + bmpHeader.dibHeader.imageHeight = size.y; + bmpHeader.dibHeader.numBitsPerPixel = ColormodeToPixelSize(colormode) * 8; + // The size of the pixel array is not known yet (because rows require to be padded) + + + // Pack pixel values + std::vector packedPixels; + packedPixels.reserve(pixelBuffer.size()); + + // How many channels do we have? + const std::size_t numChannels = ColormodeToPixelSize(colormode); + + // Calculate how many padding bytes to add per row + std::size_t paddingBytesPerRow = (4 - ((size.x * numChannels) % 4)) % 4; + + // Iterate over all pixel rows + for (std::size_t y = 0; y < size.y; y++) { + const std::size_t rowIndex = y * size.x * numChannels; + + for (std::size_t x = 0; x < size.x; x++) { + const std::size_t pixelIndex = rowIndex + x * numChannels; + + // Write actual pixel values to row + switch (colormode) { + case Colormode::RGBA: + // Write B byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 2] + ); + // Write G byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 1] + ); + // Write R byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 0] + ); + // Write A byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 3] + ); + break; + + case Colormode::RGB: + // Write B byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 2] + ); + // Write G byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 1] + ); + // Write R byte + packedPixels.insert( + packedPixels.cend(), + pixelBuffer[pixelIndex + 0] + ); + break; + } + + // Add row padding + // Since we have to pad to a multiple of 4, the padding will never be more than four bytes + for (std::size_t i = 0; i < paddingBytesPerRow; i++) + packedPixels.insert(packedPixels.cend(), 0x0); + } + } + + // Now we can finally set the fileSize field in the file header, + // and the pixelArraySize in the dib header + bmpHeader.fileHeader.filesize = + bmpHeader.fileHeader.addressPixelBuffer + + packedPixels.size(); + + bmpHeader.dibHeader.pixelArraySize = packedPixels.size(); + + // Write to file + std::ofstream ofs(filename, std::ofstream::binary); + if (!ofs.good()) + return false; + + + // Write the header + const std::vector headerBytes = bmpHeader.ToBytes(); + ofs.write((const char*)headerBytes.data(), headerBytes.size()); + + // Write two padding bytes + ofs.write("\0\0", 2); + + // Write the pixel data + ofs.write((const char*)packedPixels.data(), packedPixels.size()); + + ofs.close(); + + return true; + } + + std::uint8_t *BMP::GetPixel(const Eule::Vector2i &position) { + const std::size_t pixelIndex = + (position.y * size.x + position.x) * ColormodeToPixelSize(colormode); + + if (pixelIndex >= pixelBuffer.size()) + throw std::runtime_error("Pixel index out of range!"); + + return pixelBuffer.data() + pixelIndex; + } + + const std::uint8_t *BMP::GetPixel(const Eule::Vector2i &position) const { + const std::size_t pixelIndex = + (position.y * size.x + position.x) * ColormodeToPixelSize(colormode); + + if (pixelIndex >= pixelBuffer.size()) + throw std::runtime_error("Pixel index out of range!"); + + return pixelBuffer.data() + pixelIndex; + } + + void BMP::SetPixel(const Eule::Vector2i &position, + const std::uint8_t r, + const std::uint8_t g, + const std::uint8_t b, + const std::uint8_t a) { + + std::uint8_t* pixel = GetPixel(position); + + switch (colormode) { + case Colormode::RGBA: + pixel[0] = r; + pixel[1] = g; + pixel[2] = b; + pixel[3] = a; + break; + + case Colormode::RGB: + pixel[0] = r; + pixel[1] = g; + pixel[2] = b; + break; + } + + return; } } diff --git a/Src/BMP.h b/Src/BMP.h index 7defadc..8fc1b3a 100644 --- a/Src/BMP.h +++ b/Src/BMP.h @@ -12,11 +12,24 @@ namespace Leonetienne::BmpPP { public: explicit BMP(const Eule::Vector2i& size, const Colormode& colormode = Colormode::RGBA); + //! Will write the bmp image to a file. + bool Write(const std::string& filename) const; + + //! Will return a pointer to the first byte of a pixel at a given position + std::uint8_t* GetPixel(const Eule::Vector2i& position); + + //! Will return a pointer to the first byte of a pixel at a given position + const std::uint8_t* GetPixel(const Eule::Vector2i& position) const; + + //! Will set the color of a pixel at a given position + void SetPixel(const Eule::Vector2i& position, const std::uint8_t r, const std::uint8_t g, const std::uint8_t b, const std::uint8_t a = 0xFF); + private: //! Will return the corresponding pixel size (in bytes) of a colormode. Like, 3 for RGB and 4 for RGBA. static int ColormodeToPixelSize(const Colormode& colormode); Eule::Vector2i size; + Colormode colormode; std::vector pixelBuffer; }; diff --git a/Src/BmpHeader.cpp b/Src/BmpHeader.cpp new file mode 100644 index 0000000..ef551db --- /dev/null +++ b/Src/BmpHeader.cpp @@ -0,0 +1,30 @@ +#include "BmpHeader.h" + +namespace Leonetienne::BmpPP { + + std::vector BmpHeader::ToBytes() const { + + std::vector data; + + InsertBytes(data, fileHeader.signature); + InsertBytes(data, fileHeader.filesize); + InsertBytes(data, fileHeader.reserved0); + InsertBytes(data, fileHeader.reserved1); + InsertBytes(data, fileHeader.addressPixelBuffer); + + InsertBytes(data, dibHeader.dibHeaderSize); + InsertBytes(data, dibHeader.imageWidth); + InsertBytes(data, dibHeader.imageHeight); + InsertBytes(data, dibHeader.numColorPlanes); + InsertBytes(data, dibHeader.numBitsPerPixel); + InsertBytes(data, dibHeader.compressionUsed); + InsertBytes(data, dibHeader.pixelArraySize); + InsertBytes(data, dibHeader.printResolutionHorizontal); + InsertBytes(data, dibHeader.printResolutionVertical); + InsertBytes(data, dibHeader.numColorsInPalette); + InsertBytes(data, dibHeader.numImportantColors); + + return data; + } + +} diff --git a/Src/BmpHeader.h b/Src/BmpHeader.h new file mode 100644 index 0000000..3d0b34a --- /dev/null +++ b/Src/BmpHeader.h @@ -0,0 +1,98 @@ +#ifndef BMPPP_EXEC_BMPHEADER_H +#define BMPPP_EXEC_BMPHEADER_H + +#include +#include + +namespace Leonetienne::BmpPP { + + // This struct represents the actual image file. + class BmpHeader { + public: + + //! This header contains information about the file itself. + struct FileHeader { + //! Constant value. These two bytes identify a bmp file. + const std::uint16_t signature = 0x4D42; + + //! The size of the entire file, in bytes. + std::uint32_t filesize; + + //! Reserved... + std::uint16_t reserved0; + + //! Reserved... + std::uint16_t reserved1; + + //! The address where the pixel buffer / pixel array begins. + std::uint32_t addressPixelBuffer; + + // Since C++ tends to pad structures, we have to explicitly define how many bytes + // this struct is long. For example, sizeof(FileHeader) (without the size field) + // returns 16, even though it really should only be 14. + // This has to be set at the END of the struct, as to not be written to the file itself. + static constexpr std::uint16_t NBYTES = 14; + + + } fileHeader; + + //! This header contains information about the image itself. + //! We are using the BITMAPINFOHEADER format. + struct DibHeader { + //! The size of the dibHeader (since there are multiple to choose from). + //! This one 40 bytes, fix. + const std::uint32_t dibHeaderSize = 40; + + //! The width of the image, in pixels. + std::int32_t imageWidth; + + //! The height of the image, in pixels. + std::int32_t imageHeight; + + //! The amount of color planes used. Always 1. + const std::uint16_t numColorPlanes = 1; + + //! The amount of bits (not bytes!) per pixel. + std::uint16_t numBitsPerPixel; + + //! We don't use any compression. + const std::uint32_t compressionUsed = 0; + + //! The size of the pixel array (including row padding) + std::uint32_t pixelArraySize; + + //! The amount of pixels per meter on the horizontal axis. Kinda useless for our purposes, but required to be in the header. + const std::int32_t printResolutionHorizontal = 2835; + + //! The amount of pixels per meter on the vertical axis. Kinda useless for our purposes, but required to be in the header. + const std::int32_t printResolutionVertical = 2835; + + //! We don't have any colors in our color palette + const std::uint32_t numColorsInPalette = 0; + + //! All colors are important + const std::uint32_t numImportantColors = 0; + + // This has to be set at the END of the struct, as to not be written to the file itself. + static constexpr std::uint16_t NBYTES = 40; + + } dibHeader; + + //! Will return bytes representing the header + //! Just dumping the memory won't work, because the compiler may insert padding in between members + std::vector ToBytes() const; + + private: + template + void InsertBytes(std::vector& vec, const T& value) const { + vec.insert( + vec.cend(), + static_cast(static_cast(&value)), + static_cast(static_cast(&value)) + sizeof(value) + ); + return; + }; + }; +} + +#endif //BMPPP_EXEC_BMPHEADER_H diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt index a45f554..f66c0aa 100644 --- a/Src/CMakeLists.txt +++ b/Src/CMakeLists.txt @@ -11,4 +11,5 @@ add_library(BmpPP ${Eule} BMP.cpp + BmpHeader.cpp )