-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
Showing
8 changed files
with
169 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
P5 | ||
# Created by IrfanView | ||
2 2 | ||
255 | ||
��� |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
P6 | ||
# Created by IrfanView | ||
2 2 | ||
255 | ||
��������� |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |