Specify sf::Image's save format via a scoped enumeration

This commit is contained in:
Chris Thrasher 2024-07-13 13:49:56 -06:00
parent e185f6d53e
commit 40339ff06a
No known key found for this signature in database
GPG Key ID: 56FB686C9DFC8E2C
3 changed files with 106 additions and 144 deletions

View File

@ -36,7 +36,6 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <string_view>
#include <vector> #include <vector>
#include <cstddef> #include <cstddef>
@ -54,6 +53,18 @@ class InputStream;
class SFML_GRAPHICS_API Image class SFML_GRAPHICS_API Image
{ {
public: public:
////////////////////////////////////////////////////////////
/// \brief Supported formats for saving
///
////////////////////////////////////////////////////////////
enum class SaveFormat
{
BMP,
TGA,
PNG,
JPG
};
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Default constructor /// \brief Default constructor
/// ///
@ -220,13 +231,14 @@ public:
/// if it already exists. This function fails if the image is empty. /// if it already exists. This function fails if the image is empty.
/// ///
/// \param filename Path of the file to save /// \param filename Path of the file to save
/// \param format Encoding format to use
/// ///
/// \return True if saving was successful /// \return True if saving was successful
/// ///
/// \see saveToMemory, loadFromFile /// \see saveToMemory, loadFromFile
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
[[nodiscard]] bool saveToFile(const std::filesystem::path& filename) const; [[nodiscard]] bool saveToFile(const std::filesystem::path& filename, SaveFormat format) const;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Save the image to a buffer in memory /// \brief Save the image to a buffer in memory
@ -244,7 +256,7 @@ public:
/// \see saveToFile, loadFromMemory /// \see saveToFile, loadFromMemory
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
[[nodiscard]] std::optional<std::vector<std::uint8_t>> saveToMemory(std::string_view format) const; [[nodiscard]] std::optional<std::vector<std::uint8_t>> saveToMemory(SaveFormat format) const;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Return the size (width and height) of the image /// \brief Return the size (width and height) of the image

View File

@ -317,44 +317,34 @@ bool Image::loadFromStream(InputStream& stream)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
bool Image::saveToFile(const std::filesystem::path& filename) const bool Image::saveToFile(const std::filesystem::path& filename, SaveFormat format) const
{ {
// Make sure the image is not empty // Make sure the image is not empty
if (!m_pixels.empty() && m_size.x > 0 && m_size.y > 0) if (!m_pixels.empty() && m_size.x > 0 && m_size.y > 0)
{ {
// Deduce the image type from its extension
// Extract the extension
const std::filesystem::path extension = filename.extension();
const Vector2i convertedSize = Vector2i(m_size); const Vector2i convertedSize = Vector2i(m_size);
if (extension == ".bmp") switch (format)
{ {
// BMP format case SaveFormat::BMP:
if (stbi_write_bmp(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data())) if (stbi_write_bmp(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data()))
return true; return true;
} break;
else if (extension == ".tga") case SaveFormat::TGA:
{
// TGA format
if (stbi_write_tga(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data())) if (stbi_write_tga(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data()))
return true; return true;
} break;
else if (extension == ".png") case SaveFormat::PNG:
{
// PNG format
if (stbi_write_png(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data(), 0)) if (stbi_write_png(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data(), 0))
return true; return true;
} break;
else if (extension == ".jpg" || extension == ".jpeg") case SaveFormat::JPG:
{
// JPG format
if (stbi_write_jpg(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data(), 90)) if (stbi_write_jpg(filename.string().c_str(), convertedSize.x, convertedSize.y, 4, m_pixels.data(), 90))
return true; return true;
} break;
else default:
{ assert(false && "Invalid format enumeration provided");
err() << "Image file extension " << extension << " not supported\n"; return false;
} }
} }
@ -364,44 +354,39 @@ bool Image::saveToFile(const std::filesystem::path& filename) const
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::optional<std::vector<std::uint8_t>> Image::saveToMemory(std::string_view format) const std::optional<std::vector<std::uint8_t>> Image::saveToMemory(SaveFormat format) const
{ {
// Make sure the image is not empty // Make sure the image is not empty
if (!m_pixels.empty() && m_size.x > 0 && m_size.y > 0) if (!m_pixels.empty() && m_size.x > 0 && m_size.y > 0)
{ {
// Choose function based on format
const std::string specified = toLower(std::string(format));
const Vector2i convertedSize = Vector2i(m_size); const Vector2i convertedSize = Vector2i(m_size);
std::vector<std::uint8_t> buffer; std::vector<std::uint8_t> buffer;
if (specified == "bmp") switch (format)
{ {
// BMP format case SaveFormat::BMP:
if (stbi_write_bmp_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data())) if (stbi_write_bmp_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data()))
return buffer; return buffer;
} break;
else if (specified == "tga") case SaveFormat::TGA:
{
// TGA format
if (stbi_write_tga_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data())) if (stbi_write_tga_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data()))
return buffer; return buffer;
} break;
else if (specified == "png") case SaveFormat::PNG:
{
// PNG format
if (stbi_write_png_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data(), 0)) if (stbi_write_png_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data(), 0))
return buffer; return buffer;
} break;
else if (specified == "jpg" || specified == "jpeg") case SaveFormat::JPG:
{
// JPG format
if (stbi_write_jpg_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data(), 90)) if (stbi_write_jpg_to_func(bufferFromCallback, &buffer, convertedSize.x, convertedSize.y, 4, m_pixels.data(), 90))
return buffer; return buffer;
break;
default:
assert(false && "Invalid format enumeration provided");
return std::nullopt;
} }
} }
err() << "Failed to save image with format " << std::quoted(format) << std::endl; err() << "Failed to save image" << std::endl;
return std::nullopt; return std::nullopt;
} }

