Files
prosperon/cell_qoi.c
2025-11-26 12:52:56 -06:00

181 lines
5.8 KiB
C

#define QOI_IMPLEMENTATION
#include "qoi.h"
#include "cell.h"
#include "qjs_sdl.h"
#include <SDL3/SDL.h>
#include <string.h>
// Helper function to check for integer overflow in size calculations
static int check_size_overflow(size_t a, size_t b, size_t c, size_t *result)
{
if (a > SIZE_MAX / b) return 1;
size_t temp = a * b;
if (temp > SIZE_MAX / c) return 1;
*result = temp * c;
return 0;
}
// QOI compression/encoding
JSValue js_qoi_encode(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "compress_qoi requires an object argument");
// Check if width/height properties exist
JSValue width_val = JS_GetPropertyStr(js, argv[0], "width");
JSValue height_val = JS_GetPropertyStr(js, argv[0], "height");
if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "compress_qoi requires width and height properties");
}
int width, height;
if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "width and height must be numbers");
}
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
if (width < 1 || height < 1)
return JS_ThrowRangeError(js, "width and height must be at least 1");
// Get pixel format
JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
SDL_PixelFormat format = js2SDL_PixelFormat(js, format_val);
JS_FreeValue(js, format_val);
if (format == SDL_PIXELFORMAT_UNKNOWN)
return JS_ThrowTypeError(js, "Invalid or missing pixel format");
// Get pixels
JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
size_t pixel_len;
void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
JS_FreeValue(js, pixels_val);
if (!pixel_data)
return JS_ThrowTypeError(js, "pixels property must be a stoned blob");
// Validate buffer size
int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
size_t required_size;
if (check_size_overflow(width, height, bytes_per_pixel, &required_size)) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Image dimensions too large");
}
if (pixel_len < required_size)
return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)",
width, height, required_size, pixel_len);
// Get colorspace (optional, default to sRGB)
int colorspace = 0; // QOI_SRGB
if (argc > 1) {
colorspace = JS_ToBool(js, argv[1]);
}
// Determine number of channels based on format
int channels = SDL_ISPIXELFORMAT_ALPHA(format) ? 4 : 3;
// Prepare QOI descriptor
qoi_desc desc = {
.width = width,
.height = height,
.channels = channels,
.colorspace = colorspace
};
// Encode to QOI
int out_len;
void *qoi_data = qoi_encode(pixel_data, &desc, &out_len);
if (!qoi_data)
return JS_ThrowInternalError(js, "QOI encoding failed");
// Create result object
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, height));
JS_SetPropertyStr(js, result, "format", JS_NewString(js, "qoi"));
JS_SetPropertyStr(js, result, "channels", JS_NewInt32(js, channels));
JS_SetPropertyStr(js, result, "colorspace", JS_NewInt32(js, colorspace));
JSValue compressed_pixels = js_new_blob_stoned_copy(js, qoi_data, out_len);
free(qoi_data); // Free the QOI buffer after copying to blob
JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
return result;
}
// QOI decompression/decoding
JSValue js_qoi_decode(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
size_t len;
void *raw = js_get_blob_data(js, &len, argv[0]);
if (!raw) return JS_ThrowReferenceError(js, "could not get QOI data from array buffer");
qoi_desc desc;
void *data = qoi_decode(raw, len, &desc, 0); // 0 means use channels from file
if (!data)
return JS_NULL; // Return null if not valid QOI
// QOI always decodes to either RGB or RGBA based on the file's channel count
int channels = desc.channels;
int pitch = desc.width * channels;
size_t pixels_size = pitch * desc.height;
// If it's RGB, convert to RGBA for consistency
void *rgba_data = data;
if (channels == 3) {
rgba_data = malloc(desc.width * desc.height * 4);
if (!rgba_data) {
free(data);
return JS_ThrowOutOfMemory(js);
}
// Convert RGB to RGBA
unsigned char *src = (unsigned char*)data;
unsigned char *dst = (unsigned char*)rgba_data;
for (int i = 0; i < desc.width * desc.height; i++) {
dst[i*4] = src[i*3];
dst[i*4+1] = src[i*3+1];
dst[i*4+2] = src[i*3+2];
dst[i*4+3] = 255;
}
free(data);
pitch = desc.width * 4;
pixels_size = pitch * desc.height;
}
// Create JS object with surface data
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, desc.width));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, desc.height));
JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, pitch));
JS_SetPropertyStr(js, obj, "pixels", js_new_blob_stoned_copy(js, rgba_data, pixels_size));
JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, 8));
JS_SetPropertyStr(js, obj, "hdr", JS_NewBool(js, 0));
JS_SetPropertyStr(js, obj, "colorspace", JS_NewInt32(js, desc.colorspace));
free(rgba_data);
return obj;
}
static const JSCFunctionListEntry js_qoi_funcs[] = {
MIST_FUNC_DEF(qoi, encode, 1),
MIST_FUNC_DEF(qoi, decode, 1)
};
JSValue js_qoi_use(JSContext *js)
{
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_qoi_funcs, countof(js_qoi_funcs));
return mod;
}