feat(decoder): add shared JPEG encoder and decoder in core/Decoder/Jpeg
This commit is contained in:
parent
aa56049fbf
commit
94c7319202
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#ifndef DECODER_JPEG_HPP
|
||||
#define DECODER_JPEG_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace Decoder::Jpeg {
|
||||
|
||||
struct Image {
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t channels;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t channels, int quality);
|
||||
|
||||
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg);
|
||||
|
||||
} // namespace Decoder::Jpeg
|
||||
|
||||
#endif // DECODER_JPEG_HPP
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#include "Decoder/Jpeg.hpp"
|
||||
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
|
||||
#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<std::vector<std::uint8_t>*>(context);
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
output->insert(output->end(), bytes, bytes + size);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> 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<std::size_t>(width) * height * channels) {
|
||||
throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> output;
|
||||
const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast<int>(width), static_cast<int>(height),
|
||||
static_cast<int>(channels), pixels.data(), quality);
|
||||
if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed");
|
||||
return output;
|
||||
}
|
||||
|
||||
std::optional<Image> Decode(std::span<const std::uint8_t> 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<int>(jpeg.size()), &width, &height, &channels, 0);
|
||||
if (!decoded) return std::nullopt;
|
||||
|
||||
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels), {}};
|
||||
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(width) * height * channels);
|
||||
stbi_image_free(decoded);
|
||||
return image;
|
||||
}
|
||||
|
||||
} // namespace Decoder::Jpeg
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#include "Decoder/Jpeg.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
template<typename TFunction>
|
||||
static bool ThrowsInvalidArgument(TFunction function) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::invalid_argument&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsJpeg(const std::vector<std::uint8_t>& 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<std::uint8_t> 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<std::uint8_t>(x * 8);
|
||||
pixel[1] = static_cast<std::uint8_t>(y * 10);
|
||||
pixel[2] = 128;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> 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<long>(rgb.size()) < 4);
|
||||
|
||||
std::vector<std::uint8_t> 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<std::uint8_t>((x + y) * 4);
|
||||
}
|
||||
const std::vector<std::uint8_t> 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<long>(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<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
Require(!Decoder::Jpeg::Decode(garbage).has_value());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue