#define QOI_IMPLEMENTATION #include "qoi.h" #include "cell.h" #include "qjs_sdl.h" #include #include // 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) }; CELL_USE_FUNCS(js_qoi_funcs)