639 lines
21 KiB
C++

/*
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 <sstream>
#include <fstream>
#include <string.h>
#define BMPLIB_VERSION 0.602
namespace BMPlib
{
typedef unsigned char byte;
typedef unsigned short byte2;
typedef unsigned int byte4;
using bytestring = std::basic_string<byte>;
using bytestream = std::basic_stringstream<byte>;
template<typename T>
// 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<typename ST, typename SO, typename T>
// Will convert 0F 00 00 00 to 15 and NOT TO 251658240
std::basic_istream<ST, SO>& FromBytes(std::basic_istream<ST, SO>& 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
};
}