From 04a1104065faa86255d95d4ee6e781a28ae45576 Mon Sep 17 00:00:00 2001 From: Leonetienne Date: Sun, 6 Mar 2022 12:45:09 +0100 Subject: [PATCH] Added tests for write, and added tests/implementation of operator==. Also added implementation of FillChannel() --- Src/BMP.cpp | 46 +++++++++++++++-- Src/BMP.h | 13 ++++- Src/BmpWriter.cpp | 4 +- Test/CMakeLists.txt | 2 + Test/OperatorEquals.cpp | 55 ++++++++++++++++++++ Test/ReInitialize.cpp | 4 +- Test/Uninitialized.cpp | 2 +- Test/Write.cpp | 109 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 Test/OperatorEquals.cpp create mode 100644 Test/Write.cpp diff --git a/Src/BMP.cpp b/Src/BMP.cpp index d1d5fee..abcf1a3 100644 --- a/Src/BMP.cpp +++ b/Src/BMP.cpp @@ -37,7 +37,11 @@ namespace Leonetienne::BmpPP { // Re-initialize the pixelbuffer pixelBuffer.clear(); - pixelBuffer.resize(size.x * size.y * GetNumColorChannels()); + 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; } @@ -51,7 +55,11 @@ namespace Leonetienne::BmpPP { // Re-initialize the pixelbuffer pixelBuffer.clear(); - pixelBuffer.resize(size.x * size.y * GetNumColorChannels()); + 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; } @@ -70,7 +78,7 @@ namespace Leonetienne::BmpPP { CHECK_IF_INITIALIZED const std::size_t pixelIndex = - (position.y * size.x + position.x) * GetNumColorChannels(); + (position.y * size.x + position.x) * GetNumChannels(); if (pixelIndex >= pixelBuffer.size()) throw std::runtime_error("Pixel index out of range!"); @@ -82,7 +90,7 @@ namespace Leonetienne::BmpPP { CHECK_IF_INITIALIZED const std::size_t pixelIndex = - (position.y * size.x + position.x) * GetNumColorChannels(); + (position.y * size.x + position.x) * GetNumChannels(); if (pixelIndex >= pixelBuffer.size()) throw std::runtime_error("Pixel index out of range!"); @@ -142,7 +150,7 @@ namespace Leonetienne::BmpPP { return colormode; } - std::size_t BMP::GetNumColorChannels() const { + std::size_t BMP::GetNumChannels() const { CHECK_IF_INITIALIZED switch (colormode) { @@ -167,6 +175,34 @@ namespace Leonetienne::BmpPP { return isInitialized; } + bool BMP::operator==(const BMP &other) const { + // 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) { + const std::size_t numChannels = GetNumChannels(); + + for (std::size_t i = 0; i < pixelBuffer.size(); i += numChannels) + pixelBuffer[i + channel] = value; + + return; + } + } #undef CHECK_IF_INITIALIZED diff --git a/Src/BMP.h b/Src/BMP.h index 0e52054..676ba69 100644 --- a/Src/BMP.h +++ b/Src/BMP.h @@ -50,8 +50,8 @@ namespace Leonetienne::BmpPP { //! Will return the color mode of the image [[nodiscard]] const Colormode& GetColormode() const; - //! Will return the amount of color channels used - [[nodiscard]] std::size_t GetNumColorChannels() const; + //! Will return the amount of channels used + [[nodiscard]] std::size_t GetNumChannels() const; //! Will return the size of the raw pixel buffer, in bytes [[nodiscard]] std::size_t GetPixelbufferSize() const; @@ -67,6 +67,12 @@ namespace Leonetienne::BmpPP { //! Returns false, if unable to open, or parse, file bool Read(const std::string& filename); + //! Will compare two images for being exactly identical regarding resolution, bit depth, and pixel values. + bool operator==(const BMP& other) const; + + //! Will compare two images for not being exactly identical regarding resolution, bit depth, and pixel values. + bool operator!=(const BMP& other) const; + //! Will mirror the image horizontally void MirrorHorizontally(); @@ -91,6 +97,9 @@ namespace Leonetienne::BmpPP { //! Will copy the specified rectangle-area, and return it as a new image BMP Crop(const Eule::Rect& area); + //! Will fill a specific channel with a value + void FillChannel(const std::size_t& channel, const std::uint8_t value); + private: Eule::Vector2i size; Colormode colormode; diff --git a/Src/BmpWriter.cpp b/Src/BmpWriter.cpp index c339a93..53ae35c 100644 --- a/Src/BmpWriter.cpp +++ b/Src/BmpWriter.cpp @@ -21,7 +21,7 @@ namespace Leonetienne::BmpPP { // Populate dib header bmpHeader.dibHeader.imageWidth = image.size.x; bmpHeader.dibHeader.imageHeight = image.size.y; - bmpHeader.dibHeader.numBitsPerPixel = image.GetNumColorChannels() * 8; + bmpHeader.dibHeader.numBitsPerPixel = image.GetNumChannels() * 8; // The size of the pixel array is not known yet (because rows require to be padded) @@ -30,7 +30,7 @@ namespace Leonetienne::BmpPP { packedPixels.reserve(image.pixelBuffer.size()); // How many channels do we have? - const std::size_t numChannels = image.GetNumColorChannels(); + const std::size_t numChannels = image.GetNumChannels(); // Calculate how many padding bytes to add per row const std::size_t paddingBytesPerRow = (4 - ((image.size.x * numChannels) % 4)) % 4; diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt index d4d3853..777aa67 100644 --- a/Test/CMakeLists.txt +++ b/Test/CMakeLists.txt @@ -22,6 +22,8 @@ add_executable(Test ReInitialize.cpp Uninitialized.cpp Read.cpp + Write.cpp + OperatorEquals.cpp ) # Move test images to build dir diff --git a/Test/OperatorEquals.cpp b/Test/OperatorEquals.cpp new file mode 100644 index 0000000..151dcae --- /dev/null +++ b/Test/OperatorEquals.cpp @@ -0,0 +1,55 @@ +#include +#include +#include "Catch2.h" + +using namespace Leonetienne::BmpPP; +using namespace Eule; + +// Don't have to test operator not equal, because it just returns the opposite of this + +// Tests that two RGB images containing almost every possible color are equal, after being copied +TEST_CASE(__FILE__"/CopiedImagesAreEqual", "[OperatorEqual]") +{ + // Read a gradient image + BMP bmp_a("base_gradient.bmp"); + + // Copy it + BMP bmp_b = bmp_a; + + // Assert that they are equal + REQUIRE(bmp_a == bmp_b); + return; +} + +// Tests that changing a single pixel channel results in them not being equal anymore +TEST_CASE(__FILE__"/OneDifferingValueMakesUnequal", "[OperatorEqual]") +{ + // Read a gradient image + BMP bmp_a("base_gradient.bmp"); + + // Copy it + BMP bmp_b = bmp_a; + + // Bop it + *(bmp_a.GetPixel(bmp_a.GetDimensions() / 2) + 1) = 69; + + // Assert that they are equal + REQUIRE_FALSE(bmp_a == bmp_b); + return; +} + +// Tests that two images with the exact same pixelbuffer, but differing metadata are not equal +TEST_CASE(__FILE__"/SamePixelbufferButDifferentMetadataUnequal", "[OperatorEqual]") +{ + // Create image a + BMP bmp_a(Vector2i(800, 600), Colormode::RGB); // 1440000 values of 0 + + // Create image b + BMP bmp_b(Vector2i(600, 600), Colormode::RGBA); // Also 1440000 values of 0 + bmp_b.FillChannel(3, 0); // Make sure the alpha channel actually is zeroed + + // They only differ by their metadata. Not by pixel data. Make sure they are not euqal. + REQUIRE_FALSE(bmp_a == bmp_b); + + return; +} diff --git a/Test/ReInitialize.cpp b/Test/ReInitialize.cpp index 5dec085..1048cf2 100644 --- a/Test/ReInitialize.cpp +++ b/Test/ReInitialize.cpp @@ -12,7 +12,7 @@ TEST_CASE(__FILE__"/ReInitialize", "[ReInitialize]") SECTION("Check that the initial values are OK") { REQUIRE(bmp.GetDimensions().x == 800); REQUIRE(bmp.GetDimensions().y == 600); - REQUIRE(bmp.GetNumColorChannels() == 4); + REQUIRE(bmp.GetNumChannels() == 4); REQUIRE(bmp.GetColormode() == Colormode::RGBA); REQUIRE(bmp.GetPixelbufferSize() == 800*600*4); } @@ -23,7 +23,7 @@ TEST_CASE(__FILE__"/ReInitialize", "[ReInitialize]") SECTION("Check that getters now return the updated values") { REQUIRE(bmp.GetDimensions().x == 1920); REQUIRE(bmp.GetDimensions().y == 1080); - REQUIRE(bmp.GetNumColorChannels() == 3); + REQUIRE(bmp.GetNumChannels() == 3); REQUIRE(bmp.GetColormode() == Colormode::RGB); REQUIRE(bmp.GetPixelbufferSize() == 1920*1080*3); } diff --git a/Test/Uninitialized.cpp b/Test/Uninitialized.cpp index 3a8a090..0b2fd79 100644 --- a/Test/Uninitialized.cpp +++ b/Test/Uninitialized.cpp @@ -37,7 +37,7 @@ TEST_CASE(__FILE__"/RuntimeErrorOnUninitialized", "[Uninitialized]") ); REQUIRE_THROWS_AS( - bmp.GetNumColorChannels() + bmp.GetNumChannels() , std::runtime_error ); diff --git a/Test/Write.cpp b/Test/Write.cpp new file mode 100644 index 0000000..86537ed --- /dev/null +++ b/Test/Write.cpp @@ -0,0 +1,109 @@ +#include +#include "Catch2.h" +#include +#include + +using namespace Leonetienne::BmpPP; +using namespace Eule; + +#define IMSIZE Vector2i(800, 600) + +namespace { + inline std::tuple + ColorGradient(const Vector2i& pos) + { + std::uint8_t r, g, b, a; + + // This assumes IMSIZE.x >= IMSIZE.y + + r = ((float)pos.x / (float)IMSIZE.x) * 255.0f; + g = (1.0f - (float)pos.x / (float)IMSIZE.x) * 255.0f; + b = (1.0f - (float)pos.y / (float)IMSIZE.x) * 255.0f; + a = Math::Clamp(((float)pos.y / (float)IMSIZE.x) * 2 * 255.0f, 0.0, 255.0); + + return std::make_tuple(r, g, b, a); + } +} + +// Tests that writing an image works at all (without crashing the program) +TEST_CASE(__FILE__"/WritingDoesntCrash", "[Write]") +{ + SECTION("RGB image") { + // Create a new RGB image + BMP bmp(IMSIZE, Colormode::RGB); + + // Write it to a file + bmp.Write("test_artifact.bmp"); + } + + SECTION("RGBA image") { + // Create a new RGB image + BMP bmp(IMSIZE, Colormode::RGBA); + + // Write it to a file + bmp.Write("test_artifact.bmp"); + } + + return; +} + +// Tests that writing a file will write the correct image data +TEST_CASE(__FILE__"/WillWriteTheCorrectData", "[Write]") +{ + SECTION("RGB image") { + // Create a new RGB image + BMP bmp(IMSIZE, Colormode::RGB); + + // Populate it with colors + for (std::size_t x = 0; x < bmp.GetDimensions().x; x++) + for (std::size_t y = 0; y < bmp.GetDimensions().y; y++) { + const auto px = ColorGradient(Vector2i(x, y)); + + bmp.SetPixel( + Vector2i(x, y), + std::get<0>(px), + std::get<1>(px), + std::get<2>(px) + ); + } + + // Write it to a file + bmp.Write("test_artifact_rgb_gradient.bmp"); + + // Read it back in (reading function is tested independently) + BMP readBmp("test_artifact_rgb_gradient.bmp"); + + // Compare them + REQUIRE(bmp == readBmp); + } + + SECTION("RGBA image") { + // Create a new RGB image + BMP bmp(IMSIZE, Colormode::RGBA); + + // Populate it with colors + for (std::size_t x = 0; x < bmp.GetDimensions().x; x++) + for (std::size_t y = 0; y < bmp.GetDimensions().y; y++) { + const auto px = ColorGradient(Vector2i(x, y)); + + bmp.SetPixel( + Vector2i(x, y), + std::get<0>(px), + std::get<1>(px), + std::get<2>(px), + std::get<3>(px) + ); + } + + // Write it to a file + bmp.Write("test_artifact_rgba_gradient.bmp"); + + // Read it back in (reading function is tested independently) + BMP readBmp("test_artifact_rgba_gradient.bmp"); + + // Compare them + REQUIRE(bmp == readBmp); + } + + return; +}