#include "cell.h" #include "prosperon.h" #include #include #include #include #include #include "sdl.h" #define STB_DXT_IMPLEMENTATION #include "stb_dxt.h" irect js2irect(JSContext *js, JSValue v) { if (JS_IsNull(v)) return (irect){0,0,1,1}; irect rect; JS_GETATOM(js,rect.x,v,x,number) JS_GETATOM(js,rect.y,v,y,number) JS_GETATOM(js,rect.w,v,width,number) JS_GETATOM(js,rect.h,v,height,number) float anchor_x, anchor_y; JS_GETATOM(js, anchor_x, v, anchor_x, number) JS_GETATOM(js, anchor_y, v, anchor_y, number) rect.y -= anchor_y*rect.h; rect.x -= anchor_x*rect.w; return rect; } JSValue pixelformat2js(JSContext *js, SDL_PixelFormat fmt) { return SDL_PixelFormat2js(js, fmt); } SDL_PixelFormat js2pixelformat(JSContext *js, JSValue v) { return js2SDL_PixelFormat(js, v); } typedef struct { const char *name; SDL_ScaleMode mode; } scale_entry; static const scale_entry k_scale_table[] = { { "nearest", SDL_SCALEMODE_NEAREST }, { "linear", SDL_SCALEMODE_LINEAR }, { NULL, SDL_SCALEMODE_LINEAR } /* fallback */ }; static JSValue scalemode2js(JSContext *js, SDL_ScaleMode mode){ const scale_entry *it; for(it = k_scale_table; it->name; ++it) if(it->mode == mode) break; return JS_NewString(js, it->name ? it->name : "nearest"); } SDL_ScaleMode js2SDL_ScaleMode(JSContext *js, JSValue v){ if(JS_IsNull(v)) return SDL_SCALEMODE_NEAREST; const char *s = JS_ToCString(js, v); if(!s) return SDL_SCALEMODE_NEAREST; const scale_entry *it; for(it = k_scale_table; it->name; ++it) if(!strcmp(it->name, s)) break; JS_FreeCString(js, s); return it->mode; } // SDL_Surface free function void SDL_Surface_free(JSRuntime *rt, SDL_Surface *s) { if (s->flags & SDL_SURFACE_PREALLOCATED) free(s->pixels); SDL_DestroySurface(s); } // Class definition QJSCLASS(SDL_Surface,) // SDL_Surface methods JSC_CCALL(surface_blit, SDL_Surface *dst = js2SDL_Surface(js, self); irect dr = {0}, *pdr = NULL; if(!JS_IsNull(argv[0])){ dr = js2irect(js, argv[0]); pdr = &dr; } SDL_Surface *src = js2SDL_Surface(js, argv[1]); if (!src) return JS_ThrowReferenceError(js, "Argument must be a surface."); irect sr = {0}, *psr = NULL; if(!JS_IsNull(argv[2])){ sr = js2irect(js, argv[2]); psr = &sr; } SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[3]); SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE); SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode); ) JSC_CCALL(surface_scale, SDL_Surface *src = js2SDL_Surface(js,self); HMM_Vec2 wh = js2vec2(js,argv[0]); SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[1]); SDL_Surface *new = SDL_ScaleSurface(src, wh.x, wh.y, mode); ret = SDL_Surface2js(js,new); ) static SDL_PixelFormatDetails pdetails = { .format = SDL_PIXELFORMAT_RGBA8888, // Standard RGBA8888 format .bits_per_pixel = 32, // 8 bits per channel, 4 channels = 32 bits .bytes_per_pixel = 4, // 4 bytes per pixel .padding = {0, 0}, // Unused padding .Rmask = 0xFF000000, // Red mask .Gmask = 0x00FF0000, // Green mask .Bmask = 0x0000FF00, // Blue mask .Amask = 0x000000FF, // Alpha mask .Rbits = 8, // 8 bits for Red .Gbits = 8, // 8 bits for Green .Bbits = 8, // 8 bits for Blue .Abits = 8, // 8 bits for Alpha .Rshift = 24, // Red shift .Gshift = 16, // Green shift .Bshift = 8, // Blue shift .Ashift = 0 // Alpha shift }; JSC_CCALL(surface_fill, SDL_Surface *src = js2SDL_Surface(js,self); colorf color = js2color(js,argv[0]); rect r = { .x = 0, .y = 0, .w = src->w, .h = src->h }; SDL_FillSurfaceRect(src, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255,color.g*255,color.b*255,color.a*255)); ) JSC_CCALL(surface_rect, SDL_Surface *dst = js2SDL_Surface(js,self); rect r = js2rect(js,argv[0]); colorf color = js2color(js,argv[1]); SDL_FillSurfaceRect(dst,&r,SDL_MapRGBA(&pdetails,NULL, color.r*255,color.g*255,color.b*255,color.a*255)); ) JSC_CCALL(surface_convert, SDL_Surface *surf = js2SDL_Surface(js,self); SDL_PixelFormat fmt = js2pixelformat(js, argv[0]); SDL_Surface *dst = SDL_ConvertSurface(surf, fmt); if (!dst) return JS_ThrowInternalError(js, "Convert failed: %s", SDL_GetError()); return SDL_Surface2js(js, dst); ) JSC_CCALL(surface_dup, SDL_Surface *surf = js2SDL_Surface(js,self); SDL_Surface *conv = SDL_DuplicateSurface(surf); if (!conv) return JS_ThrowReferenceError(js, "could not blit to dup'd surface: %s", SDL_GetError()); return SDL_Surface2js(js,conv); ) JSC_CCALL(surface_pixels, SDL_Surface *surf = js2SDL_Surface(js, self); int locked = 0; if (SDL_MUSTLOCK(surf)) if (SDL_LockSurface(surf) < 0) return JS_ThrowReferenceError(js, "Lock surface failed: %s", SDL_GetError()); size_t byte_size; if (SDL_ISPIXELFORMAT_FOURCC(surf->format)) { /* Planar/YUV formats: use BitsPerPixel to compute true size */ printf("FOURCC!!! Bits is %d\n", SDL_BYTESPERPIXEL(surf->format)); byte_size = (size_t)surf->pitch * surf->h * SDL_BYTESPERPIXEL(surf->format); } else byte_size = (size_t)surf->pitch * surf->h; ret = js_new_blob_stoned_copy(js, surf->pixels, byte_size); if (locked) SDL_UnlockSurface(surf); ) JSValue js_surface_get_width(JSContext *js, JSValue self) { SDL_Surface *s = js2SDL_Surface(js,self); return JS_NewFloat64(js, s->w); } JSValue js_surface_get_height(JSContext *js, JSValue self) { SDL_Surface *s = js2SDL_Surface(js,self); return JS_NewFloat64(js, s->h); } JSValue js_surface_get_format(JSContext *js, JSValue self) { SDL_Surface *s = js2SDL_Surface(js,self); return pixelformat2js(js, s->format); } JSValue js_surface_get_pitch(JSContext *js, JSValue self) { SDL_Surface *s = js2SDL_Surface(js,self); return JS_NewFloat64(js, s->pitch); } JSC_CCALL(surface_toJSON, SDL_Surface *surf = js2SDL_Surface(js,self); // Create the result object JSValue obj = JS_NewObject(js); // Add width and height JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w)); JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h)); // Add format JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format)); // Add pitch JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch)); // Lock surface if needed int locked = 0; if (SDL_MUSTLOCK(surf)) { if (SDL_LockSurface(surf) < 0) { JS_FreeValue(js, obj); return JS_ThrowInternalError(js, "Lock surface failed: %s", SDL_GetError()); } locked = 1; } // Add pixels as ArrayBuffer size_t byte_size = surf->pitch * surf->h; JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size); JS_SetPropertyStr(js, obj, "pixels", pixels); // Unlock if we locked if (locked) SDL_UnlockSurface(surf); return obj; ) // 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; } // Helper function for BC1/BC3 compression static JSValue compress_bc_common(JSContext *js, JSValueConst *argv, int argc, int alpha_mode, const char *format_name) { if (argc < 1) return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name); // 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_%s requires width and height properties", format_name); } 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"); if (width % 4 != 0 || height % 4 != 0) return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression"); // Get pixel format JSValue format_val = JS_GetPropertyStr(js, argv[0], "format"); SDL_PixelFormat format = js2pixelformat(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); if (!pixel_data) { JS_FreeValue(js, pixels_val); return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer"); } // 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) { JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)", width, height, required_size, pixel_len); } // Get high quality mode (default true) int high_quality = 1; if (argc > 1) { high_quality = JS_ToBool(js, argv[1]); } int mode = high_quality ? STB_DXT_HIGHQUAL : STB_DXT_NORMAL; // Calculate output size with overflow check int blocks_x = width / 4; int blocks_y = height / 4; int bytes_per_block = (alpha_mode == 0) ? 8 : 16; // BC1=8, BC3=16 size_t output_size; if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) { JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "Output dimensions too large"); } // Allocate output buffer unsigned char *output = malloc(output_size); if (!output) { JS_FreeValue(js, pixels_val); return JS_ThrowOutOfMemory(js); } // Allocate RGBA conversion buffer size_t rgba_size; if (check_size_overflow(width, height, 4, &rgba_size)) { free(output); JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "RGBA buffer size too large"); } unsigned char *rgba_data = malloc(rgba_size); if (!rgba_data) { free(output); JS_FreeValue(js, pixels_val); return JS_ThrowOutOfMemory(js); } // Convert to RGBA using SDL int convert_result = SDL_ConvertPixels( width, height, format, pixel_data, width * bytes_per_pixel, SDL_PIXELFORMAT_RGBA32, rgba_data, width * 4 ); JS_FreeValue(js, pixels_val); if (convert_result != 0) { free(output); free(rgba_data); return JS_ThrowInternalError(js, "Failed to convert pixels: %s", SDL_GetError()); } // Compress blocks for (int by = 0; by < blocks_y; by++) { for (int bx = 0; bx < blocks_x; bx++) { unsigned char block[64]; // 4x4 RGBA = 64 bytes // Extract 4x4 block for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { int src_x = bx * 4 + x; int src_y = by * 4 + y; int src_idx = (src_y * width + src_x) * 4; int dst_idx = (y * 4 + x) * 4; block[dst_idx + 0] = rgba_data[src_idx + 0]; block[dst_idx + 1] = rgba_data[src_idx + 1]; block[dst_idx + 2] = rgba_data[src_idx + 2]; block[dst_idx + 3] = rgba_data[src_idx + 3]; } } // Compress block int output_idx = (by * blocks_x + bx) * bytes_per_block; stb_compress_dxt_block(output + output_idx, block, alpha_mode, mode); } } free(rgba_data); // 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, format_name)); JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block)); JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size); free(output); // Free the output buffer after copying to blob JS_SetPropertyStr(js, result, "pixels", compressed_pixels); return result; } // BC1/DXT1 compression JSC_CCALL(surface_compress_bc1, return compress_bc_common(js, argv, argc, 0, "bc1"); ) // BC3/DXT5 compression JSC_CCALL(surface_compress_bc3, return compress_bc_common(js, argv, argc, 1, "bc3"); ) // Generic helper for BC4/BC5 channel compression static JSValue compress_bc_channels(JSContext *js, JSValueConst *argv, int argc, int num_channels, const char *format_name, void (*compress_func)(unsigned char *dest, const unsigned char *src)) { if (argc < 1) return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name); // 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_%s requires width and height properties", format_name); } 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"); if (width % 4 != 0 || height % 4 != 0) return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression"); // Get pixel format JSValue format_val = JS_GetPropertyStr(js, argv[0], "format"); SDL_PixelFormat format = js2pixelformat(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); if (!pixel_data) { JS_FreeValue(js, pixels_val); return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer"); } // Validate buffer size int bytes_per_pixel = SDL_BYTESPERPIXEL(format); if (bytes_per_pixel < num_channels) { JS_FreeValue(js, pixels_val); return JS_ThrowTypeError(js, "%s compression requires a format with at least %d channel(s)", format_name, num_channels); } 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) { JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)", width, height, required_size, pixel_len); } // Calculate output size with overflow check int blocks_x = width / 4; int blocks_y = height / 4; int bytes_per_block = (num_channels == 1) ? 8 : 16; // BC4=8, BC5=16 size_t output_size; if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) { JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "Output dimensions too large"); } // Allocate output buffer unsigned char *output = malloc(output_size); if (!output) { JS_FreeValue(js, pixels_val); return JS_ThrowOutOfMemory(js); } // Allocate channel extraction buffer size_t channel_size; if (check_size_overflow(width, height, num_channels, &channel_size)) { free(output); JS_FreeValue(js, pixels_val); return JS_ThrowRangeError(js, "Channel buffer size too large"); } unsigned char *channel_data = malloc(channel_size); if (!channel_data) { free(output); JS_FreeValue(js, pixels_val); return JS_ThrowOutOfMemory(js); } // Extract channels for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int src_idx = (y * width + x) * bytes_per_pixel; int dst_idx = (y * width + x) * num_channels; // Extract first 'num_channels' channels for (int c = 0; c < num_channels; c++) { channel_data[dst_idx + c] = ((unsigned char*)pixel_data)[src_idx + c]; } } } JS_FreeValue(js, pixels_val); // Compress blocks for (int by = 0; by < blocks_y; by++) { for (int bx = 0; bx < blocks_x; bx++) { unsigned char block[32]; // Max 4x4 * 2 channels = 32 bytes // Extract 4x4 block for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { int src_x = bx * 4 + x; int src_y = by * 4 + y; int src_idx = (src_y * width + src_x) * num_channels; int dst_idx = (y * 4 + x) * num_channels; for (int c = 0; c < num_channels; c++) { block[dst_idx + c] = channel_data[src_idx + c]; } } } // Compress block int output_idx = (by * blocks_x + bx) * bytes_per_block; compress_func(output + output_idx, block); } } free(channel_data); // 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, format_name)); JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block)); JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size); free(output); // Free the output buffer after copying to blob JS_SetPropertyStr(js, result, "pixels", compressed_pixels); return result; } // BC4 compression (single channel) JSC_CCALL(surface_compress_bc4, return compress_bc_channels(js, argv, argc, 1, "bc4", stb_compress_bc4_block); ) // BC5 compression (two channels) JSC_CCALL(surface_compress_bc5, return compress_bc_channels(js, argv, argc, 2, "bc5", stb_compress_bc5_block); ) // Constructor for SDL_Surface JSC_CCALL(surface_constructor, if (argc < 1) return JS_ThrowTypeError(js, "Surface constructor requires an object argument"); // Get width and height int width, height; JS_GETATOM(js, width, argv[0], width, number) JS_GETATOM(js, height, argv[0], height, number) if (!width || !height) return JS_ThrowTypeError(js, "Surface constructor requires width and height properties"); // Check for pixel format SDL_PixelFormat format = SDL_PIXELFORMAT_RGBA32; // default JSValue format_val = JS_GetPropertyStr(js, argv[0], "format"); if (!JS_IsNull(format_val)) { format = js2pixelformat(js, format_val); } JS_FreeValue(js, format_val); // Check for pixel data JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels"); if (!JS_IsNull(pixels_val)) { // Create surface from pixel data size_t len; void *raw = js_get_blob_data(js, &len, pixels_val); if (raw == -1) { JS_FreeValue(js, pixels_val); return JS_EXCEPTION; } if (!raw) { JS_FreeValue(js, pixels_val); return JS_ThrowTypeError(js, "no data"); } int pitch; JSValue pitch_val = JS_GetPropertyStr(js, argv[0], "pitch"); if (!JS_IsNull(pitch_val)) { pitch = js2number(js, pitch_val); JS_FreeValue(js, pitch_val); } else { // Calculate pitch based on format int bytes_per_pixel = SDL_BYTESPERPIXEL(format); pitch = width * bytes_per_pixel; } // Copy the pixel data void *pixels_copy = malloc(len); if (!pixels_copy) { JS_FreeValue(js, pixels_val); return JS_ThrowOutOfMemory(js); } memcpy(pixels_copy, raw, len); SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch); if (!surface) { free(pixels_copy); JS_FreeValue(js, pixels_val); return JS_ThrowInternalError(js, "Failed to create surface from pixels: %s", SDL_GetError()); } JS_FreeValue(js, pixels_val); ret = SDL_Surface2js(js, surface); } else { // Create blank surface SDL_Surface *surface = SDL_CreateSurface(width, height, format); if (!surface) return JS_ThrowInternalError(js, "Failed to create surface: %s", SDL_GetError()); ret = SDL_Surface2js(js, surface); } ) static const JSCFunctionListEntry js_SDL_Surface_funcs[] = { MIST_FUNC_DEF(surface, blit, 4), MIST_FUNC_DEF(surface, scale, 1), MIST_FUNC_DEF(surface, fill,1), MIST_FUNC_DEF(surface, rect,2), MIST_FUNC_DEF(surface, dup, 0), MIST_FUNC_DEF(surface, pixels, 0), MIST_FUNC_DEF(surface, convert, 1), MIST_FUNC_DEF(surface, toJSON, 0), JS_CGETSET_DEF("width", js_surface_get_width, NULL), JS_CGETSET_DEF("height", js_surface_get_height, NULL), JS_CGETSET_DEF("format", js_surface_get_format, NULL), JS_CGETSET_DEF("pitch", js_surface_get_pitch, NULL), }; // Helper function to create SDL_Surface from image object static SDL_Surface* image_to_surface(JSContext *js, JSValue img_obj) { // Get width and height JSValue width_val = JS_GetPropertyStr(js, img_obj, "width"); JSValue height_val = JS_GetPropertyStr(js, img_obj, "height"); if (JS_IsNull(width_val) || JS_IsNull(height_val)) { JS_FreeValue(js, width_val); JS_FreeValue(js, height_val); return NULL; } 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 NULL; } JS_FreeValue(js, width_val); JS_FreeValue(js, height_val); // Get format JSValue format_val = JS_GetPropertyStr(js, img_obj, "format"); SDL_PixelFormat format = js2pixelformat(js, format_val); JS_FreeValue(js, format_val); if (format == SDL_PIXELFORMAT_UNKNOWN) format = SDL_PIXELFORMAT_RGBA32; // default // Get pixels JSValue pixels_val = JS_GetPropertyStr(js, img_obj, "pixels"); size_t pixel_len; void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val); if (pixel_data == -1) { JS_FreeValue(js, pixels_val); return NULL; } if (!pixel_data) { JS_FreeValue(js, pixels_val); return NULL; } // Get pitch (optional) int pitch; JSValue pitch_val = JS_GetPropertyStr(js, img_obj, "pitch"); if (!JS_IsNull(pitch_val)) { pitch = js2number(js, pitch_val); JS_FreeValue(js, pitch_val); } else { pitch = width * SDL_BYTESPERPIXEL(format); } // Create a copy of pixel data since SDL_Surface will own it void *pixels_copy = malloc(pixel_len); if (!pixels_copy) { JS_FreeValue(js, pixels_val); return NULL; } memcpy(pixels_copy, pixel_data, pixel_len); JS_FreeValue(js, pixels_val); SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch); if (!surface) { free(pixels_copy); return NULL; } return surface; } // Helper function to convert SDL_Surface back to image object static JSValue surface_to_image(JSContext *js, SDL_Surface *surf) { JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w)); JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h)); JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format)); JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch)); // Lock surface if needed int locked = 0; if (SDL_MUSTLOCK(surf)) { if (SDL_LockSurface(surf) < 0) { JS_FreeValue(js, obj); return JS_NULL; } locked = 1; } // Add pixels as stoned blob size_t byte_size = surf->pitch * surf->h; JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size); JS_SetPropertyStr(js, obj, "pixels", pixels); // Unlock if we locked if (locked) SDL_UnlockSurface(surf); // Add depth and hdr properties for completeness JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(surf->format))); JS_SetPropertyStr(js, obj, "hdr", JS_FALSE); return obj; } // Scale function for image objects JSC_CCALL(surface_scale_img, if (argc < 2) return JS_ThrowTypeError(js, "scale requires image and options objects"); SDL_Surface *src = image_to_surface(js, argv[0]); if (!src) return JS_ThrowTypeError(js, "First argument must be a valid image object"); // Get width and height from options JSValue width_val = JS_GetPropertyStr(js, argv[1], "width"); JSValue height_val = JS_GetPropertyStr(js, argv[1], "height"); int new_width = src->w, new_height = src->h; if (!JS_IsNull(width_val)) JS_ToInt32(js, &new_width, width_val); if (!JS_IsNull(height_val)) JS_ToInt32(js, &new_height, height_val); JS_FreeValue(js, width_val); JS_FreeValue(js, height_val); // Get scale mode JSValue mode_val = JS_GetPropertyStr(js, argv[1], "mode"); SDL_ScaleMode mode = js2SDL_ScaleMode(js, mode_val); JS_FreeValue(js, mode_val); SDL_Surface *dst = SDL_ScaleSurface(src, new_width, new_height, mode); SDL_DestroySurface(src); if (!dst) return JS_ThrowInternalError(js, "Scale failed: %s", SDL_GetError()); JSValue result = surface_to_image(js, dst); SDL_DestroySurface(dst); return result; ) // Fill function for image objects JSC_CCALL(surface_fill_img, if (argc < 2) return JS_ThrowTypeError(js, "fill requires image and color"); SDL_Surface *surf = image_to_surface(js, argv[0]); if (!surf) return JS_ThrowTypeError(js, "First argument must be a valid image object"); colorf color = js2color(js, argv[1]); rect r = { .x = 0, .y = 0, .w = surf->w, .h = surf->h }; SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255, color.g*255, color.b*255, color.a*255)); JSValue result = surface_to_image(js, surf); SDL_DestroySurface(surf); return result; ) // Rect function for image objects JSC_CCALL(surface_rect_img, if (argc < 3) return JS_ThrowTypeError(js, "rect requires image, rectangle, and color"); SDL_Surface *surf = image_to_surface(js, argv[0]); if (!surf) return JS_ThrowTypeError(js, "First argument must be a valid image object"); rect r = js2rect(js, argv[1]); colorf color = js2color(js, argv[2]); SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255, color.g*255, color.b*255, color.a*255)); JSValue result = surface_to_image(js, surf); SDL_DestroySurface(surf); return result; ) // Blit function for image objects JSC_CCALL(surface_blit_img, if (argc < 2) return JS_ThrowTypeError(js, "blit requires destination and source images"); SDL_Surface *dst = image_to_surface(js, argv[0]); if (!dst) return JS_ThrowTypeError(js, "First argument must be a valid destination image"); SDL_Surface *src = image_to_surface(js, argv[1]); if (!src) { SDL_DestroySurface(dst); return JS_ThrowTypeError(js, "Second argument must be a valid source image"); } irect dr = {0}, *pdr = NULL; if (argc > 2 && !JS_IsNull(argv[2])) { dr = js2irect(js, argv[2]); pdr = &dr; } irect sr = {0}, *psr = NULL; if (argc > 3 && !JS_IsNull(argv[3])) { sr = js2irect(js, argv[3]); psr = &sr; } SDL_ScaleMode mode = SDL_SCALEMODE_LINEAR; if (argc > 4) mode = js2SDL_ScaleMode(js, argv[4]); SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE); SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode); SDL_DestroySurface(src); JSValue result = surface_to_image(js, dst); SDL_DestroySurface(dst); return result; ) // Duplicate function for image objects JSC_CCALL(surface_dup_img, if (argc < 1) return JS_ThrowTypeError(js, "dup requires an image object"); SDL_Surface *surf = image_to_surface(js, argv[0]); if (!surf) return JS_ThrowTypeError(js, "Argument must be a valid image object"); SDL_Surface *dup = SDL_DuplicateSurface(surf); SDL_DestroySurface(surf); if (!dup) return JS_ThrowInternalError(js, "Duplicate failed: %s", SDL_GetError()); JSValue result = surface_to_image(js, dup); SDL_DestroySurface(dup); return result; ) // Generic convert function for pixel format/colorspace conversion JSC_CCALL(surface_convert_generic, if (argc < 2) return JS_ThrowTypeError(js, "convert requires source and conversion objects"); // Parse source object int src_width, src_height; JS_GETATOM(js, src_width, argv[0], width, number) JS_GETATOM(js, src_height, argv[0], height, number) if (!src_width || !src_height) return JS_ThrowTypeError(js, "source object requires width and height"); // Get source format JSValue src_format_val = JS_GetPropertyStr(js, argv[0], "format"); SDL_PixelFormat src_format = js2pixelformat(js, src_format_val); JS_FreeValue(js, src_format_val); if (src_format == SDL_PIXELFORMAT_UNKNOWN) return JS_ThrowTypeError(js, "source object requires valid format"); // Get source pixels JSValue src_pixels_val = JS_GetPropertyStr(js, argv[0], "pixels"); size_t src_len; void *src_pixels = js_get_blob_data(js, &src_len, src_pixels_val); if (src_pixels == -1) { JS_FreeValue(js, src_pixels_val); return JS_EXCEPTION; } // Get source pitch (optional, calculate if not provided) int src_pitch; JSValue src_pitch_val = JS_GetPropertyStr(js, argv[0], "pitch"); if (!JS_IsNull(src_pitch_val)) { src_pitch = js2number(js, src_pitch_val); JS_FreeValue(js, src_pitch_val); } else { src_pitch = src_width * SDL_BYTESPERPIXEL(src_format); } // Get source colorspace (optional) JSValue src_colorspace_val = JS_GetPropertyStr(js, argv[0], "colorspace"); SDL_Colorspace src_colorspace = SDL_COLORSPACE_SRGB; // default if (!JS_IsNull(src_colorspace_val)) { // For now, we'll use a simple numeric value for colorspace int colorspace_num; if (JS_ToInt32(js, &colorspace_num, src_colorspace_val) == 0) { src_colorspace = (SDL_Colorspace)colorspace_num; } } JS_FreeValue(js, src_colorspace_val); // Parse conversion object JSValue dst_format_val = JS_GetPropertyStr(js, argv[1], "format"); SDL_PixelFormat dst_format = js2pixelformat(js, dst_format_val); JS_FreeValue(js, dst_format_val); if (dst_format == SDL_PIXELFORMAT_UNKNOWN) return JS_ThrowTypeError(js, "conversion object requires valid format"); // Get destination pitch (optional) int dst_pitch; JSValue dst_pitch_val = JS_GetPropertyStr(js, argv[1], "pitch"); if (!JS_IsNull(dst_pitch_val)) { dst_pitch = js2number(js, dst_pitch_val); JS_FreeValue(js, dst_pitch_val); } else { dst_pitch = src_width * SDL_BYTESPERPIXEL(dst_format); } // Get destination colorspace (optional) JSValue dst_colorspace_val = JS_GetPropertyStr(js, argv[1], "colorspace"); SDL_Colorspace dst_colorspace = SDL_COLORSPACE_SRGB; // default if (!JS_IsNull(dst_colorspace_val)) { int colorspace_num; if (JS_ToInt32(js, &colorspace_num, dst_colorspace_val) == 0) { dst_colorspace = (SDL_Colorspace)colorspace_num; } } JS_FreeValue(js, dst_colorspace_val); // Calculate destination buffer size size_t dst_size = dst_pitch * src_height; void *dst_pixels = malloc(dst_size); if (!dst_pixels) { JS_FreeValue(js, src_pixels_val); return JS_ThrowOutOfMemory(js); } // Check if we have colorspace info for both source and dest bool has_src_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[0], "colorspace")); bool has_dst_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[1], "colorspace")); bool success; if (has_src_colorspace || has_dst_colorspace) { // Use SDL_ConvertPixelsAndColorspace success = SDL_ConvertPixelsAndColorspace( src_width, src_height, src_format, src_colorspace, 0, src_pixels, src_pitch, dst_format, dst_colorspace, 0, dst_pixels, dst_pitch ); } else { // Use SDL_ConvertPixels success = SDL_ConvertPixels( src_width, src_height, src_format, src_pixels, src_pitch, dst_format, dst_pixels, dst_pitch ); } JS_FreeValue(js, src_pixels_val); if (!success) { free(dst_pixels); return JS_ThrowInternalError(js, "Pixel conversion failed: %s", SDL_GetError()); } // Create result image object JSValue result = JS_NewObject(js); JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, src_width)); JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, src_height)); JS_SetPropertyStr(js, result, "format", pixelformat2js(js, dst_format)); JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, dst_pitch)); JSValue pixels = js_new_blob_stoned_copy(js, dst_pixels, dst_size); free(dst_pixels); JS_SetPropertyStr(js, result, "pixels", pixels); // Add depth and hdr for consistency JS_SetPropertyStr(js, result, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(dst_format))); JS_SetPropertyStr(js, result, "hdr", JS_FALSE); return result; ) CELL_USE_INIT( QJSCLASSPREP_FUNCS(SDL_Surface) // Add the surface constructor JSValue ctor = JS_NewCFunction2(js, js_surface_constructor, "surface", 1, JS_CFUNC_constructor, 0); JSValue proto = JS_GetClassProto(js, js_SDL_Surface_id); JS_SetConstructor(js, ctor, proto); JS_FreeValue(js, proto); // Add the generic convert function as a property on the constructor JS_SetPropertyStr(js, ctor, "convert", JS_NewCFunction(js, js_surface_convert_generic, "convert", 2)); // Add the compression functions as static methods on the constructor JS_SetPropertyStr(js, ctor, "compress_bc1", JS_NewCFunction(js, js_surface_compress_bc1, "compress_bc1", 2)); JS_SetPropertyStr(js, ctor, "compress_bc3", JS_NewCFunction(js, js_surface_compress_bc3, "compress_bc3", 2)); JS_SetPropertyStr(js, ctor, "compress_bc4", JS_NewCFunction(js, js_surface_compress_bc4, "compress_bc4", 1)); JS_SetPropertyStr(js, ctor, "compress_bc5", JS_NewCFunction(js, js_surface_compress_bc5, "compress_bc5", 1)); // Add standalone image manipulation functions JS_SetPropertyStr(js, ctor, "scale", JS_NewCFunction(js, js_surface_scale_img, "scale", 2)); JS_SetPropertyStr(js, ctor, "fill", JS_NewCFunction(js, js_surface_fill_img, "fill", 2)); JS_SetPropertyStr(js, ctor, "rect", JS_NewCFunction(js, js_surface_rect_img, "rect", 3)); JS_SetPropertyStr(js, ctor, "blit", JS_NewCFunction(js, js_surface_blit_img, "blit", 5)); JS_SetPropertyStr(js, ctor, "dup", JS_NewCFunction(js, js_surface_dup_img, "dup", 1)); return ctor; )