View File

@ -117,7 +117,7 @@ TEST_CASE("[Graphics] sf::Image")
SECTION("Successful load") SECTION("Successful load")
{ {
const auto memory = sf::Image({24, 24}, sf::Color::Green).saveToMemory("png").value(); const auto memory = sf::Image({24, 24}, sf::Color::Green).saveToMemory(sf::Image::SaveFormat::PNG).value();
const sf::Image image(memory.data(), memory.size()); const sf::Image image(memory.data(), memory.size());
CHECK(image.getSize() == sf::Vector2u(24, 24)); CHECK(image.getSize() == sf::Vector2u(24, 24));
CHECK(image.getPixelsPtr() != nullptr); CHECK(image.getPixelsPtr() != nullptr);
@ -344,13 +344,7 @@ TEST_CASE("[Graphics] sf::Image")
SECTION("Successful load") SECTION("Successful load")
{ {
const auto memory = []() const auto memory = sf::Image({24, 24}, sf::Color::Green).saveToMemory(sf::Image::SaveFormat::PNG).value();
{
sf::Image savedImage;
savedImage.resize({24, 24}, sf::Color::Green);
return savedImage.saveToMemory("png").value();
}();
CHECK(image.loadFromMemory(memory.data(), memory.size())); CHECK(image.loadFromMemory(memory.data(), memory.size()));
CHECK(image.getSize() == sf::Vector2u(24, 24)); CHECK(image.getSize() == sf::Vector2u(24, 24));
CHECK(image.getPixelsPtr() != nullptr); CHECK(image.getPixelsPtr() != nullptr);
@ -384,24 +378,12 @@ TEST_CASE("[Graphics] sf::Image")
{ {
SECTION("Invalid size") SECTION("Invalid size")
{ {
CHECK(!sf::Image({10, 0}, sf::Color::Magenta).saveToFile("test.jpg")); CHECK(!sf::Image({10, 0}, sf::Color::Magenta).saveToFile("test.jpg", sf::Image::SaveFormat::JPG));
CHECK(!sf::Image({0, 10}, sf::Color::Magenta).saveToFile("test.jpg")); CHECK(!sf::Image({0, 10}, sf::Color::Magenta).saveToFile("test.jpg", sf::Image::SaveFormat::JPG));
} }
const sf::Image image({256, 256}, sf::Color::Magenta); const sf::Image image({256, 256}, sf::Color::Magenta);
SECTION("No extension")
{
CHECK(!image.saveToFile("wheresmyextension"));
CHECK(!image.saveToFile("pls/add/extension"));
}
SECTION("Invalid extension")
{
CHECK(!image.saveToFile("test.ps"));
CHECK(!image.saveToFile("test.foo"));
}
SECTION("Successful save") SECTION("Successful save")
{ {
auto filename = std::filesystem::temp_directory_path(); auto filename = std::filesystem::temp_directory_path();
@ -409,27 +391,32 @@ TEST_CASE("[Graphics] sf::Image")
SECTION("To .bmp") SECTION("To .bmp")
{ {
filename /= "test.bmp"; filename /= "test.bmp";
CHECK(image.saveToFile(filename)); CHECK(image.saveToFile(filename, sf::Image::SaveFormat::BMP));
} }
SECTION("To .tga") SECTION("To .tga")
{ {
filename /= "test.tga"; filename /= "test.tga";
CHECK(image.saveToFile(filename)); CHECK(image.saveToFile(filename, sf::Image::SaveFormat::TGA));
} }
SECTION("To .png") SECTION("To .png")
{ {
filename /= "test.png"; filename /= "test.png";
CHECK(image.saveToFile(filename)); CHECK(image.saveToFile(filename, sf::Image::SaveFormat::PNG));
} }
// Cannot test JPEG encoding due to it triggering UB in stbiw__jpg_writeBits // Cannot test JPEG encoding due to it triggering UB in stbiw__jpg_writeBits
const sf::Image loadedImage(filename); SECTION("Filename without extension")
{
filename /= "test";
CHECK(image.saveToFile(filename, sf::Image::SaveFormat::PNG));
}
const auto loadedImage = sf::Image(filename);
CHECK(loadedImage.getSize() == sf::Vector2u(256, 256)); CHECK(loadedImage.getSize() == sf::Vector2u(256, 256));
CHECK(loadedImage.getPixelsPtr() != nullptr); CHECK(loadedImage.getPixelsPtr() != nullptr);
CHECK(std::filesystem::remove(filename)); CHECK(std::filesystem::remove(filename));
} }
} }
@ -437,72 +424,50 @@ TEST_CASE("[Graphics] sf::Image")
SECTION("saveToMemory()") SECTION("saveToMemory()")
{ {
SECTION("Invalid size") SECTION("Invalid size")
{ CHECK(!sf::Image({10, 0}, sf::Color::Magenta).saveToMemory(sf::Image::SaveFormat::JPG));
CHECK(!sf::Image({10, 0}, sf::Color::Magenta).saveToMemory("test.jpg")); CHECK(!sf::Image({0, 10}, sf::Color::Magenta).saveToMemory(sf::Image::SaveFormat::JPG));
CHECK(!sf::Image({0, 10}, sf::Color::Magenta).saveToMemory("test.jpg"));
}
const sf::Image image({16, 16}, sf::Color::Magenta);
SECTION("No extension")
{
CHECK(!image.saveToMemory(""));
}
SECTION("Invalid extension")
{
CHECK(!image.saveToMemory("."));
CHECK(!image.saveToMemory("gif"));
CHECK(!image.saveToMemory(".jpg")); // Supposed to be "jpg"
} }
SECTION("Successful save") SECTION("Successful save")
{ {
std::optional<std::vector<std::uint8_t>> maybeOutput; const sf::Image image({16, 16}, sf::Color::Magenta);
SECTION("To bmp") SECTION("To bmp")
{ {
maybeOutput = image.saveToMemory("bmp"); const auto memory = image.saveToMemory(sf::Image::SaveFormat::BMP).value();
REQUIRE(maybeOutput.has_value()); REQUIRE(memory.size() == 1146);
const auto& output = *maybeOutput; CHECK(memory[0] == 66);
REQUIRE(output.size() == 1146); CHECK(memory[1] == 77);
CHECK(output[0] == 66); CHECK(memory[2] == 122);
CHECK(output[1] == 77); CHECK(memory[3] == 4);
CHECK(output[2] == 122); CHECK(memory[1000] == 255);
CHECK(output[3] == 4); CHECK(memory[1001] == 255);
CHECK(output[1000] == 255); CHECK(memory[1002] == 255);
CHECK(output[1001] == 255); CHECK(memory[1003] == 0);
CHECK(output[1002] == 255);
CHECK(output[1003] == 0);
} }
SECTION("To tga") SECTION("To tga")
{ {
maybeOutput = image.saveToMemory("tga"); const auto memory = image.saveToMemory(sf::Image::SaveFormat::TGA).value();
REQUIRE(maybeOutput.has_value()); REQUIRE(memory.size() == 98);
const auto& output = *maybeOutput; CHECK(memory[0] == 0);
REQUIRE(output.size() == 98); CHECK(memory[1] == 0);
CHECK(output[0] == 0); CHECK(memory[2] == 10);
CHECK(output[1] == 0); CHECK(memory[3] == 0);
CHECK(output[2] == 10);
CHECK(output[3] == 0);
} }
SECTION("To png") SECTION("To png")
{ {
maybeOutput = image.saveToMemory("png"); const auto memory = image.saveToMemory(sf::Image::SaveFormat::PNG).value();
REQUIRE(maybeOutput.has_value()); REQUIRE(memory.size() == 92);
const auto& output = *maybeOutput; CHECK(memory[0] == 137);
REQUIRE(output.size() == 92); CHECK(memory[1] == 80);
CHECK(output[0] == 137); CHECK(memory[2] == 78);
CHECK(output[1] == 80); CHECK(memory[3] == 71);
CHECK(output[2] == 78);
CHECK(output[3] == 71);
} }
// Cannot test JPEG encoding due to it triggering UB in stbiw__jpg_writeBits // Cannot test JPEG encoding due to it triggering UB in stbiw__jpg_writeBits
} }
}
SECTION("Set/get pixel") SECTION("Set/get pixel")
{ {