509 lines
15 KiB
C++
509 lines
15 KiB
C++
#include "BMP.h"
|
|
#include <stdexcept>
|
|
#include <algorithm>
|
|
#include "BmpWriter.h"
|
|
#include "BmpReader.h"
|
|
|
|
#define CHECK_IF_INITIALIZED if (!isInitialized) throw std::runtime_error("Image not initialized!");
|
|
|
|
namespace Leonetienne::BmpPP {
|
|
|
|
using namespace Eule;
|
|
|
|
BMP::BMP() {
|
|
// Do nothing
|
|
return;
|
|
}
|
|
|
|
BMP::BMP(const std::string &filename) {
|
|
if(!Read(filename))
|
|
throw std::runtime_error("Unable to read bmp image!");
|
|
|
|
return;
|
|
}
|
|
|
|
BMP::BMP(const Vector2i &size, const Colormode& colormode)
|
|
:
|
|
size { size },
|
|
colormode { colormode }
|
|
{
|
|
ReInitialize(size, colormode);
|
|
|
|
return;
|
|
}
|
|
|
|
void BMP::ReInitialize(const Vector2i &size) {
|
|
isInitialized = true;
|
|
|
|
// Carry over new attributes
|
|
this->size = size;
|
|
|
|
// Re-initialize the pixelbuffer
|
|
pixelBuffer.clear();
|
|
pixelBuffer.resize(size.x * size.y * GetNumChannels());
|
|
|
|
// If we're initializing with an alpha channel, set it to 255
|
|
if (colormode == Colormode::RGBA)
|
|
FillChannel(3, 255);
|
|
|
|
return;
|
|
}
|
|
|
|
void BMP::ReInitialize(const Vector2i &size, const Colormode &colormode) {
|
|
isInitialized = true;
|
|
|
|
// Carry over new attributes
|
|
this->size = size;
|
|
this->colormode = colormode;
|
|
|
|
// Re-initialize the pixelbuffer
|
|
pixelBuffer.clear();
|
|
pixelBuffer.resize(size.x * size.y * GetNumChannels());
|
|
|
|
// If we're initializing with an alpha channel, set it to 255
|
|
if (colormode == Colormode::RGBA)
|
|
FillChannel(3, 255);
|
|
|
|
return;
|
|
}
|
|
|
|
bool BMP::Write(const std::string &filename) const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return BmpWriter::Write(*this, filename);
|
|
}
|
|
|
|
bool BMP::Read(const std::string &filename) {
|
|
return BmpReader::Read(*this, filename);
|
|
}
|
|
|
|
std::uint8_t *BMP::GetPixel(const Vector2i &position) {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
const std::size_t pixelIndex =
|
|
(position.y * size.x + position.x) * GetNumChannels();
|
|
|
|
if (pixelIndex >= pixelBuffer.size())
|
|
throw std::runtime_error("Pixel index out of range!");
|
|
|
|
return pixelBuffer.data() + pixelIndex;
|
|
}
|
|
|
|
const std::uint8_t *BMP::GetPixel(const Vector2i &position) const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
const std::size_t pixelIndex =
|
|
(position.y * size.x + position.x) * GetNumChannels();
|
|
|
|
if (pixelIndex >= pixelBuffer.size())
|
|
throw std::runtime_error("Pixel index out of range!");
|
|
|
|
return pixelBuffer.data() + pixelIndex;
|
|
}
|
|
|
|
void BMP::SetPixel(const Vector2i &position,
|
|
const std::uint8_t v)
|
|
{
|
|
CHECK_IF_INITIALIZED
|
|
|
|
std::uint8_t* pixel = GetPixel(position);
|
|
|
|
pixel[0] = v;
|
|
pixel[1] = v;
|
|
pixel[2] = v;
|
|
|
|
return;
|
|
}
|
|
|
|
void BMP::SetPixel(const Vector2i &position,
|
|
const std::uint8_t r,
|
|
const std::uint8_t g,
|
|
const std::uint8_t b)
|
|
{
|
|
CHECK_IF_INITIALIZED
|
|
|
|
std::uint8_t* pixel = GetPixel(position);
|
|
|
|
pixel[0] = r;
|
|
pixel[1] = g;
|
|
pixel[2] = b;
|
|
|
|
return;
|
|
}
|
|
|
|
void BMP::SetPixel(const Vector2i &position,
|
|
const std::uint8_t r,
|
|
const std::uint8_t g,
|
|
const std::uint8_t b,
|
|
const std::uint8_t a)
|
|
{
|
|
CHECK_IF_INITIALIZED
|
|
|
|
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;
|
|
}
|
|
|
|
std::uint8_t *BMP::data() {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return pixelBuffer.data();
|
|
}
|
|
|
|
const std::uint8_t *BMP::data() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return pixelBuffer.data();
|
|
}
|
|
|
|
const Vector2i &BMP::GetDimensions() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return size;
|
|
}
|
|
|
|
const Colormode &BMP::GetColormode() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return colormode;
|
|
}
|
|
|
|
std::size_t BMP::GetNumChannels() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
switch (colormode) {
|
|
case Colormode::RGB:
|
|
return 3;
|
|
|
|
case Colormode::RGBA:
|
|
return 4;
|
|
}
|
|
|
|
// Unreachable
|
|
return -1;
|
|
}
|
|
|
|
std::size_t BMP::GetPixelbufferSize() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
return pixelBuffer.size();
|
|
}
|
|
|
|
bool BMP::IsInitialized() const {
|
|
return isInitialized;
|
|
}
|
|
|
|
const std::vector<std::uint8_t> &BMP::GetPixelbuffer() const {
|
|
CHECK_IF_INITIALIZED
|
|
return pixelBuffer;
|
|
}
|
|
|
|
bool BMP::operator==(const BMP &other) const {
|
|
// Check initialization status
|
|
if (isInitialized != other.isInitialized)
|
|
return false;
|
|
|
|
// Check metadata
|
|
if (colormode != other.colormode)
|
|
return false;
|
|
|
|
if (size != other.size)
|
|
return false;
|
|
|
|
// Check pixel values
|
|
if (pixelBuffer != other.pixelBuffer)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BMP::operator!=(const BMP &other) const {
|
|
return !operator==(other);
|
|
}
|
|
|
|
void BMP::FillChannel(const size_t &channel, const std::uint8_t value) {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
if (GetNumChannels() <= channel)
|
|
throw std::runtime_error("Channel index out of range!");
|
|
|
|
const std::size_t numChannels = GetNumChannels();
|
|
|
|
for (std::size_t i = 0; i < pixelBuffer.size(); i += numChannels)
|
|
pixelBuffer[i + channel] = value;
|
|
|
|
return;
|
|
}
|
|
|
|
void BMP::SwapChannels(const size_t &channel1, const size_t &channel2) {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
if ((GetNumChannels() <= channel1) || (GetNumChannels() <= channel2))
|
|
throw std::runtime_error("Channel index out of range!");
|
|
|
|
const std::size_t numChannels = GetNumChannels();
|
|
|
|
for (std::size_t i = 0; i < pixelBuffer.size(); i += numChannels)
|
|
std::swap(pixelBuffer[i + channel1], pixelBuffer[i + channel2]);
|
|
|
|
return;
|
|
}
|
|
|
|
BMP BMP::MirrorHorizontally() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Create a new image matching this's metadata
|
|
BMP bmp(size, colormode);
|
|
|
|
// Now copy over the pixels, mirroring it horizontally
|
|
const std::size_t numChannels = GetNumChannels();
|
|
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;
|
|
const std::size_t flippedPixelIndex = rowIndex + (size.x - 1 - x) * numChannels;
|
|
|
|
// Copy over the whole pixel
|
|
std::copy(
|
|
pixelBuffer.cbegin() + flippedPixelIndex,
|
|
pixelBuffer.cbegin() + flippedPixelIndex + numChannels,
|
|
bmp.pixelBuffer.begin() + pixelIndex
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
// return it
|
|
return bmp;
|
|
}
|
|
|
|
BMP BMP::MirrorVertically() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Create a new image matching this's metadata
|
|
BMP bmp(size, colormode);
|
|
|
|
const std::size_t numChannels = GetNumChannels();
|
|
const std::size_t rowLength = size.x * numChannels;
|
|
|
|
// Now iterate over all rows, and copy them over, mirroring it vertically
|
|
for (std::size_t y = 0; y < size.y; y++) {
|
|
const std::size_t rowIndex = y * rowLength;
|
|
const std::size_t flippedRowIndex = (size.y - 1 - y) * rowLength;
|
|
|
|
// Copy over the whole row
|
|
std::copy(
|
|
pixelBuffer.cbegin() + flippedRowIndex,
|
|
pixelBuffer.cbegin() + flippedRowIndex + rowLength,
|
|
bmp.pixelBuffer.begin() + rowIndex
|
|
);
|
|
|
|
}
|
|
|
|
// return it
|
|
return bmp;
|
|
}
|
|
|
|
BMP BMP::Rotate90degClockwise() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Create a new image matching this ones metadata, but with width and height flipped
|
|
BMP bmp(Vector2i(size.y, size.x), colormode);
|
|
|
|
// Now copy over the pixels, rotating it by 90 deg
|
|
const std::size_t numChannels = GetNumChannels();
|
|
for (std::size_t y = 0; y < size.y; y++) {
|
|
for (std::size_t x = 0; x < size.x; x++) {
|
|
const std::size_t pixelIndex = (y * size.x + x) * numChannels;
|
|
const std::size_t rotatedPixelIndex = (x * size.y + (size.y - 1 - y)) * numChannels;
|
|
|
|
// Copy over the whole pixel
|
|
std::copy(
|
|
pixelBuffer.cbegin() + pixelIndex,
|
|
pixelBuffer.cbegin() + pixelIndex + numChannels,
|
|
bmp.pixelBuffer.begin() + rotatedPixelIndex
|
|
);
|
|
}
|
|
}
|
|
|
|
// return it
|
|
return bmp;
|
|
}
|
|
|
|
BMP BMP::Rotate90degCounterclockwise() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Create a new image matching this ones metadata, but with width and height flipped
|
|
BMP bmp(Vector2i(size.y, size.x), colormode);
|
|
|
|
// Now copy over the pixels, rotating it by -90 deg
|
|
const std::size_t numChannels = GetNumChannels();
|
|
for (std::size_t y = 0; y < size.y; y++) {
|
|
for (std::size_t x = 0; x < size.x; x++) {
|
|
const std::size_t pixelIndex = (y * size.x + x) * numChannels;
|
|
const std::size_t rotatedPixelIndex = ((size.x - 1 - x) * size.y + y) * numChannels;
|
|
|
|
// Copy over the whole pixel
|
|
std::copy(
|
|
pixelBuffer.cbegin() + pixelIndex,
|
|
pixelBuffer.cbegin() + pixelIndex + numChannels,
|
|
bmp.pixelBuffer.begin() + rotatedPixelIndex
|
|
);
|
|
}
|
|
}
|
|
|
|
// return it
|
|
return bmp;
|
|
}
|
|
|
|
BMP BMP::Rotate180deg() const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Basically, what we're doing here, is mirroring
|
|
// the image horizontally and vertically at the same time
|
|
|
|
// Create a new image matching this's metadata
|
|
BMP bmp(size, colormode);
|
|
|
|
// Now copy over the pixels, mirroring it horizontally and vertically
|
|
const std::size_t numChannels = GetNumChannels();
|
|
const std::size_t rowLength = size.x * numChannels;
|
|
|
|
for (std::size_t y = 0; y < size.y; y++) {
|
|
const std::size_t rowIndex = y * rowLength;
|
|
const std::size_t flippedRowIndex = (size.y - 1 - y) * rowLength;
|
|
|
|
for (std::size_t x = 0; x < size.x; x++) {
|
|
const std::size_t pixelIndex = rowIndex + x * numChannels;
|
|
const std::size_t rotatedPixelIndex = flippedRowIndex + (size.x - 1 - x) * numChannels;
|
|
|
|
// Copy over the whole pixel
|
|
std::copy(
|
|
pixelBuffer.cbegin() + pixelIndex,
|
|
pixelBuffer.cbegin() + pixelIndex + numChannels,
|
|
bmp.pixelBuffer.begin() + rotatedPixelIndex
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
// return it
|
|
return bmp;
|
|
}
|
|
|
|
void BMP::ConvertColormode(const Colormode &colormode) {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Do we already have the target color mode?
|
|
if (this->colormode == colormode)
|
|
return;
|
|
|
|
// So, we actually have to do something. Darn it.
|
|
// Move the old pixel buffer into a a.. well.. buffer. We're gonna need it
|
|
const std::vector<std::uint8_t> oldPixelBuffer = std::move(pixelBuffer);
|
|
|
|
// Re-initialize our image with our new color mode
|
|
ReInitialize(size, colormode);
|
|
|
|
// What should we convert then?
|
|
switch (colormode) { // this is the PARAMETER!
|
|
// Convert RGBA to RGB.
|
|
// Just drop the alpha channel.
|
|
case Colormode::RGB: {
|
|
// Jump through the old pixel buffer, four bytes at a time
|
|
for (std::size_t i = 0; i < oldPixelBuffer.size(); i += 4) {
|
|
// Per jump, copy three bytes into the new one. These are r,g,b.
|
|
std::copy(
|
|
oldPixelBuffer.cbegin() + i,
|
|
oldPixelBuffer.cbegin() + i + 3,
|
|
pixelBuffer.begin() + i - (i/4) // Subtract one byte per pixel, because the new pixelBuffer is RGB, not RGBA.
|
|
);
|
|
}
|
|
|
|
// Done.
|
|
}
|
|
return;
|
|
|
|
// Convert RGB to RGBA.
|
|
// Just fill the new alpha channel with 0xFF.
|
|
case Colormode::RGBA: {
|
|
// Jump through the old pixel buffer, three bytes at a time
|
|
for (std::size_t i = 0; i < oldPixelBuffer.size(); i += 3) {
|
|
// Per jump, copy these three bytes into the new one. These are r,g,b.
|
|
std::copy(
|
|
oldPixelBuffer.cbegin() + i,
|
|
oldPixelBuffer.cbegin() + i + 3,
|
|
pixelBuffer.begin() + i + (i/3) // Add one byte per pixel, because the new pixelBuffer is RGBA, not RGB.
|
|
);
|
|
}
|
|
|
|
// The alpha channel should already be set to 0xFF, by ReInitialize().
|
|
|
|
// Done.
|
|
}
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
BMP BMP::Crop(const Vector2i &topleft, const Vector2i &cropSize) const {
|
|
CHECK_IF_INITIALIZED
|
|
|
|
// Check that the cropping rect is within our pixel coordinates
|
|
if (
|
|
(topleft.x < 0) || (topleft.y < 0) ||
|
|
(topleft.x + cropSize.x > size.x) || (topleft.y + cropSize.y > size.y)
|
|
)
|
|
throw std::runtime_error("Rect coordinates are not contained inside image!");
|
|
|
|
// Check that the area of the cropping rect is > 0
|
|
if (cropSize.x * cropSize.y == 0)
|
|
throw std::runtime_error("Cropping area is 0!");
|
|
|
|
///////////////////
|
|
// Enough checks...
|
|
|
|
// Create a new image of the rects size, and our color mode
|
|
BMP bmp(cropSize, colormode);
|
|
|
|
// Now copy over our pixel data
|
|
const std::size_t numChannels = GetNumChannels();
|
|
const std::size_t sourceRowLength = size.x * numChannels;
|
|
const std::size_t targetRowLength = bmp.GetDimensions().x * numChannels;
|
|
|
|
for (std::size_t y = topleft.y; y < topleft.y + cropSize.y; y++) {
|
|
const std::size_t sourceRowIndex = y * sourceRowLength;
|
|
const std::size_t targetRowIndex = (y - topleft.y) * targetRowLength;
|
|
|
|
// Now just copy over this entire row(segment)
|
|
std::copy(
|
|
pixelBuffer.cbegin() + sourceRowIndex + topleft.x * numChannels,
|
|
pixelBuffer.cbegin() + sourceRowIndex + (topleft.x * numChannels) + targetRowLength,
|
|
bmp.pixelBuffer.begin() + targetRowIndex
|
|
);
|
|
}
|
|
|
|
// Done.
|
|
return bmp;
|
|
}
|
|
}
|
|
|
|
#undef CHECK_IF_INITIALIZED
|