Files
cell-image/resize.c
2025-12-14 13:30:24 -06:00

195 lines
7.0 KiB
C

#include "cell.h"
#include <string.h>
#include <stdlib.h>
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize2.h"
// Resize an image to new dimensions
// Input: image object with {width, height, pixels (blob)}
// Optional: opts object with {filter: "nearest"|"linear"|"cubic"|"mitchell"}
// Returns: new image object with resized pixels
JSValue js_resize_resize(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
if (argc < 3)
return JS_ThrowTypeError(js, "resize.resize requires (image, new_width, new_height, [opts])");
// Get source dimensions
JSValue src_width_val = JS_GetPropertyStr(js, argv[0], "width");
JSValue src_height_val = JS_GetPropertyStr(js, argv[0], "height");
if (JS_IsNull(src_width_val) || JS_IsNull(src_height_val)) {
JS_FreeValue(js, src_width_val);
JS_FreeValue(js, src_height_val);
return JS_ThrowTypeError(js, "resize.resize requires image with width and height properties");
}
int src_width, src_height;
if (JS_ToInt32(js, &src_width, src_width_val) < 0 || JS_ToInt32(js, &src_height, src_height_val) < 0) {
JS_FreeValue(js, src_width_val);
JS_FreeValue(js, src_height_val);
return JS_ThrowTypeError(js, "width and height must be numbers");
}
JS_FreeValue(js, src_width_val);
JS_FreeValue(js, src_height_val);
// Get destination dimensions
int dst_width, dst_height;
if (JS_ToInt32(js, &dst_width, argv[1]) < 0 || JS_ToInt32(js, &dst_height, argv[2]) < 0)
return JS_ThrowTypeError(js, "new_width and new_height must be numbers");
if (dst_width < 1 || dst_height < 1)
return JS_ThrowRangeError(js, "destination dimensions must be at least 1x1");
if (src_width < 1 || src_height < 1)
return JS_ThrowRangeError(js, "source dimensions must be at least 1x1");
// Get source pixels
JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
size_t pixel_len;
void *src_pixels = js_get_blob_data(js, &pixel_len, pixels_val);
JS_FreeValue(js, pixels_val);
if (src_pixels == NULL) return JS_EXCEPTION;
if (!src_pixels) return JS_ThrowTypeError(js, "pixels blob has no data");
size_t required_size = src_width * src_height * 4;
if (pixel_len < required_size)
return JS_ThrowRangeError(js, "source pixels buffer too small (need %zu, got %zu)", required_size, pixel_len);
// Determine filter from options
stbir_filter filter = STBIR_FILTER_DEFAULT;
if (argc >= 4 && !JS_IsNull(argv[3])) {
JSValue filter_val = JS_GetPropertyStr(js, argv[3], "filter");
if (!JS_IsNull(filter_val)) {
const char *filter_str = JS_ToCString(js, filter_val);
if (filter_str) {
if (strcmp(filter_str, "nearest") == 0 || strcmp(filter_str, "point") == 0)
filter = STBIR_FILTER_POINT_SAMPLE;
else if (strcmp(filter_str, "linear") == 0 || strcmp(filter_str, "box") == 0)
filter = STBIR_FILTER_BOX;
else if (strcmp(filter_str, "cubic") == 0 || strcmp(filter_str, "catmullrom") == 0)
filter = STBIR_FILTER_CATMULLROM;
else if (strcmp(filter_str, "mitchell") == 0)
filter = STBIR_FILTER_MITCHELL;
JS_FreeCString(js, filter_str);
}
}
JS_FreeValue(js, filter_val);
}
// Allocate destination buffer
size_t dst_size = dst_width * dst_height * 4;
unsigned char *dst_pixels = malloc(dst_size);
if (!dst_pixels)
return JS_ThrowInternalError(js, "failed to allocate destination buffer");
// Perform resize using stbir
STBIR_RESIZE resize;
stbir_resize_init(&resize,
src_pixels, src_width, src_height, src_width * 4,
dst_pixels, dst_width, dst_height, dst_width * 4,
STBIR_RGBA, STBIR_TYPE_UINT8);
stbir_set_filters(&resize, filter, filter);
stbir_set_pixel_subrect(&resize, 0, 0, dst_width, dst_height);
int result = stbir_resize_extended(&resize);
if (!result) {
free(dst_pixels);
return JS_ThrowInternalError(js, "stbir resize failed");
}
// Build result object
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, dst_width));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, dst_height));
JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, dst_width * 4));
JS_SetPropertyStr(js, obj, "pixels", js_new_blob_stoned_copy(js, dst_pixels, dst_size));
JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, 8));
free(dst_pixels);
return obj;
}
// Convenience function to resize to fit within max dimensions while preserving aspect ratio
// resize.fit(image, max_width, max_height, [opts])
JSValue js_resize_fit(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
if (argc < 3)
return JS_ThrowTypeError(js, "resize.fit requires (image, max_width, max_height, [opts])");
JSValue src_width_val = JS_GetPropertyStr(js, argv[0], "width");
JSValue src_height_val = JS_GetPropertyStr(js, argv[0], "height");
int src_width, src_height;
if (JS_ToInt32(js, &src_width, src_width_val) < 0 || JS_ToInt32(js, &src_height, src_height_val) < 0) {
JS_FreeValue(js, src_width_val);
JS_FreeValue(js, src_height_val);
return JS_ThrowTypeError(js, "image must have width and height");
}
JS_FreeValue(js, src_width_val);
JS_FreeValue(js, src_height_val);
int max_width, max_height;
if (JS_ToInt32(js, &max_width, argv[1]) < 0 || JS_ToInt32(js, &max_height, argv[2]) < 0)
return JS_ThrowTypeError(js, "max dimensions must be numbers");
// Calculate scaled dimensions preserving aspect ratio
double scale_x = (double)max_width / src_width;
double scale_y = (double)max_height / src_height;
double scale = scale_x < scale_y ? scale_x : scale_y;
int dst_width = (int)(src_width * scale);
int dst_height = (int)(src_height * scale);
if (dst_width < 1) dst_width = 1;
if (dst_height < 1) dst_height = 1;
// Build args for resize
JSValue new_args[4];
new_args[0] = argv[0];
new_args[1] = JS_NewInt32(js, dst_width);
new_args[2] = JS_NewInt32(js, dst_height);
new_args[3] = argc >= 4 ? argv[3] : JS_NULL;
JSValue result = js_resize_resize(js, this_val, 4, new_args);
JS_FreeValue(js, new_args[1]);
JS_FreeValue(js, new_args[2]);
return result;
}
// Resize to exact square dimensions (useful for texture atlases)
// resize.square(image, size, [opts])
JSValue js_resize_square(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
if (argc < 2)
return JS_ThrowTypeError(js, "resize.square requires (image, size, [opts])");
int size;
if (JS_ToInt32(js, &size, argv[1]) < 0)
return JS_ThrowTypeError(js, "size must be a number");
JSValue new_args[4];
new_args[0] = argv[0];
new_args[1] = JS_NewInt32(js, size);
new_args[2] = JS_NewInt32(js, size);
new_args[3] = argc >= 3 ? argv[2] : JS_NULL;
JSValue result = js_resize_resize(js, this_val, 4, new_args);
JS_FreeValue(js, new_args[1]);
JS_FreeValue(js, new_args[2]);
return result;
}
static const JSCFunctionListEntry js_resize_funcs[] = {
MIST_FUNC_DEF(resize, resize, 4),
MIST_FUNC_DEF(resize, fit, 4),
MIST_FUNC_DEF(resize, square, 3)
};
CELL_USE_FUNCS(js_resize_funcs)