libs: implement JpegDec and JpegEnc

This commit is contained in:
nmzik
2026-10-23 18:00:48 +02:00
parent 77a8f30758
commit ba0e93daac
7 changed files with 1205 additions and 2 deletions
+12
View File
@@ -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 $<TARGET_FILE:host_input_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
add_test(NAME jpeg_dec COMMAND $<TARGET_FILE:jpeg_dec_tests>)
add_test(NAME jpeg_enc COMMAND $<TARGET_FILE:jpeg_enc_tests>)
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
@@ -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
@@ -32,8 +32,6 @@
#include <vector>
#include <vulkan/vk_platform.h>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
#include "stb_image.h"
// IWYU pragma: no_include <intrin.h>
+402
View File
@@ -0,0 +1,402 @@
#include "common/abi.h"
#include "common/logging/log.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cstddef>
#include <cstdlib>
// 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<int32_t>(0x80650001u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_SIZE = static_cast<int32_t>(0x80650002u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_PARAM = static_cast<int32_t>(0x80650003u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_HANDLE = static_cast<int32_t>(0x80650004u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_COEF_MEMORY = static_cast<int32_t>(0x80650005u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast<int32_t>(0x80650006u);
constexpr int32_t JPEG_DEC_ERROR_INVALID_DATA = static_cast<int32_t>(0x80650010u);
constexpr int32_t JPEG_DEC_ERROR_UNSUPPORT_DATA = static_cast<int32_t>(0x80650011u);
constexpr int32_t JPEG_DEC_ERROR_DECODE_ERROR = static_cast<int32_t>(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<const uint8_t*>(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<uint32_t>(h_max, factor >> 4u);
v_max = std::max<uint32_t>(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<uint8_t>((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<uint8_t>(
(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<int32_t>(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<JpegDecContext*>(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<JpegDecContext*>(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<const JpegDecContext*>(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<uintptr_t>(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<uint64_t>(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<const stbi_uc*>(param->jpeg_mem_addr),
static_cast<int>(param->jpeg_mem_size), &src_width,
&src_height, nullptr, 3);
if (rgb == nullptr || static_cast<uint32_t>(src_width) != info.image_width ||
static_cast<uint32_t>(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<size_t>(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<uint8_t>((c + count / 2) / count);
}
}
}
}
const auto alpha = static_cast<uint8_t>(param->alpha_value);
const auto* p = rgb;
for (uint32_t y = 0; y < height; y++) {
auto* dst = static_cast<uint8_t*>(param->image_mem_addr) + static_cast<size_t>(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<int32_t>((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
+240
View File
@@ -0,0 +1,240 @@
#include "common/abi.h"
#include "common/logging/log.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <vector>
#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<int32_t>(0x80650101u);
constexpr int32_t JPEG_ENC_ERROR_INVALID_SIZE = static_cast<int32_t>(0x80650102u);
constexpr int32_t JPEG_ENC_ERROR_INVALID_PARAM = static_cast<int32_t>(0x80650103u);
constexpr int32_t JPEG_ENC_ERROR_INVALID_HANDLE = static_cast<int32_t>(0x80650104u);
constexpr int32_t JPEG_ENC_ERROR_DATA_OVERFLOW = static_cast<int32_t>(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<JpegEncWriter*>(context);
if (writer->overflow || static_cast<uint32_t>(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<int32_t>(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<JpegEncContext*>(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<JpegEncContext*>(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<const JpegEncContext*>(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<uintptr_t>(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<uint64_t>(param->image_pitch) * height > param->image_mem_size) {
return JPEG_ENC_ERROR_INVALID_SIZE;
}
const uint32_t components = gray ? 1 : 3;
std::vector<uint8_t> pixels(static_cast<size_t>(width) * height * components);
auto* dst = pixels.data();
for (uint32_t y = 0; y < height; y++) {
const auto* src = param->image_mem_addr + static_cast<size_t>(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<uint8_t>((77 * rgb[0] + 150 * rgb[1] + 29 * rgb[2] + 128) >> 8);
continue;
}
for (const int c: rgb) {
*dst++ = static_cast<uint8_t>(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<int>(width), static_cast<int>(height),
static_cast<int>(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<int32_t>(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
+4
View File
@@ -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);
+342
View File
@@ -0,0 +1,342 @@
#include "libs/libJpegDec.cpp"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBI_WRITE_NO_STDIO
#include "stb_image_write.h"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <vector>
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<int>(y * 32), static_cast<int>(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<uint8_t> Encode(int quality, uint32_t width = WIDTH) {
std::vector<uint8_t> rgb(width * HEIGHT * 3);
for (uint32_t i = 0; i < rgb.size(); i++) {
rgb[i] = static_cast<uint8_t>(Expected(i / 3 % width, i / 3 / width, i % 3));
}
std::vector<uint8_t> jpeg;
stbi_write_jpg_to_func(
[](void* context, void* data, int size) {
auto* out = static_cast<std::vector<uint8_t>*>(context);
const auto* bytes = static_cast<const uint8_t*>(data);
out->insert(out->end(), bytes, bytes + size);
},
&jpeg, static_cast<int>(width), HEIGHT, 3, rgb.data(), quality);
return jpeg;
}
// The first marker of this type in an stb_image_write output.
auto Marker(std::vector<uint8_t>& 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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint8_t, (WIDTH * 4 + 8) * HEIGHT> 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<uint32_t>(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, &param, nullptr) ==
static_cast<int32_t>(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, &param, 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, &param, 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, &param, 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, &param, nullptr) ==
static_cast<int32_t>(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<uint32_t>(odd.size());
param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8U8Y8V8;
param.down_scale = 1;
CHECK(JpegDecDecode(decoder.handle, &param, 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<uint8_t, 128> coefficients {};
alignas(4) std::array<uint8_t, 8 * 8 * 4> 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, &param, &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, &param, nullptr) == JPEG_DEC_ERROR_INVALID_COEF_MEMORY);
param.coefficient_mem_addr = nullptr;
param.coefficient_mem_size = coefficients.size();
CHECK(JpegDecDecode(decoder.handle, &param, 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<uint8_t, WIDTH * HEIGHT * 4> image {};
JpegDecDecodeParam param {.jpeg_mem_addr = jpeg.data(),
.image_mem_addr = image.data(),
.jpeg_mem_size = static_cast<uint32_t>(jpeg.size()),
.image_mem_size = image.size(),
.down_scale = 1,
.pixel_format = JPEG_DEC_PIXEL_FORMAT_R8G8B8A8,
.alpha_value = 255};
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) > 0);
const auto dht = Marker(jpeg, 0xc4);
jpeg.erase(dht, dht + 2 + (dht[2] << 8 | dht[3]));
param.jpeg_mem_size = static_cast<uint32_t>(jpeg.size());
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) == JPEG_DEC_ERROR_DECODE_ERROR);
}
void TestDecodeValidation() {
auto jpeg = Encode(100);
Decoder decoder(WIDTH);
alignas(4) std::array<uint8_t, WIDTH * HEIGHT * 4> image {};
const JpegDecDecodeParam valid {.jpeg_mem_addr = jpeg.data(),
.image_mem_addr = image.data(),
.jpeg_mem_size = static_cast<uint32_t>(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, &param, nullptr) == JPEG_DEC_ERROR_INVALID_ADDR);
param.pixel_format = JPEG_DEC_PIXEL_FORMAT_Y8;
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) > 0);
param = valid;
param.image_mem_size--;
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) == JPEG_DEC_ERROR_INVALID_SIZE);
param = valid;
param.image_pitch = WIDTH * 4 - 4;
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) == JPEG_DEC_ERROR_INVALID_PARAM);
param = valid;
param.pixel_format = 2;
CHECK(JpegDecDecode(decoder.handle, &param, nullptr) == JPEG_DEC_ERROR_INVALID_PARAM);
param = valid;
param.alpha_value = 256;
CHECK(JpegDecDecode(decoder.handle, &param, 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, &param, 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");
}
+205
View File
@@ -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 <array>
#include <cstdio>
#include <cstdlib>
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<uint8_t, 4096>;
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<int>(y * 32), static_cast<int>(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<uint8_t, WIDTH * HEIGHT * 4> bgra {};
std::array<uint8_t, WIDTH * HEIGHT> 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, &param, &info);
CHECK(info.data_size == static_cast<uint32_t>(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, &param, 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, &param, 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, &param, 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<uint8_t, WIDTH * HEIGHT * 4> 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, &param, 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, &param, 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, &param, &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<uint8_t, WIDTH * HEIGHT * 4> 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, &param, nullptr) == JPEG_ENC_ERROR_INVALID_ADDR);
param = valid;
param.image_mem_size--;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_SIZE);
param = valid;
param.image_pitch = WIDTH * 4 - 4;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
param = valid;
param.sampling_type = JPEG_ENC_SAMPLING_TYPE_FULL;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
param = valid;
param.pixel_format = JPEG_ENC_PIXEL_FORMAT_Y8;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
param = valid;
param.restart_interval = -2;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
param = valid;
param.encode_mode = 2;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
param = valid;
param.pixel_format = 2;
CHECK(JpegEncEncode(encoder.handle, &param, nullptr) == JPEG_ENC_ERROR_INVALID_PARAM);
}
} // namespace
int main() {
TestEncode();
TestValidation();
std::puts("JPEG encoder tests passed");
}