From a83282d3a3cd6c31fdcee8332e34bee36e984242 Mon Sep 17 00:00:00 2001 From: Razakhel Date: Mon, 18 Dec 2023 23:01:15 +0100 Subject: [PATCH] [Data/ImageFormat] Image loading is now made using stb_image - Loading an image through ImageFormat always uses stb_image - This adds many more supported formats and will avoid maintenance on the existing ones - The PngFormat & TgaFormat custom loaders remain for now, but aren't used by ImageFormat::load() anymore - Added unit tests to check basic image loading with different formats --- CMakeLists.txt | 2 +- src/RaZ/Data/ImageFormat.cpp | 66 ++++++++++-- ...303\253f\303\240\303\271ltT\303\252st.bmp" | Bin 0 -> 70 bytes ...303\253f\303\240\303\271ltT\303\252st.gif" | Bin 0 -> 810 bytes ...303\253f\303\240\303\271ltT\303\252st.jpg" | Bin 0 -> 658 bytes ...303\253f\303\240\303\271ltT\303\252st.pgm" | 5 + ...303\253f\303\240\303\271ltT\303\252st.ppm" | 5 + tests/src/RaZ/Data/ImageFormat.cpp | 99 ++++++++++++++++++ 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 "tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.bmp" create mode 100644 "tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.gif" create mode 100644 "tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.jpg" create mode 100644 "tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.pgm" create mode 100644 "tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.ppm" create mode 100644 tests/src/RaZ/Data/ImageFormat.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 22f61000..838cee5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,7 +451,7 @@ else () target_compile_definitions(RaZ PUBLIC RAZ_NO_LUA) endif () -target_link_libraries(RaZ PRIVATE fastgltf simdjson) +target_link_libraries(RaZ PRIVATE fastgltf simdjson stb) # Compiling RaZ's sources target_sources(RaZ PRIVATE ${RAZ_FILES}) diff --git a/src/RaZ/Data/ImageFormat.cpp b/src/RaZ/Data/ImageFormat.cpp index 2a622f60..cdbf3d8d 100644 --- a/src/RaZ/Data/ImageFormat.cpp +++ b/src/RaZ/Data/ImageFormat.cpp @@ -1,30 +1,82 @@ #include "RaZ/Data/Image.hpp" #include "RaZ/Data/ImageFormat.hpp" #include "RaZ/Data/PngFormat.hpp" -#include "RaZ/Data/TgaFormat.hpp" #include "RaZ/Utils/FilePath.hpp" +#include "RaZ/Utils/Logger.hpp" #include "RaZ/Utils/StrUtils.hpp" +#define STB_IMAGE_IMPLEMENTATION +#define STBI_FAILURE_USERMSG +#define STBI_WINDOWS_UTF8 +#include "stb_image.h" + namespace Raz::ImageFormat { +namespace { + +struct ImageDataDeleter { + void operator()(void* data) noexcept { stbi_image_free(data); } +}; + +ImageColorspace recoverColorspace(int channelCount) { + switch (channelCount) { + case 1: return ImageColorspace::GRAY; + case 2: return ImageColorspace::GRAY_ALPHA; + case 3: return ImageColorspace::RGB; + case 4: return ImageColorspace::RGBA; + default: + throw std::invalid_argument("Error: Unsupported number of channels."); + } +} + +} // namespace + Image load(const FilePath& filePath, bool flipVertically) { - const std::string fileExt = StrUtils::toLowercaseCopy(filePath.recoverExtension().toUtf8()); + Logger::debug("[ImageFormat] Loading image '" + filePath + "'..."); - if (fileExt == "png") - return PngFormat::load(filePath, flipVertically); - else if (fileExt == "tga") - return TgaFormat::load(filePath, flipVertically); + const std::string fileStr = filePath.toUtf8(); + const bool isHdr = stbi_is_hdr(fileStr.c_str()); + + stbi_set_flip_vertically_on_load(flipVertically); + + int width {}; + int height {}; + int channelCount {}; + std::unique_ptr data; - throw std::invalid_argument("[ImageFormat] Unsupported image file extension '" + fileExt + "' for loading."); + if (isHdr) + data.reset(stbi_loadf(fileStr.c_str(), &width, &height, &channelCount, 0)); + else + data.reset(stbi_load(fileStr.c_str(), &width, &height, &channelCount, 0)); + + if (data == nullptr) + throw std::invalid_argument("[ImageFormat] Cannot load image '" + filePath + "': " + stbi_failure_reason()); + + const std::size_t valueCount = width * height * channelCount; + + Image img(width, height, recoverColorspace(channelCount), (isHdr ? ImageDataType::FLOAT : ImageDataType::BYTE)); + + if (isHdr) + std::copy_n(static_cast(data.get()), valueCount, static_cast(img.getDataPtr())); + else + std::copy_n(static_cast(data.get()), valueCount, static_cast(img.getDataPtr())); + + Logger::debug("[ImageFormat] Loaded image"); + + return img; } void save(const FilePath& filePath, const Image& image, bool flipVertically) { + Logger::debug("[ImageFormat] Saving image to '" + filePath + "'..."); + const std::string fileExt = StrUtils::toLowercaseCopy(filePath.recoverExtension().toUtf8()); if (fileExt == "png") PngFormat::save(filePath, image, flipVertically); else throw std::invalid_argument("[ImageFormat] Unsupported image file extension '" + fileExt + "' for saving."); + + Logger::debug("[ImageFormat] Saved image"); } } // namespace Raz::ImageFormat diff --git "a/tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.bmp" "b/tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.bmp" new file mode 100644 index 0000000000000000000000000000000000000000..0c46852158989b50566d5cc2e876e15a84c2eb36 GIT binary patch literal 70 scmZ?rbz^`4Ga#h_#7t1k$e;jZLBJ6{1_ls@f%os<|KGoVKNy490Q~zA4FCWD literal 0 HcmV?d00001 diff --git "a/tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.gif" "b/tests/assets/images/d\303\253f\303\240\303\271ltT\303\252st.gif" new file mode 100644 index 0000000000000000000000000000000000000000..50317439144178b6d321c0fce3e0306010c1116a GIT binary patch literal 810 zcmZ?wbhEHbWMW`q_|CwvfB*jX@82_wg3%Bdks+Y?lLhQ=9S{l16YL!942&!s3``2_j6xdp@o1cgOJMMZh|#U;coyG6VInuyV4pa*FVB^NNrR z{vTivwh= zDOELf4NWZ*Q!{f5ODks=S2uSLPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ z&aO$5r%atTea6gLixw|gx@`H1m8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds z_3+W-Cr_U}fAR9w$4{TXeEs(Q$Io9Ne=#yJL%ap|8JfQYf&OA*VPR%r2lxYE z#}NLy#lXYN2#h>tK?Zw<-@=jCO1W*5esSopOK8%3;_ + +namespace { + +constexpr std::array rawValues = { 191, 239, + 239, 191 }; + +constexpr std::array jpegValues = { 197, 232, + 230, 198 }; + +void checkImageData(const Raz::Image& loadedImg, + uint8_t expectedChannelCount, + Raz::ImageColorspace expectedColorspace, + const std::array& expectedValues) { + REQUIRE(loadedImg.getWidth() == 2); + REQUIRE(loadedImg.getHeight() == 2); + REQUIRE(loadedImg.getChannelCount() == expectedChannelCount); + REQUIRE(loadedImg.getColorspace() == expectedColorspace); + REQUIRE(loadedImg.getDataType() == Raz::ImageDataType::BYTE); + REQUIRE_FALSE(loadedImg.isEmpty()); + + CHECK(loadedImg.recoverByteValue(0, 0, 0) == expectedValues[0]); + CHECK(loadedImg.recoverByteValue(1, 0, 0) == expectedValues[1]); + CHECK(loadedImg.recoverByteValue(0, 1, 0) == expectedValues[2]); + CHECK(loadedImg.recoverByteValue(1, 1, 0) == expectedValues[3]); + + if (expectedChannelCount >= 3) { + CHECK(loadedImg.recoverByteValue(0, 0, 1) == expectedValues[0]); + CHECK(loadedImg.recoverByteValue(0, 0, 2) == expectedValues[0]); + + CHECK(loadedImg.recoverByteValue(1, 0, 1) == expectedValues[1]); + CHECK(loadedImg.recoverByteValue(1, 0, 2) == expectedValues[1]); + + CHECK(loadedImg.recoverByteValue(0, 1, 1) == expectedValues[2]); + CHECK(loadedImg.recoverByteValue(0, 1, 2) == expectedValues[2]); + + CHECK(loadedImg.recoverByteValue(1, 1, 1) == expectedValues[3]); + CHECK(loadedImg.recoverByteValue(1, 1, 2) == expectedValues[3]); + } + + if (expectedColorspace == Raz::ImageColorspace::GRAY_ALPHA || expectedColorspace == Raz::ImageColorspace::RGBA) { + const uint8_t alphaChannelIndex = (expectedChannelCount == 2 ? 1 : 3); + CHECK(loadedImg.recoverByteValue(0, 0, alphaChannelIndex) == 255); + CHECK(loadedImg.recoverByteValue(1, 0, alphaChannelIndex) == 255); + CHECK(loadedImg.recoverByteValue(0, 1, alphaChannelIndex) == 255); + CHECK(loadedImg.recoverByteValue(1, 1, alphaChannelIndex) == 255); + } +} + +void checkImage(const Raz::FilePath& filePath, uint8_t expectedChannelCount, const std::array& expectedValues) { + const Raz::ImageColorspace expectedColorspace = (expectedChannelCount == 4 ? Raz::ImageColorspace::RGBA + : (expectedChannelCount == 3 ? Raz::ImageColorspace::RGB + : (expectedChannelCount == 2 ? Raz::ImageColorspace::GRAY_ALPHA + : Raz::ImageColorspace::GRAY))); + + checkImageData(Raz::ImageFormat::load(filePath), + expectedChannelCount, + expectedColorspace, + expectedValues); + checkImageData(Raz::ImageFormat::load(filePath, true), + expectedChannelCount, + expectedColorspace, + { expectedValues[2], expectedValues[3], expectedValues[0], expectedValues[1] }); +} + +} // namespace + +TEST_CASE("ImageFormat load BMP") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.bmp", 4, rawValues); +} + +TEST_CASE("ImageFormat load GIF") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.gif", 4, rawValues); +} + +TEST_CASE("ImageFormat load JPEG") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.jpg", 3, jpegValues); +} + +TEST_CASE("ImageFormat load PGM") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.pgm", 1, rawValues); +} + +TEST_CASE("ImageFormat load PNG") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.png", 4, rawValues); +} + +TEST_CASE("ImageFormat load PPM") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.ppm", 3, rawValues); +} + +TEST_CASE("ImageFormat load TGA") { + checkImage(RAZ_TESTS_ROOT "assets/images/dëfàùltTêst.tga", 3, rawValues); +}