/* Author: Leon Etienne Copyright (c) 2021, Leon Etienne https://github.com/Leonetienne https://github.com/Leonetienne/BMPlib License: Don't Be a Jerk: The Open Source Software License. Last Update: Jan, 7, 2021 This software is free and open source. Please read the full license: https://github.com/Leonetienne/BMPlib/blob/master/license.txt #define BMPLIB_SILENT if you want bmplib to stop writing exceptions to stderr */ #pragma once #include #include #include #define BMPLIB_VERSION 0.602 namespace BMPlib { typedef unsigned char byte; typedef unsigned short byte2; typedef unsigned int byte4; using bytestring = std::basic_string; using bytestream = std::basic_stringstream; template // Will convert (int)15 to 0F 00 00 00 and NOT TO 00 00 00 0F bytestring ToBytes(T t) { bytestream toret; long long sizeofT = (long long)sizeof(T); // Let's make it a signed value to keep the compiler happy with the comparison byte* bPtr = (byte*)&t; for (long long i = 0; i < sizeofT; i++) toret << *(bPtr + i); return toret.str(); } template // Will convert 0F 00 00 00 to 15 and NOT TO 251658240 std::basic_istream& FromBytes(std::basic_istream& is, T& b) { const std::size_t sizeofT = sizeof(T); b = 0x0; byte buf; for (std::size_t i = 0; i < sizeofT; i++) { is.read((ST*)&buf, 1); T bbuf = buf << (i * 8); b |= bbuf; } return is; } class BMP { public: enum class COLOR_MODE { BW, // This is a special case, since BMP doesn't support 1 channel. It is just RGB, but with a 1-channel pixel buffer RGB, RGBA }; BMP() noexcept { width = 0; height = 0; colorMode = COLOR_MODE::BW; sizeofPxlbfr = 0; numChannelsFile = 0; numChannelsPXBF = 0; pixelbfr = nullptr; isInitialized = false; return; } explicit BMP(const std::size_t& width, const std::size_t& height, const BMP::COLOR_MODE& colorMode = BMP::COLOR_MODE::RGB) : isInitialized{false} { ReInitialize(width, height, colorMode); return; } void ReInitialize(const std::size_t& width, const std::size_t& height, const BMP::COLOR_MODE& colorMode = BMP::COLOR_MODE::RGB) { if ((!width) || (!height)) ThrowException("Bad image dimensions!"); // Initialize bunch of stuff this->width = width; this->height = height; this->colorMode = colorMode; switch (colorMode) { case COLOR_MODE::BW: numChannelsFile = 3; numChannelsPXBF = 1; break; case COLOR_MODE::RGB: numChannelsFile = 3; numChannelsPXBF = 3; break; case COLOR_MODE::RGBA: numChannelsFile = 4; numChannelsPXBF = 4; break; } // Delete pixelbuffer if already exists if (isInitialized) delete[] pixelbfr; sizeofPxlbfr = sizeof(byte) * width * height * numChannelsPXBF; // Try to allocate memory for the pixelbuffer try { pixelbfr = new byte[sizeofPxlbfr]; } catch (std::bad_alloc& e) { // too bad! ThrowException(std::string("Can't allocate memory for pixelbuffer!") + e.what()); } // Make image black memset(pixelbfr, 0, sizeofPxlbfr); isInitialized = true; return; } #ifdef __linux__ #pragma GCC diagnostic push // g++ doesn't see the parameters, numPx, and curPx getting used inside the switch... we don't want these false warnings #pragma GCC diagnostic ignored "-Wunused-variable" #pragma GCC diagnostic ignored "-Wunused-parameter" #endif // Will convert between color modes. Set isNonColorData to true to treat pixeldata as raw values rather than color void ConvertTo(const BMP::COLOR_MODE& convto, bool isNonColorData = false) { // Damn this method is one hell of a mess if (!isInitialized) ThrowException("Not initialized!"); byte* tmp_pxlbfr; try { tmp_pxlbfr = new byte[sizeofPxlbfr]; } catch (std::bad_alloc&e ) { // too bad! ThrowException(std::string("Can't allocate memory for temporary conversion pixelbuffer!") + e.what()); return; // This won't ever be reached but it satisfies the compiler, soooo... } const std::size_t numPx = width * height; const std::size_t oldPxlbfrSize = sizeofPxlbfr; const byte* curPx; // Usage may vary on the conversion in question. It's just a pixel cache for a small performance improvement memcpy(tmp_pxlbfr, pixelbfr, sizeofPxlbfr); #ifdef __linux__ #pragma GCC diagnostic pop // Let's enable these warnings again #endif switch (colorMode) { case COLOR_MODE::BW: #ifndef __linux__ #pragma region CONVERT_FROM_BW #endif switch (convto) { case COLOR_MODE::RGB: // BW -> RGB ReInitialize(width, height, COLOR_MODE::RGB); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i; pixelbfr[i * 3 + 0] = *curPx; pixelbfr[i * 3 + 1] = *curPx; pixelbfr[i * 3 + 2] = *curPx; } break; case COLOR_MODE::RGBA: // BW -> RGBA ReInitialize(width, height, COLOR_MODE::RGBA); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i; pixelbfr[i * 4 + 0] = *curPx; pixelbfr[i * 4 + 1] = *curPx; pixelbfr[i * 4 + 2] = *curPx; pixelbfr[i * 4 + 3] = 0xFF; } break; default: break; } #ifndef __linux__ #pragma endregion #endif break; case COLOR_MODE::RGB: #ifndef __linux__ #pragma region CONVERT_FROM_RGB #endif switch (convto) { case COLOR_MODE::BW: // RGB -> BW ReInitialize(width, height, COLOR_MODE::BW); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i * 3; // Don't ask me why but the compiler hates dereferencing tmp_pxlbfr/curPx via [] and throws false warnings... if (isNonColorData) pixelbfr[i] = (byte)(( *(curPx + 0) + *(curPx + 1) + *(curPx + 2)) * 0.33333333); else pixelbfr[i] = (byte)( *(curPx + 0) * 0.3 + *(curPx + 1) * 0.59 + *(curPx + 2) * 0.11); } break; case COLOR_MODE::RGBA: // RGB -> RGBA ReInitialize(width, height, COLOR_MODE::RGBA); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i * 3; pixelbfr[i * 4 + 0] = *(curPx + 0); pixelbfr[i * 4 + 1] = *(curPx + 1); pixelbfr[i * 4 + 2] = *(curPx + 2); pixelbfr[i * 4 + 3] = 0xFF; } break; default: break; } #ifndef __linux__ #pragma endregion #endif break; case COLOR_MODE::RGBA: #ifndef __linux__ #pragma region CONVERT_FROM_RGBA #endif switch (convto) { case COLOR_MODE::BW: // RGBA -> BW ReInitialize(width, height, COLOR_MODE::BW); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i * 4; if (isNonColorData) pixelbfr[i] = (byte)(( *(curPx + 0) + *(curPx + 1) + *(curPx + 2)) * 0.33333333); else pixelbfr[i] = (byte)( *(curPx + 0) * 0.3 + *(curPx + 1) * 0.59 + *(curPx + 2) * 0.11); } break; case COLOR_MODE::RGB: // RGBA -> RGB ReInitialize(width, height, COLOR_MODE::RGB); for (std::size_t i = 0; i < numPx; i++) { curPx = tmp_pxlbfr + i * 4; pixelbfr[i * 3 + 0] = *(curPx + 0); pixelbfr[i * 3 + 1] = *(curPx + 1); pixelbfr[i * 3 + 2] = *(curPx + 2); } break; default: break; } #ifndef __linux__ #pragma endregion #endif break; } delete[] tmp_pxlbfr; return; } byte* GetPixelBuffer() noexcept { return pixelbfr; } const byte* GetPixelBuffer() const noexcept { return pixelbfr; } std::size_t GetWidth() const noexcept { return width; } std::size_t GetHeight() const noexcept { return height; } COLOR_MODE GetColorMode() const noexcept { return colorMode; } bool IsInitialized() const noexcept { return isInitialized; } std::size_t CalculatePixelIndex(const std::size_t& x, const std::size_t& y) const { if ((x >= width) || (y >= height)) ThrowException("Pixel coordinates out of range!"); return numChannelsPXBF * ((y * width) + x); } byte* GetPixel(const std::size_t& x, const std::size_t& y) { return pixelbfr + CalculatePixelIndex(x, y); } const byte* GetPixel(const std::size_t& x, const std::size_t& y) const { return pixelbfr + CalculatePixelIndex(x, y); } // Sets a pixels color // If using RGBA, use all // If using RGB, a gets ignored // If using BW, use only r void SetPixel(const std::size_t& x, const std::size_t& y, const byte& r, const byte& g = 0, const byte& b = 0, const byte& a = 0) { byte* px = GetPixel(x, y); switch (colorMode) { case COLOR_MODE::BW: px[0] = r; break; case COLOR_MODE::RGB: px[0] = r; px[1] = g; px[2] = b; break; case COLOR_MODE::RGBA: px[0] = r; px[1] = g; px[2] = b; px[3] = a; break; } return; } // Will write a bmp image bool Write(const std::string& filename) { if (!isInitialized) return false; std::size_t paddingSize = (4 - ((width * numChannelsFile) % 4)) % 4; // number of padding bytes per scanline byte paddingData[4] = { 0x69,0x69,0x69,0x69 }; // dummy-data for padding bytestream data; data // BMP Header << ToBytes(byte2(0x4D42)) // signature << ToBytes(byte4(0x36 + sizeofPxlbfr + paddingSize * height)) // size of the bmp file (all bytes) << ToBytes(byte2(0)) // unused << ToBytes(byte2(0)) // unused << ToBytes(byte4(0x36)) // Offset where the pixel array begins (size of both headers) // DIB Header << ToBytes(byte4(0x28)) // Number of bytes in DIB header (without this field) << ToBytes(byte4(width)) // width << ToBytes(byte4(height)) // height << ToBytes(byte2(1)) // number of planes used << ToBytes(byte2(numChannelsFile * 8)) // bit-depth << ToBytes(byte4(0)) // no compression << ToBytes(byte4(sizeofPxlbfr + paddingSize * height)) // Size of raw bitmap data (including padding) << ToBytes(byte4(0xB13)) // print resolution pixels/meter X << ToBytes(byte4(0xB13)) // print resolution pixels/meter Y << ToBytes(byte4(0)) // 0 colors in the color palette << ToBytes(byte4(0)); // 0 means all colors are important // Dumbass unusual pixel order of bmp made me do this... for (long long y = height - 1; y >= 0; y--) { for (std::size_t x = 0; x < width; x++) { std::size_t idx = CalculatePixelIndex(x, (std::size_t)y); // No precision lost here. I just need a type that can get as large as std::size_t but is also signed (because for y >= 0) switch (colorMode) { case COLOR_MODE::BW: // pixelbfr ==> R-G-B ==> B-G-R ==> bmp format data.write((pixelbfr + idx), 1); // B data.write((pixelbfr + idx), 1); // G data.write((pixelbfr + idx), 1); // R break; case COLOR_MODE::RGB: // pixelbfr ==> R-G-B ==> B-G-R ==> bmp format data.write((pixelbfr + idx + 2), 1); // B data.write((pixelbfr + idx + 1), 1); // G data.write((pixelbfr + idx + 0), 1); // R break; case COLOR_MODE::RGBA: // pixelbfr ==> R-G-B-A ==> B-G-R-A ==> bmp format data.write((pixelbfr + idx + 2), 1); // B data.write((pixelbfr + idx + 1), 1); // G data.write((pixelbfr + idx + 0), 1); // R data.write((pixelbfr + idx + 3), 1); // A break; } } if ((colorMode == COLOR_MODE::BW) || (colorMode == COLOR_MODE::RGB)) if (paddingSize > 0) data.write(paddingData, paddingSize); } // write file std::ofstream bs; bs.open(filename, std::ofstream::binary); if (!bs.good()) return false; bytestring bytes = data.str(); bs.write((const char*)bytes.c_str(), bytes.length()); bs.flush(); bs.close(); return true; } // Will read a bmp image bool Read(std::string filename) { std::ifstream bs; bs.open(filename, std::ifstream::binary); if (!bs.good()) return false; // Check BMP signature byte2 signature; FromBytes(bs, signature); if (signature != 0x4D42) return false; // Gather filesize byte4 fileLen; FromBytes(bs, fileLen); byte2 unused0; byte2 unused1; FromBytes(bs, unused0); FromBytes(bs, unused1); byte4 offsetPixelArray; FromBytes(bs, offsetPixelArray); byte4 dibHeadLen; FromBytes(bs, dibHeadLen); // Gather image dimensions byte4 imgWidth; byte4 imgHeight; FromBytes(bs, imgWidth); FromBytes(bs, imgHeight); byte2 numPlanes; FromBytes(bs, numPlanes); // Gather image bit-depth byte2 bitDepth; FromBytes(bs, bitDepth); switch (bitDepth) { // BW is not supported so we can't read a bw image case 24: colorMode = COLOR_MODE::RGB; break; case 32: colorMode = COLOR_MODE::RGBA; } byte4 compression; FromBytes(bs, compression); // Gather size of pixel buffer byte4 sizeofPixelBuffer; FromBytes(bs, sizeofPixelBuffer); byte4 printresX; byte4 printresY; FromBytes(bs, printresX); FromBytes(bs, printresY); byte4 colorsInPalette; byte4 importantColors; FromBytes(bs, colorsInPalette); FromBytes(bs, importantColors); // Go to the beginning of the pixel array bs.seekg(offsetPixelArray); // Initialize image ReInitialize(imgWidth, imgHeight, colorMode); // Calculate scanline padding size std::size_t paddingSize = (4 - ((width * numChannelsFile) % 4)) % 4; paddingSize = paddingSize < 4 ? paddingSize : 0; // Dumbass unusual pixel order of bmp made me do this... for (long long y = imgHeight - 1; y >= 0; y--) { std::size_t byteCounter = 0; for (std::size_t x = 0; x < imgWidth; x++) { std::size_t idx = CalculatePixelIndex(x, (std::size_t)y); // No precision lost here. I just need a type that can get as large as std::size_t but is also signed (because for y >= 0) switch (colorMode) { case COLOR_MODE::RGB: // bmp format ==> B-G-R ==> R-G-B ==> pixelbfr bs.read((char*)(pixelbfr + idx + 2), 1); // Read B byte bs.read((char*)(pixelbfr + idx + 1), 1); // Read G byte bs.read((char*)(pixelbfr + idx + 0), 1); // Read R byte byteCounter += 3; break; case COLOR_MODE::RGBA: // bmp format ==> B-G-R-A ==> R-G-B-A ==> pixelbfr bs.read((char*)(pixelbfr + idx + 2), 1); // Read B byte bs.read((char*)(pixelbfr + idx + 1), 1); // Read G byte bs.read((char*)(pixelbfr + idx + 0), 1); // Read R byte bs.read((char*)(pixelbfr + idx + 3), 1); // Read A byte byteCounter += 4; break; default: break; } } if ((colorMode == COLOR_MODE::BW) || (colorMode == COLOR_MODE::RGB)) // RGBA will always be a multiple of 4 bytes if (byteCounter % 4) // If this scan line was not a multiple of 4 bytes long, account for padding bs.ignore(paddingSize); } bs.close(); return true; } ~BMP() { if (isInitialized) { delete[] pixelbfr; pixelbfr = nullptr; } return; } private: void ThrowException(const std::string msg) const { #ifndef BMPLIB_SILENT #ifdef _IOSTREAM_ std::cerr << "BMPlib exception: " << msg << std::endl; #endif #endif throw msg; return; } std::size_t width; std::size_t height; std::size_t numChannelsFile; // Num channels of the file format std::size_t numChannelsPXBF; // Num channels of the pixel buffer COLOR_MODE colorMode; bool isInitialized; byte* pixelbfr; std::size_t sizeofPxlbfr; // how many bytes the pixelbuffer is long }; }