Skip to content

Commit

Permalink
[Data/ImageFormat] Image loading is now made using stb_image
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Razakhel committed Dec 19, 2023
1 parent 8bda1f9 commit a83282d
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 8 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
66 changes: 59 additions & 7 deletions src/RaZ/Data/ImageFormat.cpp
Original file line number Diff line number Diff line change
@@ -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<void, ImageDataDeleter> 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<float*>(data.get()), valueCount, static_cast<float*>(img.getDataPtr()));
else
std::copy_n(static_cast<uint8_t*>(data.get()), valueCount, static_cast<uint8_t*>(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
Binary file added tests/assets/images/dëfàùltTêst.bmp
Binary file not shown.
Binary file added tests/assets/images/dëfàùltTêst.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/assets/images/dëfàùltTêst.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions tests/assets/images/dëfàùltTêst.pgm
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
P5
# Created by IrfanView
2 2
255
���
5 changes: 5 additions & 0 deletions tests/assets/images/dëfàùltTêst.ppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
P6
# Created by IrfanView
2 2
255
��������￿�
99 changes: 99 additions & 0 deletions tests/src/RaZ/Data/ImageFormat.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "Catch.hpp"

#include "RaZ/Data/Image.hpp"
#include "RaZ/Data/ImageFormat.hpp"

#include <array>

namespace {

constexpr std::array<uint8_t, 4> rawValues = { 191, 239,
239, 191 };

constexpr std::array<uint8_t, 4> jpegValues = { 197, 232,
230, 198 };

void checkImageData(const Raz::Image& loadedImg,
uint8_t expectedChannelCount,
Raz::ImageColorspace expectedColorspace,
const std::array<uint8_t, 4>& 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<uint8_t, 4>& 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);
}

0 comments on commit a83282d

Please sign in to comment.