From 94c73192025a5d9f5d12fb2819d2306a72c2f136 Mon Sep 17 00:00:00 2001 From: Qznad <110358650+Qznad@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:02:01 +0100 Subject: [PATCH] feat(decoder): add shared JPEG encoder and decoder in core/Decoder/Jpeg --- CMakeLists.txt | 1 + core/Decoder/Jpeg/CMakeLists.txt | 13 ++++ core/Decoder/Jpeg/include/Decoder/Jpeg.hpp | 25 +++++++ core/Decoder/Jpeg/src/Jpeg.cpp | 65 ++++++++++++++++++ core/Decoder/Jpeg/tests/Jpeg.cpp | 77 ++++++++++++++++++++++ core/libs/prx/libSceJpegEnc/CMakeLists.txt | 1 - 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 core/Decoder/Jpeg/CMakeLists.txt create mode 100644 core/Decoder/Jpeg/include/Decoder/Jpeg.hpp create mode 100644 core/Decoder/Jpeg/src/Jpeg.cpp create mode 100644 core/Decoder/Jpeg/tests/Jpeg.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 008b7a847..3751edef8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,5 +45,6 @@ add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL) set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON) add_subdirectory(core/shader/recompiler) +add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/libs) add_subdirectory(core/relinker) diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt new file mode 100644 index 000000000..f01189479 --- /dev/null +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library(decoder_jpeg STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Jpeg.cpp) +target_include_directories(decoder_jpeg PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(decoder_jpeg SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/stb) +set_target_properties(decoder_jpeg PROPERTIES + CXX_EXTENSIONS OFF + POSITION_INDEPENDENT_CODE ON +) + +if(BUILD_TESTING) + add_executable(decoder_jpeg_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Jpeg.cpp) + target_link_libraries(decoder_jpeg_tests PRIVATE decoder_jpeg) + add_test(NAME decoder_jpeg COMMAND decoder_jpeg_tests) +endif() diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp new file mode 100644 index 000000000..330aaeb77 --- /dev/null +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -0,0 +1,25 @@ +#ifndef DECODER_JPEG_HPP +#define DECODER_JPEG_HPP + +#include +#include +#include +#include + +namespace Decoder::Jpeg { + +struct Image { + std::uint32_t width; + std::uint32_t height; + std::uint32_t channels; + std::vector pixels; +}; + +std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, + std::uint32_t channels, int quality); + +std::optional Decode(std::span jpeg); + +} // namespace Decoder::Jpeg + +#endif // DECODER_JPEG_HPP diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp new file mode 100644 index 000000000..788a866b7 --- /dev/null +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -0,0 +1,65 @@ +#include "Decoder/Jpeg.hpp" + +#include +#include +#include + +#define STB_IMAGE_WRITE_STATIC +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STBI_WRITE_NO_STDIO +#include "stb_image_write.h" + +#define STB_IMAGE_STATIC +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ONLY_JPEG +#define STBI_NO_STDIO +#include "stb_image.h" + +namespace Decoder::Jpeg { + +namespace { + +constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; + +void appendBytes(void* context, void* data, int size) { + auto* output = static_cast*>(context); + const auto* bytes = static_cast(data); + output->insert(output->end(), bytes, bytes + size); +} + +} // namespace + +std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, + std::uint32_t channels, int quality) { + if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { + throw std::invalid_argument("Jpeg::Encode: unsupported image size"); + } + if (channels != 1 && channels != 3) throw std::invalid_argument("Jpeg::Encode: channels must be 1 or 3"); + if (quality < 1 || quality > 100) throw std::invalid_argument("Jpeg::Encode: quality must be 1-100"); + if (pixels.size() < static_cast(width) * height * channels) { + throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); + } + + std::vector output; + const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast(width), static_cast(height), + static_cast(channels), pixels.data(), quality); + if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed"); + return output; +} + +std::optional Decode(std::span jpeg) { + if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0); + if (!decoded) return std::nullopt; + + Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; + image.pixels.assign(decoded, decoded + static_cast(width) * height * channels); + stbi_image_free(decoded); + return image; +} + +} // namespace Decoder::Jpeg diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp new file mode 100644 index 000000000..38e2aa42d --- /dev/null +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -0,0 +1,77 @@ +#include "Decoder/Jpeg.hpp" + +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool ThrowsInvalidArgument(TFunction function) { + try { + function(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static bool IsJpeg(const std::vector& data) { + return data.size() > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[data.size() - 2] == 0xFF && data.back() == 0xD9; +} + +static int Difference(std::uint8_t left, std::uint8_t right) { + return left > right ? left - right : right - left; +} + +int main() { + constexpr std::uint32_t width = 32; + constexpr std::uint32_t height = 24; + + std::vector rgb(width * height * 3); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + std::uint8_t* pixel = &rgb[(y * width + x) * 3]; + pixel[0] = static_cast(x * 8); + pixel[1] = static_cast(y * 10); + pixel[2] = 128; + } + } + + const std::vector rgbJpeg = Decoder::Jpeg::Encode(rgb, width, height, 3, 90); + Require(IsJpeg(rgbJpeg)); + const auto rgbImage = Decoder::Jpeg::Decode(rgbJpeg); + Require(rgbImage.has_value()); + Require(rgbImage->width == width && rgbImage->height == height && rgbImage->channels == 3); + Require(rgbImage->pixels.size() == rgb.size()); + long totalError = 0; + for (std::size_t i = 0; i < rgb.size(); ++i) totalError += Difference(rgb[i], rgbImage->pixels[i]); + Require(totalError / static_cast(rgb.size()) < 4); + + std::vector gray(width * height); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) gray[y * width + x] = static_cast((x + y) * 4); + } + const std::vector grayJpeg = Decoder::Jpeg::Encode(gray, width, height, 1, 90); + Require(IsJpeg(grayJpeg)); + const auto grayImage = Decoder::Jpeg::Decode(grayJpeg); + Require(grayImage.has_value()); + Require(grayImage->width == width && grayImage->height == height); + totalError = 0; + for (std::size_t i = 0; i < gray.size(); ++i) totalError += Difference(gray[i], grayImage->pixels[i * grayImage->channels]); + Require(totalError / static_cast(gray.size()) < 4); + + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 4, 90); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 0); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 101); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0, height, 3, 90); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0x10000, 1, 3, 90); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height + 1, 3, 90); })); + + Require(!Decoder::Jpeg::Decode({}).has_value()); + const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; + Require(!Decoder::Jpeg::Decode(garbage).has_value()); + + return 0; +} diff --git a/core/libs/prx/libSceJpegEnc/CMakeLists.txt b/core/libs/prx/libSceJpegEnc/CMakeLists.txt index faa7b7367..16a34878c 100644 --- a/core/libs/prx/libSceJpegEnc/CMakeLists.txt +++ b/core/libs/prx/libSceJpegEnc/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(libSceJpegEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceJpegEnc PRIVATE ${LIBS_INCLUDE_DIR}) -target_include_directories(libSceJpegEnc SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/3rdparty/stb) set_target_properties(libSceJpegEnc PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceJpegEnc)