#include "cell.h" #include #include #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)