From ba0e93daac4236603c5cff4f9521d09f48805ad2 Mon Sep 17 00:00:00 2001 From: nmzik Date: Fri, 23 Oct 2026 18:00:48 +0200 Subject: [PATCH] libs: implement JpegDec and JpegEnc --- CMakeLists.txt | 12 + src/graphics/presentation/window/window.cpp | 2 - src/libs/libJpegDec.cpp | 402 ++++++++++++++++++++ src/libs/libJpegEnc.cpp | 240 ++++++++++++ src/libs/libs.cpp | 4 + tests/JpegDecTests.cpp | 342 +++++++++++++++++ tests/JpegEncTests.cpp | 205 ++++++++++ 7 files changed, 1205 insertions(+), 2 deletions(-) create mode 100644 src/libs/libJpegDec.cpp create mode 100644 src/libs/libJpegEnc.cpp create mode 100644 tests/JpegDecTests.cpp create mode 100644 tests/JpegEncTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5503e6e9e..5c5781c24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -541,6 +541,14 @@ add_executable(png_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/PngEncTests.cpp target_link_libraries(png_enc_tests common fmt::fmt) target_include_directories(png_enc_tests PRIVATE ${inc_headers}) +add_executable(jpeg_dec_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/JpegDecTests.cpp") +target_link_libraries(jpeg_dec_tests common fmt::fmt) +target_include_directories(jpeg_dec_tests PRIVATE ${inc_headers}) + +add_executable(jpeg_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/JpegEncTests.cpp") +target_link_libraries(jpeg_enc_tests common fmt::fmt) +target_include_directories(jpeg_enc_tests PRIVATE ${inc_headers}) + add_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp") target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static) target_include_directories(ces_tests PRIVATE ${inc_headers}) @@ -652,6 +660,8 @@ if(BUILD_TESTING) add_test(NAME host_input COMMAND $) add_test(NAME save_data_memory COMMAND $) add_test(NAME png_enc COMMAND $) + add_test(NAME jpeg_dec COMMAND $) + add_test(NAME jpeg_enc COMMAND $) add_test(NAME ces COMMAND $) add_test(NAME save_data_dialog COMMAND $) add_test(NAME http_uri_parse COMMAND $) @@ -742,6 +752,8 @@ if(BUILD_TESTING) host_input_tests save_data_memory_tests png_enc_tests + jpeg_dec_tests + jpeg_enc_tests ces_tests save_data_dialog_tests http_uri_parse_tests diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 689086249..71657beb9 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -32,8 +32,6 @@ #include #include -#define STB_IMAGE_IMPLEMENTATION -#define STBI_NO_SIMD #include "stb_image.h" // IWYU pragma: no_include diff --git a/src/libs/libJpegDec.cpp b/src/libs/libJpegDec.cpp new file mode 100644 index 000000000..c31992e9a --- /dev/null +++ b/src/libs/libJpegDec.cpp @@ -0,0 +1,402 @@ +#include "common/abi.h" +#include "common/logging/log.h" +#include "libs/libs.h" +#include "loader/symbolDatabase.h" + +#include +#include +#include + +// The emulator's stb_image implementation. Guest JPEG data reaches it here, and zeroed allocations +// keep a scan that uses a Huffman table no DHT defined (an MJPEG frame) from reading uninitialised +// tables. +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_SIMD +#define STBI_MALLOC(size) calloc(1, size) +#define STBI_REALLOC(pointer, size) realloc(pointer, size) +#define STBI_FREE(pointer) free(pointer) +#include "stb_image.h" + +namespace Libs { + +LIB_VERSION("JpegDec", 1, "JpegDec", 1, 1); + +namespace JpegDec { + +constexpr int32_t JPEG_DEC_ERROR_INVALID_ADDR = static_cast(0x80650001u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_SIZE = static_cast(0x80650002u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_PARAM = static_cast(0x80650003u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_HANDLE = static_cast(0x80650004u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_COEF_MEMORY = static_cast(0x80650005u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80650006u); +constexpr int32_t JPEG_DEC_ERROR_INVALID_DATA = static_cast(0x80650010u); +constexpr int32_t JPEG_DEC_ERROR_UNSUPPORT_DATA = static_cast(0x80650011u); +constexpr int32_t JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80650012u); + +constexpr uint32_t JPEG_DEC_ATTRIBUTE_CSC_STD_SAMPLING = 1; +constexpr uint32_t JPEG_DEC_ATTRIBUTE_CSC_FULL_SAMPLING = 2; +constexpr uint16_t JPEG_DEC_DECODE_MODE_MJPEG = 1; +constexpr uint16_t JPEG_DEC_PIXEL_FORMAT_R8G8B8A8 = 0; +constexpr uint16_t JPEG_DEC_PIXEL_FORMAT_B8G8R8A8 = 1; +constexpr uint16_t JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8 = 10; +constexpr uint16_t JPEG_DEC_PIXEL_FORMAT_Y8 = 11; +constexpr uint16_t JPEG_DEC_COLOR_SPACE_UNKNOWN = 0; +constexpr uint16_t JPEG_DEC_COLOR_SPACE_YCC = 1; +constexpr uint16_t JPEG_DEC_COLOR_SPACE_GRAYSCALE = 2; + +struct JpegDecCreateParam { + uint32_t this_size; + uint32_t attribute; + uint32_t max_image_width; +}; + +struct JpegDecDecodeParam { + const void* jpeg_mem_addr; + void* image_mem_addr; + void* coefficient_mem_addr; + uint32_t jpeg_mem_size; + uint32_t image_mem_size; + uint32_t coefficient_mem_size; + uint16_t decode_mode; + uint16_t down_scale; + uint16_t pixel_format; + uint16_t alpha_value; + uint32_t image_pitch; +}; + +struct JpegDecParseParam { + const void* jpeg_mem_addr; + uint32_t jpeg_mem_size; + uint16_t decode_mode; + uint16_t down_scale; +}; + +struct JpegDecImageInfo { + uint32_t image_width; + uint32_t image_height; + uint16_t color_space; + uint16_t component_number; + uint8_t component_id[4]; + uint8_t sampling_factor[4]; + uint32_t coefficient_mem_size; + uint32_t suitable_csc_attribute; + uint32_t output_image_width; + uint32_t output_image_height; +}; + +static_assert(sizeof(JpegDecCreateParam) == 12 && sizeof(JpegDecDecodeParam) == 48); +static_assert(sizeof(JpegDecParseParam) == 16 && sizeof(JpegDecImageInfo) == 36); + +struct JpegDecContext { + uint64_t magic; + uint32_t attribute; + uint32_t max_image_width; +}; + +constexpr uint64_t JPEG_DEC_CONTEXT_MAGIC = 0x4b5954594a504744ull; // KYTYJPGD + +static int32_t CheckCreateParam(const JpegDecCreateParam* param) { + if (param == nullptr) { + return JPEG_DEC_ERROR_INVALID_ADDR; + } + if (param->this_size != sizeof(JpegDecCreateParam)) { + return JPEG_DEC_ERROR_INVALID_SIZE; + } + if ((param->attribute != JPEG_DEC_ATTRIBUTE_CSC_STD_SAMPLING && + param->attribute != JPEG_DEC_ATTRIBUTE_CSC_FULL_SAMPLING) || + param->max_image_width == 0 || param->max_image_width > 0xffffu) { + return JPEG_DEC_ERROR_INVALID_PARAM; + } + return 0; +} + +static bool CheckScale(uint16_t decode_mode, uint16_t down_scale) { + return decode_mode <= JPEG_DEC_DECODE_MODE_MJPEG && + (down_scale == 1 || down_scale == 2 || down_scale == 4 || down_scale == 8); +} + +// Fills the image information from the frame header (T.81 B.2.2) that precedes the first scan. +static int32_t ParseHeader(const void* jpeg, uint32_t size, uint16_t down_scale, + JpegDecImageInfo* info) { + const auto* data = static_cast(jpeg); + if (size < 4 || data[0] != 0xff || data[1] != 0xd8) { + return JPEG_DEC_ERROR_INVALID_DATA; + } + for (uint32_t offset = 2; offset + 4 <= size;) { + const uint8_t marker = data[offset + 1]; + if (data[offset] != 0xff || marker == 0xda || marker == 0xd9) { + return JPEG_DEC_ERROR_INVALID_DATA; + } + if (marker == 0xff) { + offset++; + continue; + } + const uint32_t length = (data[offset + 2] << 8u) | data[offset + 3]; + const auto* segment = data + offset + 4; + if (length < 2 || offset + 2 + length > size) { + return JPEG_DEC_ERROR_INVALID_DATA; + } + offset += 2 + length; + // SOF0-SOF15 except DHT, JPG and DAC. + if (marker < 0xc0 || marker > 0xcf || marker == 0xc4 || marker == 0xc8 || marker == 0xcc) { + continue; + } + const uint32_t components = length < 8 ? 0 : segment[5]; + if (components == 0 || length < 8 + components * 3 || (segment[3] | segment[4]) == 0) { + return JPEG_DEC_ERROR_INVALID_DATA; + } + *info = {}; + info->image_height = (segment[1] << 8u) | segment[2]; + info->image_width = (segment[3] << 8u) | segment[4]; + // Baseline, extended and progressive Huffman 8-bit frames with a known height. + if (marker > 0xc2 || segment[0] != 8 || components > 4 || info->image_height == 0) { + return JPEG_DEC_ERROR_UNSUPPORT_DATA; + } + uint32_t h_max = 1; + uint32_t v_max = 1; + for (uint32_t i = 0; i < components; i++) { + const uint8_t factor = segment[7 + i * 3]; + if ((factor >> 4u) - 1u > 3u || (factor & 0xfu) - 1u > 3u) { + return JPEG_DEC_ERROR_INVALID_DATA; + } + info->component_id[i] = segment[6 + i * 3]; + info->sampling_factor[i] = factor; + h_max = std::max(h_max, factor >> 4u); + v_max = std::max(v_max, factor & 0xfu); + } + info->component_number = components; + if (marker == 0xc2) { + // Progressive scans keep every 16-bit DCT coefficient of the MCU-padded image. + const uint32_t mcu_x = (info->image_width + h_max * 8 - 1) / (h_max * 8); + const uint32_t mcu_y = (info->image_height + v_max * 8 - 1) / (v_max * 8); + for (uint32_t i = 0; i < components; i++) { + info->coefficient_mem_size += mcu_x * (info->sampling_factor[i] >> 4u) * mcu_y * + (info->sampling_factor[i] & 0xfu) * 128u; + } + } + switch (components) { + case 1: info->color_space = JPEG_DEC_COLOR_SPACE_GRAYSCALE; break; + case 3: info->color_space = JPEG_DEC_COLOR_SPACE_YCC; break; + default: info->color_space = JPEG_DEC_COLOR_SPACE_UNKNOWN; break; + } + // Standard sampling: grayscale, or 1x1 chroma with luma at most 2x2 (4:4:4/4:2:2/4:2:0). + const bool standard = + components == 1 || (info->sampling_factor[1] == 0x11 && + info->sampling_factor[2] == 0x11 && h_max <= 2 && v_max <= 2); + info->suitable_csc_attribute = + standard ? JPEG_DEC_ATTRIBUTE_CSC_STD_SAMPLING : JPEG_DEC_ATTRIBUTE_CSC_FULL_SAMPLING; + info->output_image_width = (info->image_width + down_scale - 1) / down_scale; + info->output_image_height = (info->image_height + down_scale - 1) / down_scale; + return 0; + } + return JPEG_DEC_ERROR_INVALID_DATA; +} + +static uint8_t Luma(const uint8_t* p) { + return static_cast((77 * p[0] + 150 * p[1] + 29 * p[2] + 128) >> 8); +} + +// JFIF chroma of the average of two pixels, rounding half down so a saturated pair stays 255. +static uint8_t Chroma(const uint8_t* p, const uint8_t* q, int r, int g, int b) { + return static_cast( + (r * (p[0] + q[0]) + g * (p[1] + q[1]) + b * (p[2] + q[2]) + 65791) >> 9); +} + +static int32_t KYTY_SYSV_ABI JpegDecQueryMemorySize(const JpegDecCreateParam* param) { + PRINT_NAME(); + const auto result = CheckCreateParam(param); + return result != 0 ? result : static_cast(sizeof(JpegDecContext)); +} + +static int32_t KYTY_SYSV_ABI JpegDecCreate(const JpegDecCreateParam* param, void* memory_address, + uint32_t memory_size, void** handle) { + PRINT_NAME(); + if (const auto result = CheckCreateParam(param); result != 0) { + return result; + } + if (memory_address == nullptr || handle == nullptr) { + return JPEG_DEC_ERROR_INVALID_ADDR; + } + if (memory_size < sizeof(JpegDecContext)) { + return JPEG_DEC_ERROR_INVALID_SIZE; + } + *static_cast(memory_address) = {JPEG_DEC_CONTEXT_MAGIC, param->attribute, + param->max_image_width}; + *handle = memory_address; + return 0; +} + +static int32_t KYTY_SYSV_ABI JpegDecDelete(void* handle) { + PRINT_NAME(); + auto* ctx = static_cast(handle); + if (ctx == nullptr || ctx->magic != JPEG_DEC_CONTEXT_MAGIC) { + return JPEG_DEC_ERROR_INVALID_HANDLE; + } + ctx->magic = 0; + return 0; +} + +static int32_t KYTY_SYSV_ABI JpegDecParseHeader(const JpegDecParseParam* param, + JpegDecImageInfo* image_info) { + PRINT_NAME(); + if (param == nullptr || image_info == nullptr || param->jpeg_mem_addr == nullptr) { + return JPEG_DEC_ERROR_INVALID_ADDR; + } + if (param->jpeg_mem_size == 0) { + return JPEG_DEC_ERROR_INVALID_SIZE; + } + if (!CheckScale(param->decode_mode, param->down_scale)) { + return JPEG_DEC_ERROR_INVALID_PARAM; + } + return ParseHeader(param->jpeg_mem_addr, param->jpeg_mem_size, param->down_scale, image_info); +} + +static int32_t KYTY_SYSV_ABI JpegDecDecode(void* handle, const JpegDecDecodeParam* param, + JpegDecImageInfo* image_info) { + PRINT_NAME(); + const auto* ctx = static_cast(handle); + if (ctx == nullptr || ctx->magic != JPEG_DEC_CONTEXT_MAGIC) { + return JPEG_DEC_ERROR_INVALID_HANDLE; + } + if (param == nullptr || param->jpeg_mem_addr == nullptr || param->image_mem_addr == nullptr || + (param->pixel_format != JPEG_DEC_PIXEL_FORMAT_Y8 && + (reinterpret_cast(param->image_mem_addr) & 3u) != 0)) { + return JPEG_DEC_ERROR_INVALID_ADDR; + } + const auto format = param->pixel_format; + LOGF("\t jpeg_mem_size = %" PRIu32 ", image_mem_size = %" PRIu32 ", mode = %" PRIu16 + ", down_scale = %" PRIu16 ", pixel_format = %" PRIu16 ", image_pitch = %" PRIu32 "\n", + param->jpeg_mem_size, param->image_mem_size, param->decode_mode, param->down_scale, format, + param->image_pitch); + if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) { + return JPEG_DEC_ERROR_INVALID_SIZE; + } + if (!CheckScale(param->decode_mode, param->down_scale) || param->alpha_value > 255 || + (format != JPEG_DEC_PIXEL_FORMAT_R8G8B8A8 && format != JPEG_DEC_PIXEL_FORMAT_B8G8R8A8 && + format != JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8 && format != JPEG_DEC_PIXEL_FORMAT_Y8) || + (format != JPEG_DEC_PIXEL_FORMAT_Y8 && (param->image_pitch & 3u) != 0)) { + return JPEG_DEC_ERROR_INVALID_PARAM; + } + + JpegDecImageInfo info {}; + if (const auto result = + ParseHeader(param->jpeg_mem_addr, param->jpeg_mem_size, param->down_scale, &info); + result != 0) { + return result; + } + if (info.color_space == JPEG_DEC_COLOR_SPACE_UNKNOWN) { + return JPEG_DEC_ERROR_UNSUPPORT_DATA; + } + if (info.output_image_width > ctx->max_image_width || + info.suitable_csc_attribute > ctx->attribute) { + return JPEG_DEC_ERROR_INVALID_WORK_MEMORY; + } + if (info.coefficient_mem_size > param->coefficient_mem_size || + (info.coefficient_mem_size != 0 && param->coefficient_mem_addr == nullptr)) { + return JPEG_DEC_ERROR_INVALID_COEF_MEMORY; + } + const uint32_t width = info.output_image_width; + const uint32_t height = info.output_image_height; + const bool large = width > 32767 || height > 32767; + if (large && image_info == nullptr) { + return JPEG_DEC_ERROR_INVALID_ADDR; + } + uint32_t row = width * 4; + if (format == JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8) { + row = (width + 1) / 2 * 4; + } else if (format == JPEG_DEC_PIXEL_FORMAT_Y8) { + row = width; + } + const uint32_t pitch = param->image_pitch == 0 ? row : param->image_pitch; + if (pitch < row) { + return JPEG_DEC_ERROR_INVALID_PARAM; + } + if (static_cast(pitch) * height > param->image_mem_size) { + return JPEG_DEC_ERROR_INVALID_SIZE; + } + + int src_width = 0; + int src_height = 0; + auto* rgb = stbi_load_from_memory(static_cast(param->jpeg_mem_addr), + static_cast(param->jpeg_mem_size), &src_width, + &src_height, nullptr, 3); + if (rgb == nullptr || static_cast(src_width) != info.image_width || + static_cast(src_height) != info.image_height) { + stbi_image_free(rgb); + return JPEG_DEC_ERROR_DECODE_ERROR; + } + // Each output pixel averages its down_scale x down_scale source block, packed in place: an + // output pixel never lands past a source pixel still to be read. + const uint32_t scale = param->down_scale; + if (scale > 1) { + auto* out = rgb; + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + const uint32_t x_end = std::min((x + 1) * scale, info.image_width); + const uint32_t y_end = std::min((y + 1) * scale, info.image_height); + const uint32_t count = (x_end - x * scale) * (y_end - y * scale); + uint32_t sum[3] = {}; + for (uint32_t sy = y * scale; sy < y_end; sy++) { + for (uint32_t sx = x * scale; sx < x_end; sx++) { + const auto* p = rgb + (static_cast(sy) * info.image_width + sx) * 3; + sum[0] += p[0]; + sum[1] += p[1]; + sum[2] += p[2]; + } + } + for (const uint32_t c: sum) { + *out++ = static_cast((c + count / 2) / count); + } + } + } + } + const auto alpha = static_cast(param->alpha_value); + const auto* p = rgb; + for (uint32_t y = 0; y < height; y++) { + auto* dst = static_cast(param->image_mem_addr) + static_cast(y) * pitch; + for (uint32_t x = 0; x < width; x++, p += 3) { + switch (format) { + case JPEG_DEC_PIXEL_FORMAT_R8G8B8A8: + dst = std::copy_n(p, 3, dst); + *dst++ = alpha; + break; + case JPEG_DEC_PIXEL_FORMAT_B8G8R8A8: + *dst++ = p[2]; + *dst++ = p[1]; + *dst++ = p[0]; + *dst++ = alpha; + break; + case JPEG_DEC_PIXEL_FORMAT_Y8: *dst++ = Luma(p); break; + default: + if (x % 2 == 0) { + // An odd width repeats the last pixel. + const auto* q = x + 1 < width ? p + 3 : p; + *dst++ = Luma(p); + *dst++ = Chroma(p, q, -43, -85, 128); + *dst++ = Luma(q); + *dst++ = Chroma(p, q, 128, -107, -21); + } + break; + } + } + } + stbi_image_free(rgb); + if (image_info != nullptr) { + *image_info = info; + } + return large ? 0 : static_cast((width << 16u) | height); +} + +} // namespace JpegDec + +LIB_DEFINE(InitJpegDec_1) { + PRINT_NAME_ENABLE(true); + + LIB_FUNC("uNAUmANZMEw", JpegDec::JpegDecQueryMemorySize); + LIB_FUNC("JPh3Zgg0Zwc", JpegDec::JpegDecCreate); + LIB_FUNC("1kzQRoWEgSA", JpegDec::JpegDecDecode); + LIB_FUNC("Hwh11+m5KoI", JpegDec::JpegDecDelete); + LIB_FUNC("LSinoSQH790", JpegDec::JpegDecParseHeader); +} + +} // namespace Libs diff --git a/src/libs/libJpegEnc.cpp b/src/libs/libJpegEnc.cpp new file mode 100644 index 000000000..b8e80cfd2 --- /dev/null +++ b/src/libs/libJpegEnc.cpp @@ -0,0 +1,240 @@ +#include "common/abi.h" +#include "common/logging/log.h" +#include "libs/libs.h" +#include "loader/symbolDatabase.h" + +#include +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STB_IMAGE_WRITE_STATIC +#define STBIWDEF static inline +#define STBI_WRITE_NO_STDIO +#include "stb_image_write.h" + +namespace Libs { + +LIB_VERSION("JpegEnc", 1, "JpegEnc", 1, 1); + +namespace JpegEnc { + +constexpr int32_t JPEG_ENC_ERROR_INVALID_ADDR = static_cast(0x80650101u); +constexpr int32_t JPEG_ENC_ERROR_INVALID_SIZE = static_cast(0x80650102u); +constexpr int32_t JPEG_ENC_ERROR_INVALID_PARAM = static_cast(0x80650103u); +constexpr int32_t JPEG_ENC_ERROR_INVALID_HANDLE = static_cast(0x80650104u); +constexpr int32_t JPEG_ENC_ERROR_DATA_OVERFLOW = static_cast(0x80650110u); + +constexpr uint16_t JPEG_ENC_PIXEL_FORMAT_R8G8B8A8 = 0; +constexpr uint16_t JPEG_ENC_PIXEL_FORMAT_B8G8R8A8 = 1; +constexpr uint16_t JPEG_ENC_PIXEL_FORMAT_Y8U8Y8V8 = 10; +constexpr uint16_t JPEG_ENC_PIXEL_FORMAT_Y8 = 11; +constexpr uint16_t JPEG_ENC_ENCODE_MODE_MJPEG = 1; +constexpr uint16_t JPEG_ENC_COLOR_SPACE_YCC = 1; +constexpr uint16_t JPEG_ENC_COLOR_SPACE_GRAYSCALE = 2; +constexpr uint8_t JPEG_ENC_SAMPLING_TYPE_FULL = 0; +constexpr uint8_t JPEG_ENC_SAMPLING_TYPE_422 = 1; +constexpr uint8_t JPEG_ENC_SAMPLING_TYPE_420 = 2; + +struct JpegEncCreateParam { + uint32_t this_size; + uint32_t attribute; +}; + +struct JpegEncEncodeParam { + const uint8_t* image_mem_addr; + uint8_t* jpeg_mem_addr; + uint32_t image_mem_size; + uint32_t jpeg_mem_size; + uint32_t image_width; + uint32_t image_height; + uint32_t image_pitch; + uint16_t pixel_format; + uint16_t encode_mode; + uint16_t color_space; + uint8_t sampling_type; + uint8_t compression_ratio; + int32_t restart_interval; +}; + +struct JpegEncOutputInfo { + uint32_t data_size; + uint32_t processed_height; +}; + +static_assert(sizeof(JpegEncCreateParam) == 8 && sizeof(JpegEncEncodeParam) == 48); + +struct JpegEncContext { + uint64_t magic; +}; + +constexpr uint64_t JPEG_ENC_CONTEXT_MAGIC = 0x4b5954594a504745ull; // KYTYJPGE + +struct JpegEncWriter { + uint8_t* dst; + uint32_t capacity; + uint32_t size; + bool overflow; +}; + +static int32_t CheckCreateParam(const JpegEncCreateParam* param) { + if (param == nullptr) { + return JPEG_ENC_ERROR_INVALID_ADDR; + } + if (param->this_size != sizeof(JpegEncCreateParam)) { + return JPEG_ENC_ERROR_INVALID_SIZE; + } + return param->attribute != 0 ? JPEG_ENC_ERROR_INVALID_PARAM : 0; +} + +static void JpegEncWrite(void* context, void* data, int size) { + auto* writer = static_cast(context); + if (writer->overflow || static_cast(size) > writer->capacity - writer->size) { + writer->overflow = true; + return; + } + std::memcpy(writer->dst + writer->size, data, size); + writer->size += size; +} + +static int32_t KYTY_SYSV_ABI JpegEncQueryMemorySize(const JpegEncCreateParam* param) { + PRINT_NAME(); + const auto result = CheckCreateParam(param); + return result != 0 ? result : static_cast(sizeof(JpegEncContext)); +} + +static int32_t KYTY_SYSV_ABI JpegEncCreate(const JpegEncCreateParam* param, void* memory_address, + uint32_t memory_size, void** handle) { + PRINT_NAME(); + if (const auto result = CheckCreateParam(param); result != 0) { + return result; + } + if (memory_address == nullptr || handle == nullptr) { + return JPEG_ENC_ERROR_INVALID_ADDR; + } + if (memory_size < sizeof(JpegEncContext)) { + return JPEG_ENC_ERROR_INVALID_SIZE; + } + static_cast(memory_address)->magic = JPEG_ENC_CONTEXT_MAGIC; + *handle = memory_address; + return 0; +} + +static int32_t KYTY_SYSV_ABI JpegEncDelete(void* handle) { + PRINT_NAME(); + auto* ctx = static_cast(handle); + if (ctx == nullptr || ctx->magic != JPEG_ENC_CONTEXT_MAGIC) { + return JPEG_ENC_ERROR_INVALID_HANDLE; + } + ctx->magic = 0; + return 0; +} + +// stb_image_write picks the chroma sampling from the quality, always writes three components and +// full headers, and writes no restart markers; these only change the bytes of a valid JPEG. +static int32_t KYTY_SYSV_ABI JpegEncEncode(void* handle, const JpegEncEncodeParam* param, + JpegEncOutputInfo* output_info) { + PRINT_NAME(); + const auto* ctx = static_cast(handle); + if (ctx == nullptr || ctx->magic != JPEG_ENC_CONTEXT_MAGIC) { + return JPEG_ENC_ERROR_INVALID_HANDLE; + } + if (param == nullptr || param->image_mem_addr == nullptr || param->jpeg_mem_addr == nullptr || + (param->pixel_format != JPEG_ENC_PIXEL_FORMAT_Y8 && + (reinterpret_cast(param->image_mem_addr) & 3u) != 0)) { + return JPEG_ENC_ERROR_INVALID_ADDR; + } + const auto format = param->pixel_format; + const uint32_t width = param->image_width; + const uint32_t height = param->image_height; + LOGF("\t %" PRIu32 "x%" PRIu32 ", pitch = %" PRIu32 ", pixel_format = %" PRIu16 + ", mode = %" PRIu16 ", color_space = %" PRIu16 ", sampling = %" PRIu8 ", ratio = %" PRIu8 + ", restart = %" PRId32 "\n", + width, height, param->image_pitch, format, param->encode_mode, param->color_space, + param->sampling_type, param->compression_ratio, param->restart_interval); + const bool gray = param->color_space == JPEG_ENC_COLOR_SPACE_GRAYSCALE; + uint32_t row = width * 4; + if (format == JPEG_ENC_PIXEL_FORMAT_Y8U8Y8V8) { + row = (width + 1) / 2 * 4; + } else if (format == JPEG_ENC_PIXEL_FORMAT_Y8) { + row = width; + } + // Grayscale takes full sampling; YCbCr takes 4:2:2 or 4:2:0 from a color format. + const auto sampling = param->sampling_type; + const bool ycc = + param->color_space == JPEG_ENC_COLOR_SPACE_YCC && format != JPEG_ENC_PIXEL_FORMAT_Y8 && + (sampling == JPEG_ENC_SAMPLING_TYPE_422 || sampling == JPEG_ENC_SAMPLING_TYPE_420); + if (width == 0 || width > 0xffff || height == 0 || height > 0xffff || + param->image_pitch < row || + (format != JPEG_ENC_PIXEL_FORMAT_Y8 && (param->image_pitch & 3u) != 0) || + (format != JPEG_ENC_PIXEL_FORMAT_R8G8B8A8 && format != JPEG_ENC_PIXEL_FORMAT_B8G8R8A8 && + format != JPEG_ENC_PIXEL_FORMAT_Y8U8Y8V8 && format != JPEG_ENC_PIXEL_FORMAT_Y8) || + param->encode_mode > JPEG_ENC_ENCODE_MODE_MJPEG || + !(ycc || (gray && sampling == JPEG_ENC_SAMPLING_TYPE_FULL)) || + param->restart_interval < -1 || param->restart_interval > 0xffff) { + return JPEG_ENC_ERROR_INVALID_PARAM; + } + if (param->jpeg_mem_size == 0 || + static_cast(param->image_pitch) * height > param->image_mem_size) { + return JPEG_ENC_ERROR_INVALID_SIZE; + } + + const uint32_t components = gray ? 1 : 3; + std::vector pixels(static_cast(width) * height * components); + auto* dst = pixels.data(); + for (uint32_t y = 0; y < height; y++) { + const auto* src = param->image_mem_addr + static_cast(y) * param->image_pitch; + for (uint32_t x = 0; x < width; x++) { + int rgb[3] = {src[x], src[x], src[x]}; + if (format == JPEG_ENC_PIXEL_FORMAT_Y8U8Y8V8) { + // JFIF YCbCr to RGB from the YUYV pair that holds this pixel; gray keeps the luma. + const auto* pair = src + x / 2 * 4; + const int luma = pair[x % 2 * 2] * 256 + 128; + const int cb = gray ? 0 : pair[1] - 128; + const int cr = gray ? 0 : pair[3] - 128; + rgb[0] = std::clamp((luma + 359 * cr) >> 8, 0, 255); + rgb[1] = std::clamp((luma - 88 * cb - 183 * cr) >> 8, 0, 255); + rgb[2] = std::clamp((luma + 454 * cb) >> 8, 0, 255); + } else if (format != JPEG_ENC_PIXEL_FORMAT_Y8) { + const auto* p = src + x * 4; + const bool bgr = format == JPEG_ENC_PIXEL_FORMAT_B8G8R8A8; + rgb[0] = p[bgr ? 2 : 0]; + rgb[1] = p[1]; + rgb[2] = p[bgr ? 0 : 2]; + } + if (gray) { + *dst++ = + static_cast((77 * rgb[0] + 150 * rgb[1] + 29 * rgb[2] + 128) >> 8); + continue; + } + for (const int c: rgb) { + *dst++ = static_cast(c); + } + } + } + + // A lower compression ratio means higher quality: 0 maps to stb quality 100 and 255 to 1. + JpegEncWriter writer {param->jpeg_mem_addr, param->jpeg_mem_size, 0, false}; + stbi_write_jpg_to_func(JpegEncWrite, &writer, static_cast(width), static_cast(height), + static_cast(components), pixels.data(), + 100 - param->compression_ratio * 99 / 255); + if (output_info != nullptr) { + output_info->data_size = writer.size; + output_info->processed_height = writer.overflow ? 0 : height; + } + return writer.overflow ? JPEG_ENC_ERROR_DATA_OVERFLOW : static_cast(writer.size); +} + +} // namespace JpegEnc + +LIB_DEFINE(InitJpegEnc_1) { + PRINT_NAME_ENABLE(true); + + LIB_FUNC("o6ZgXfFdWXQ", JpegEnc::JpegEncQueryMemorySize); + LIB_FUNC("K+rocojkr-I", JpegEnc::JpegEncCreate); + LIB_FUNC("QbrU0cUghEM", JpegEnc::JpegEncEncode); + LIB_FUNC("j1LyMdaM+C0", JpegEnc::JpegEncDelete); +} + +} // namespace Libs diff --git a/src/libs/libs.cpp b/src/libs/libs.cpp index 4ab0af0f7..236f9ff2b 100644 --- a/src/libs/libs.cpp +++ b/src/libs/libs.cpp @@ -92,6 +92,8 @@ LIB_DEFINE(InitFont_1); LIB_DEFINE(InitFontFt_1); LIB_DEFINE(InitAgcDriver_1); LIB_DEFINE(InitHmd2_1); +LIB_DEFINE(InitJpegDec_1); +LIB_DEFINE(InitJpegEnc_1); LIB_DEFINE(InitVrTracker2_1); LIB_DEFINE(InitLibKernel_1); LIB_DEFINE(InitNet_1); @@ -132,6 +134,8 @@ void InitAll(Loader::SymbolDatabase* s) { InitDeviceService_1(s); InitUsbd_1(s); InitHmd2_1(s); + InitJpegDec_1(s); + InitJpegEnc_1(s); InitVrTracker2_1(s); InitLibKernel_1(s); LibMouse::InitMouse_1(s); diff --git a/tests/JpegDecTests.cpp b/tests/JpegDecTests.cpp new file mode 100644 index 000000000..a37c3aa0f --- /dev/null +++ b/tests/JpegDecTests.cpp @@ -0,0 +1,342 @@ +#include "libs/libJpegDec.cpp" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STBI_WRITE_NO_STDIO +#include "stb_image_write.h" + +#include +#include +#include +#include +#include + +namespace Loader { +void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {} +namespace Timer { +double GetTimeMs() { + return 0.0; +} +} // namespace Timer +} // namespace Loader + +namespace { +using namespace Libs::JpegDec; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::fprintf(stderr, "JpegDecTests:%d: %s\n", __LINE__, #condition); \ + std::abort(); \ + } \ + } while (false) + +constexpr uint32_t WIDTH = 16; +constexpr uint32_t HEIGHT = 8; + +// cjpeg -grayscale -progressive -optimize -quality 100 of an 8x8 image with pixel x*16 + y*8, +// without the JFIF APP0 segment. +constexpr uint8_t PROGRESSIVE[] = { + 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xc2, 0x00, 0x0b, 0x08, 0x00, 0x08, 0x00, 0x08, + 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x27, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x16, 0x10, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0xff, + 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x9b, 0x2d, 0x9f, 0xff, 0xc4, 0x00, 0x18, + 0x10, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x22, 0x41, 0x52, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3f, 0x02, + 0x44, 0x33, 0x47, 0xff, 0xc4, 0x00, 0x16, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xf0, 0xff, 0xda, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x01, 0x3f, 0x21, 0x9c, 0x47, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x10, 0x7f, 0xff, 0xc4, 0x00, 0x16, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0xf0, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, + 0x00, 0x01, 0x3f, 0x10, 0x8f, 0x43, 0xff, 0xd9}; + +// Red 128, green by row, blue by column. +int Expected(uint32_t x, uint32_t y, uint32_t c) { + const int rgb[3] = {128, static_cast(y * 32), static_cast(x * 16)}; + return rgb[c]; +} + +int Luma(uint32_t x, uint32_t y) { + return (77 * 128 + 150 * Expected(x, y, 1) + 29 * Expected(x, y, 2) + 128) >> 8; +} + +void CheckNear(int value, int expected) { + CHECK(std::abs(value - expected) <= 8); +} + +// Quality 100 keeps 4:4:4 sampling; 50 subsamples to 4:2:0. +std::vector Encode(int quality, uint32_t width = WIDTH) { + std::vector rgb(width * HEIGHT * 3); + for (uint32_t i = 0; i < rgb.size(); i++) { + rgb[i] = static_cast(Expected(i / 3 % width, i / 3 / width, i % 3)); + } + std::vector jpeg; + stbi_write_jpg_to_func( + [](void* context, void* data, int size) { + auto* out = static_cast*>(context); + const auto* bytes = static_cast(data); + out->insert(out->end(), bytes, bytes + size); + }, + &jpeg, static_cast(width), HEIGHT, 3, rgb.data(), quality); + return jpeg; +} + +// The first marker of this type in an stb_image_write output. +auto Marker(std::vector& jpeg, uint8_t type) { + const uint8_t marker[2] = {0xff, type}; + const auto it = std::search(jpeg.begin(), jpeg.end(), std::begin(marker), std::end(marker)); + CHECK(it != jpeg.end()); + return it; +} + +struct Decoder { + JpegDecContext context {}; + void* handle = nullptr; + + explicit Decoder(uint32_t max_width) { + const JpegDecCreateParam create {sizeof(JpegDecCreateParam), + JPEG_DEC_ATTRIBUTE_CSC_STD_SAMPLING, max_width}; + CHECK(JpegDecQueryMemorySize(&create) == sizeof(context)); + CHECK(JpegDecCreate(&create, &context, sizeof(context), &handle) == 0); + } + + ~Decoder() { + CHECK(JpegDecDelete(handle) == 0); + CHECK(JpegDecDelete(handle) == JPEG_DEC_ERROR_INVALID_HANDLE); + } +}; + +void TestParseHeader() { + auto jpeg = Encode(100); + JpegDecParseParam parse {jpeg.data(), static_cast(jpeg.size()), 0, 2}; + JpegDecImageInfo info {}; + CHECK(JpegDecParseHeader(&parse, &info) == 0); + CHECK(info.image_width == WIDTH && info.image_height == HEIGHT); + CHECK(info.color_space == JPEG_DEC_COLOR_SPACE_YCC && info.component_number == 3); + CHECK(info.component_id[0] == 1 && info.component_id[1] == 2 && info.component_id[2] == 3); + CHECK(info.sampling_factor[0] == 0x11 && info.sampling_factor[2] == 0x11); + CHECK(info.coefficient_mem_size == 0); + CHECK(info.suitable_csc_attribute == JPEG_DEC_ATTRIBUTE_CSC_STD_SAMPLING); + CHECK(info.output_image_width == WIDTH / 2 && info.output_image_height == HEIGHT / 2); + + auto subsampled = Encode(50); + parse = {subsampled.data(), static_cast(subsampled.size()), 1, 8}; + CHECK(JpegDecParseHeader(&parse, &info) == 0); + CHECK(info.sampling_factor[0] == 0x22 && info.sampling_factor[1] == 0x11); + CHECK(info.output_image_width == 2 && info.output_image_height == 1); + + // 4x1 luma needs full sampling. + Marker(subsampled, 0xc0)[11] = 0x41; + CHECK(JpegDecParseHeader(&parse, &info) == 0); + CHECK(info.suitable_csc_attribute == JPEG_DEC_ATTRIBUTE_CSC_FULL_SAMPLING); + + // A progressive frame keeps 16-bit coefficients for 2x1 blocks of each of the 3 components. + const auto sof = Marker(jpeg, 0xc0); + sof[1] = 0xc2; + parse = {jpeg.data(), static_cast(jpeg.size()), 0, 1}; + CHECK(JpegDecParseHeader(&parse, &info) == 0 && info.coefficient_mem_size == 3 * 2 * 128); + sof[1] = 0xc3; + CHECK(JpegDecParseHeader(&parse, &info) == JPEG_DEC_ERROR_UNSUPPORT_DATA); + + parse.down_scale = 3; + CHECK(JpegDecParseHeader(&parse, &info) == JPEG_DEC_ERROR_INVALID_PARAM); + parse = {jpeg.data(), 0, 0, 1}; + CHECK(JpegDecParseHeader(&parse, &info) == JPEG_DEC_ERROR_INVALID_SIZE); + CHECK(JpegDecParseHeader(&parse, nullptr) == JPEG_DEC_ERROR_INVALID_ADDR); + jpeg[1] = 0; + parse = {jpeg.data(), static_cast(jpeg.size()), 0, 1}; + CHECK(JpegDecParseHeader(&parse, &info) == JPEG_DEC_ERROR_INVALID_DATA); +} + +void TestDecode() { + const auto jpeg = Encode(100); + Decoder decoder(WIDTH); + alignas(4) std::array image {}; + // GT7 passes a non-NULL coefficient address with size 0 for baseline images. + JpegDecDecodeParam param {.jpeg_mem_addr = jpeg.data(), + .image_mem_addr = image.data(), + .coefficient_mem_addr = image.data(), + .jpeg_mem_size = static_cast(jpeg.size()), + .image_mem_size = image.size(), + .down_scale = 1, + .pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8, + .alpha_value = 200, + .image_pitch = WIDTH * 4 + 8}; + image.fill(0xee); + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == + static_cast(WIDTH << 16 | HEIGHT)); + for (uint32_t y = 0; y < HEIGHT; y++) { + const auto* row = &image[y * param.image_pitch]; + for (uint32_t x = 0; x < WIDTH; x++) { + for (uint32_t c = 0; c < 3; c++) { + CheckNear(row[x * 4 + c], Expected(x, y, c)); + } + CHECK(row[x * 4 + 3] == 200); + } + CHECK(row[WIDTH * 4] == 0xee && row[WIDTH * 4 + 7] == 0xee); + } + + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_B8G8R8A8; + param.image_pitch = 0; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + CheckNear(image[(3 * WIDTH + 5) * 4], Expected(5, 3, 2)); + CheckNear(image[(3 * WIDTH + 5) * 4 + 2], 128); + + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + CheckNear(image[3 * WIDTH + 5], Luma(5, 3)); + + // YUYV holds both lumas and the pair's average chroma; saturated pairs stay at 255. + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + const auto* pair = &image[(3 * WIDTH + 4) * 2]; + CheckNear(pair[0], Luma(4, 3)); + CheckNear(pair[2], Luma(5, 3)); + const int blue = (Expected(4, 3, 2) + Expected(5, 3, 2)) / 2; + CheckNear(pair[1], (-43 * 128 - 85 * Expected(4, 3, 1) + 128 * blue + 32896) >> 8); + CheckNear(pair[3], (128 * 128 - 107 * Expected(4, 3, 1) - 21 * blue + 32896) >> 8); + constexpr uint8_t red[3] = {255, 0, 0}; + constexpr uint8_t pure_blue[3] = {0, 0, 255}; + CHECK(Chroma(pure_blue, pure_blue, -43, -85, 128) == 255); + CHECK(Chroma(red, red, 128, -107, -21) == 255); + + // Each downscaled pixel averages its 4x4 block: (1, 1) covers columns and rows 4-7. + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8; + param.down_scale = 4; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == + static_cast(WIDTH / 4 << 16 | HEIGHT / 4)); + CheckNear(image[(WIDTH / 4 + 1) * 4 + 1], (4 + 5 + 6 + 7) * 32 / 4); + CheckNear(image[(WIDTH / 4 + 1) * 4 + 2], (4 + 5 + 6 + 7) * 16 / 4); + + // An odd YUYV width repeats the last pixel. + const auto odd = Encode(100, WIDTH - 1); + param.jpeg_mem_addr = odd.data(); + param.jpeg_mem_size = static_cast(odd.size()); + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8; + param.down_scale = 1; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + const auto* last = &image[(3 * WIDTH + WIDTH - 2) * 2]; + CheckNear(last[0], Luma(WIDTH - 2, 3)); + CHECK(last[2] == last[0]); +} + +void TestProgressive() { + Decoder decoder(8); + std::array coefficients {}; + alignas(4) std::array image {}; + JpegDecDecodeParam param {.jpeg_mem_addr = PROGRESSIVE, + .image_mem_addr = image.data(), + .coefficient_mem_addr = coefficients.data(), + .jpeg_mem_size = sizeof(PROGRESSIVE), + .image_mem_size = image.size(), + .coefficient_mem_size = coefficients.size(), + .down_scale = 1, + .pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8, + .alpha_value = 255}; + JpegDecImageInfo info {}; + CHECK(JpegDecDecode(decoder.handle, ¶m, &info) == (8 << 16 | 8)); + CHECK(info.color_space == JPEG_DEC_COLOR_SPACE_GRAYSCALE && info.component_number == 1); + for (uint32_t i = 0; i < 64; i++) { + CheckNear(image[i * 4], i % 8 * 16 + i / 8 * 8); + } + param.coefficient_mem_size--; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_COEF_MEMORY); + param.coefficient_mem_addr = nullptr; + param.coefficient_mem_size = coefficients.size(); + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_COEF_MEMORY); +} + +// A scan whose Huffman tables no DHT defined gets zeroed tables, not the previous decode's: they +// consume no bits, so stb fails at the scan's first stuffed 0xff. +void TestMissingTables() { + auto jpeg = Encode(100); + Decoder decoder(WIDTH); + alignas(4) std::array image {}; + JpegDecDecodeParam param {.jpeg_mem_addr = jpeg.data(), + .image_mem_addr = image.data(), + .jpeg_mem_size = static_cast(jpeg.size()), + .image_mem_size = image.size(), + .down_scale = 1, + .pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8, + .alpha_value = 255}; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + const auto dht = Marker(jpeg, 0xc4); + jpeg.erase(dht, dht + 2 + (dht[2] << 8 | dht[3])); + param.jpeg_mem_size = static_cast(jpeg.size()); + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_DECODE_ERROR); +} + +void TestDecodeValidation() { + auto jpeg = Encode(100); + Decoder decoder(WIDTH); + alignas(4) std::array image {}; + const JpegDecDecodeParam valid {.jpeg_mem_addr = jpeg.data(), + .image_mem_addr = image.data(), + .jpeg_mem_size = static_cast(jpeg.size()), + .image_mem_size = image.size(), + .down_scale = 1, + .pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8, + .alpha_value = 255}; + CHECK(JpegDecDecode(decoder.handle, &valid, nullptr) > 0); + CHECK(JpegDecDecode(nullptr, &valid, nullptr) == JPEG_DEC_ERROR_INVALID_HANDLE); + CHECK(JpegDecDecode(decoder.handle, nullptr, nullptr) == JPEG_DEC_ERROR_INVALID_ADDR); + + auto param = valid; + param.image_mem_addr = image.data() + 2; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_ADDR); + param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) > 0); + param = valid; + param.image_mem_size--; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_SIZE); + param = valid; + param.image_pitch = WIDTH * 4 - 4; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_PARAM); + param = valid; + param.pixel_format = 2; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_PARAM); + param = valid; + param.alpha_value = 256; + CHECK(JpegDecDecode(decoder.handle, ¶m, nullptr) == JPEG_DEC_ERROR_INVALID_PARAM); + + // The width limit applies after downscaling. + Decoder narrow(WIDTH / 2); + CHECK(JpegDecDecode(narrow.handle, &valid, nullptr) == JPEG_DEC_ERROR_INVALID_WORK_MEMORY); + param = valid; + param.down_scale = 2; + CHECK(JpegDecDecode(narrow.handle, ¶m, nullptr) > 0); + + // Full sampling needs a decoder created for it. + const auto sof = Marker(jpeg, 0xc0); + sof[11] = 0x41; + CHECK(JpegDecDecode(decoder.handle, &valid, nullptr) == JPEG_DEC_ERROR_INVALID_WORK_MEMORY); + // An output above 32767 pixels needs image information. + sof[11] = 0x11; + sof[7] = 40000 >> 8; + sof[8] = 40000 & 0xff; + Decoder wide(0xffff); + CHECK(JpegDecDecode(wide.handle, &valid, nullptr) == JPEG_DEC_ERROR_INVALID_ADDR); + + JpegDecCreateParam create {sizeof(JpegDecCreateParam), 3, WIDTH}; + CHECK(JpegDecQueryMemorySize(&create) == JPEG_DEC_ERROR_INVALID_PARAM); + create = {sizeof(JpegDecCreateParam), JPEG_DEC_ATTRIBUTE_CSC_FULL_SAMPLING, 0}; + CHECK(JpegDecQueryMemorySize(&create) == JPEG_DEC_ERROR_INVALID_PARAM); + create.this_size = 8; + CHECK(JpegDecQueryMemorySize(&create) == JPEG_DEC_ERROR_INVALID_SIZE); + CHECK(JpegDecQueryMemorySize(nullptr) == JPEG_DEC_ERROR_INVALID_ADDR); +} +} // namespace + +int main() { + TestParseHeader(); + TestDecode(); + TestProgressive(); + TestMissingTables(); + TestDecodeValidation(); + std::puts("JPEG decoder tests passed"); +} diff --git a/tests/JpegEncTests.cpp b/tests/JpegEncTests.cpp new file mode 100644 index 000000000..72381bc41 --- /dev/null +++ b/tests/JpegEncTests.cpp @@ -0,0 +1,205 @@ +#include "libs/libJpegEnc.cpp" + +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ONLY_JPEG +#define STBI_NO_STDIO +#include "stb_image.h" + +#include +#include +#include + +namespace Loader { +void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {} +namespace Timer { +double GetTimeMs() { + return 0.0; +} +} // namespace Timer +} // namespace Loader + +namespace { +using namespace Libs::JpegEnc; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::fprintf(stderr, "JpegEncTests:%d: %s\n", __LINE__, #condition); \ + std::abort(); \ + } \ + } while (false) + +constexpr uint32_t WIDTH = 16; +constexpr uint32_t HEIGHT = 8; + +using Jpeg = std::array; +using Pattern = int (*)(uint32_t x, uint32_t y, uint32_t c); + +struct Encoder { + JpegEncContext context {}; + void* handle = nullptr; + + Encoder() { + const JpegEncCreateParam create {sizeof(JpegEncCreateParam), 0}; + CHECK(JpegEncQueryMemorySize(&create) == sizeof(context)); + CHECK(JpegEncCreate(&create, &context, sizeof(context), &handle) == 0); + } + + ~Encoder() { + CHECK(JpegEncDelete(handle) == 0); + CHECK(JpegEncDelete(handle) == JPEG_ENC_ERROR_INVALID_HANDLE); + } +}; + +// Red 128, green by row, blue by column. +int Expected(uint32_t x, uint32_t y, uint32_t c) { + const int rgb[3] = {128, static_cast(y * 32), static_cast(x * 16)}; + return rgb[c]; +} + +int Luma(uint32_t x, uint32_t y, uint32_t /*c*/) { + return (77 * 128 + 150 * Expected(x, y, 1) + 29 * Expected(x, y, 2) + 128) >> 8; +} + +// Decodes the output and checks every channel against the pattern. +void CheckDecoded(const Jpeg& jpeg, int32_t size, Pattern pattern) { + CHECK(size > 0); + int width = 0; + int height = 0; + auto* rgb = stbi_load_from_memory(jpeg.data(), size, &width, &height, nullptr, 3); + CHECK(rgb != nullptr && width == WIDTH && height == HEIGHT); + for (uint32_t i = 0; i < WIDTH * HEIGHT * 3; i++) { + CHECK(std::abs(rgb[i] - pattern(i / 3 % WIDTH, i / 3 / WIDTH, i % 3)) <= 8); + } + stbi_image_free(rgb); +} + +void TestEncode() { + Encoder encoder; + alignas(4) std::array bgra {}; + std::array y8 {}; + for (uint32_t i = 0; i < WIDTH * HEIGHT; i++) { + const uint32_t x = i % WIDTH; + const uint32_t y = i / WIDTH; + bgra[i * 4] = Expected(x, y, 2); + bgra[i * 4 + 1] = Expected(x, y, 1); + bgra[i * 4 + 2] = Expected(x, y, 0); + y8[i] = Luma(x, y, 0); + } + Jpeg jpeg {}; + JpegEncEncodeParam param {.image_mem_addr = bgra.data(), + .jpeg_mem_addr = jpeg.data(), + .image_mem_size = bgra.size(), + .jpeg_mem_size = jpeg.size(), + .image_width = WIDTH, + .image_height = HEIGHT, + .image_pitch = WIDTH * 4, + .pixel_format = JPEG_ENC_PIXEL_FORMAT_B8G8R8A8, + .color_space = JPEG_ENC_COLOR_SPACE_YCC, + .sampling_type = JPEG_ENC_SAMPLING_TYPE_420}; + JpegEncOutputInfo info {}; + const auto size = JpegEncEncode(encoder.handle, ¶m, &info); + CHECK(info.data_size == static_cast(size) && info.processed_height == HEIGHT); + CheckDecoded(jpeg, size, Expected); + const auto color = param; + + // The same pixels read as RGBA swap red and blue. + param.pixel_format = JPEG_ENC_PIXEL_FORMAT_R8G8B8A8; + CheckDecoded(jpeg, JpegEncEncode(encoder.handle, ¶m, nullptr), + [](uint32_t x, uint32_t y, uint32_t c) { return Expected(x, y, 2 - c); }); + + param = color; + param.color_space = JPEG_ENC_COLOR_SPACE_GRAYSCALE; + param.sampling_type = JPEG_ENC_SAMPLING_TYPE_FULL; + CheckDecoded(jpeg, JpegEncEncode(encoder.handle, ¶m, nullptr), Luma); + + param.image_mem_addr = y8.data(); + param.image_mem_size = y8.size(); + param.image_pitch = WIDTH; + param.pixel_format = JPEG_ENC_PIXEL_FORMAT_Y8; + CheckDecoded(jpeg, JpegEncEncode(encoder.handle, ¶m, nullptr), Luma); + + // GT7's YUYV rows hold 2 bytes per pixel at a pitch of width * 4. + constexpr uint8_t pair[4] = {64, 160, 192, 96}; + alignas(4) std::array yuyv {}; + for (uint32_t i = 0; i < yuyv.size(); i++) { + yuyv[i] = i % (WIDTH * 4) < WIDTH * 2 ? pair[i % 4] : 0xff; + } + param = color; + param.image_mem_addr = yuyv.data(); + param.pixel_format = JPEG_ENC_PIXEL_FORMAT_Y8U8Y8V8; + param.sampling_type = JPEG_ENC_SAMPLING_TYPE_422; + CheckDecoded(jpeg, JpegEncEncode(encoder.handle, ¶m, nullptr), + [](uint32_t x, uint32_t, uint32_t c) { + constexpr int rgb[2][3] = {{19, 76, 121}, {147, 204, 249}}; + return rgb[x % 2][c]; + }); + param.color_space = JPEG_ENC_COLOR_SPACE_GRAYSCALE; + param.sampling_type = JPEG_ENC_SAMPLING_TYPE_FULL; + CheckDecoded(jpeg, JpegEncEncode(encoder.handle, ¶m, nullptr), + [](uint32_t x, uint32_t, uint32_t) { return x % 2 == 0 ? 64 : 192; }); + + param = color; + param.jpeg_mem_size = 100; + CHECK(JpegEncEncode(encoder.handle, ¶m, &info) == JPEG_ENC_ERROR_DATA_OVERFLOW); + CHECK(info.data_size <= 100 && info.processed_height == 0); +} + +void TestValidation() { + JpegEncCreateParam create {sizeof(JpegEncCreateParam), 1}; + CHECK(JpegEncQueryMemorySize(nullptr) == JPEG_ENC_ERROR_INVALID_ADDR); + CHECK(JpegEncQueryMemorySize(&create) == JPEG_ENC_ERROR_INVALID_PARAM); + create = {4, 0}; + CHECK(JpegEncQueryMemorySize(&create) == JPEG_ENC_ERROR_INVALID_SIZE); + + Encoder encoder; + alignas(4) std::array rgba {}; + Jpeg jpeg {}; + const JpegEncEncodeParam valid {.image_mem_addr = rgba.data(), + .jpeg_mem_addr = jpeg.data(), + .image_mem_size = rgba.size(), + .jpeg_mem_size = jpeg.size(), + .image_width = WIDTH, + .image_height = HEIGHT, + .image_pitch = WIDTH * 4, + .pixel_format = JPEG_ENC_PIXEL_FORMAT_R8G8B8A8, + .color_space = JPEG_ENC_COLOR_SPACE_YCC, + .sampling_type = JPEG_ENC_SAMPLING_TYPE_422, + .compression_ratio = 50, + .restart_interval = -1}; + CHECK(JpegEncEncode(encoder.handle, &valid, nullptr) > 0); + CHECK(JpegEncEncode(nullptr, &valid, nullptr) == JPEG_ENC_ERROR_INVALID_HANDLE); + CHECK(JpegEncEncode(encoder.handle, nullptr, nullptr) == JPEG_ENC_ERROR_INVALID_ADDR); + + auto param = valid; + param.image_mem_addr = rgba.data() + 1; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_ADDR); + param = valid; + param.image_mem_size--; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_SIZE); + param = valid; + param.image_pitch = WIDTH * 4 - 4; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); + param = valid; + param.sampling_type = JPEG_ENC_SAMPLING_TYPE_FULL; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); + param = valid; + param.pixel_format = JPEG_ENC_PIXEL_FORMAT_Y8; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); + param = valid; + param.restart_interval = -2; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); + param = valid; + param.encode_mode = 2; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); + param = valid; + param.pixel_format = 2; + CHECK(JpegEncEncode(encoder.handle, ¶m, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM); +} +} // namespace + +int main() { + TestEncode(); + TestValidation(); + std::puts("JPEG encoder tests passed"); +}