176 lines
5.6 KiB
C
176 lines
5.6 KiB
C
#define QOI_IMPLEMENTATION
|
|
#include "qoi.h"
|
|
#include "cell.h"
|
|
#include <string.h>
|
|
#include <stdlib.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");
|
|
const char *format_str = JS_ToCString(js, format_val);
|
|
JS_FreeValue(js, format_val);
|
|
|
|
if (!format_str)
|
|
return JS_ThrowTypeError(js, "Invalid or missing pixel format");
|
|
|
|
// Determine channels from format string
|
|
int channels = 4; // Default to RGBA
|
|
if (strstr(format_str, "rgb") && !strstr(format_str, "a")) {
|
|
channels = 3; // RGB without alpha
|
|
}
|
|
|
|
// 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 == NULL)
|
|
return JS_EXCEPTION;
|
|
if (!pixel_data)
|
|
return JS_ThrowTypeError(js, "blob has no data");
|
|
|
|
// Validate buffer size
|
|
size_t required_size = width * height * channels;
|
|
if (pixel_len < required_size) {
|
|
JS_FreeCString(js, (char*)format_str);
|
|
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]);
|
|
}
|
|
|
|
// Encode to QOI
|
|
qoi_desc desc = {
|
|
.width = width,
|
|
.height = height,
|
|
.channels = channels,
|
|
.colorspace = colorspace
|
|
};
|
|
|
|
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 == NULL) return JS_EXCEPTION;
|
|
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)
|