#define STB_IMAGE_IMPLEMENTATION #include "../include/stb_image.h" #include #include #include #include #include #include "../include/chroma.h" // Check if file exists and is readable static int file_exists(const char *path) { struct stat st; return (stat(path, &st) == 0 && S_ISREG(st.st_mode)); } // Get file size static long get_file_size(const char *path) { struct stat st; if (stat(path, &st) != 0) { return -1; } return st.st_size; } // Calculate optimal image size based on output dimensions static void calculate_optimal_size(int original_width, int original_height, int max_output_width, int max_output_height, int *optimal_width, int *optimal_height) { // If image is smaller than outputs, keep original size if (original_width <= max_output_width && original_height <= max_output_height) { *optimal_width = original_width; *optimal_height = original_height; return; } // Calculate scale factor to fit within max output dimensions float scale_x = (float)max_output_width / original_width; float scale_y = (float)max_output_height / original_height; float scale = (scale_x < scale_y) ? scale_x : scale_y; // Apply scale factor with minimum size to avoid too small images scale = (scale > 1.0f) ? 1.0f : scale; scale = (scale < 0.25f) ? 0.25f : scale; // XXX: don't scale below 25% *optimal_width = (int)(original_width * scale); *optimal_height = (int)(original_height * scale); // Ensure even dimensions for better GPU alignment *optimal_width = (*optimal_width / 2) * 2; *optimal_height = (*optimal_height / 2) * 2; } // FIXME: this is a very simple way of implementing box filter downsampling for // memory efficiency Could be better, but this is good enough *for the time // being*. Must be revisited in the future to see how it stands as the program // evolves. static int downsample_image(unsigned char *src_data, int src_width, int src_height, unsigned char *dst_data, int dst_width, int dst_height) { if (!src_data || !dst_data) { return -1; } float x_ratio = (float)src_width / dst_width; float y_ratio = (float)src_height / dst_height; for (int y = 0; y < dst_height; y++) { for (int x = 0; x < dst_width; x++) { // Calculate corresponding source pixel int src_x = (int)(x * x_ratio); int src_y = (int)(y * y_ratio); // Ensure we're within bounds src_x = (src_x >= src_width) ? src_width - 1 : src_x; src_y = (src_y >= src_height) ? src_height - 1 : src_y; // Copy pixel (RGBA) int src_idx = (src_y * src_width + src_x) * 4; int dst_idx = (y * dst_width + x) * 4; dst_data[dst_idx + 0] = src_data[src_idx + 0]; // R dst_data[dst_idx + 1] = src_data[src_idx + 1]; // G dst_data[dst_idx + 2] = src_data[src_idx + 2]; // B dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A } } return 0; } // Load image from file with configurable downsampling int chroma_image_load(chroma_image_t *image, const char *path, const chroma_config_t *config) { if (!image || !path) { chroma_log("ERROR", "Invalid parameters for image loading"); return CHROMA_ERROR_INIT; } // Initialize image structure memset(image, 0, sizeof(chroma_image_t)); strncpy(image->path, path, MAX_PATH_LEN - 1); image->path[MAX_PATH_LEN - 1] = '\0'; // Check if file exists if (!file_exists(path)) { chroma_log("ERROR", "Image file does not exist: %s", path); return CHROMA_ERROR_IMAGE; } // Get file size for logging long file_size = get_file_size(path); if (file_size > 0) { chroma_log("DEBUG", "Loading image: %s (%.2f MB)", path, (double)file_size / (1024.0 * 1024.0)); } // Load image data using stb_image, force RGBA format to avoid conversion stbi_set_flip_vertically_on_load(0); // keep images right-side up image->data = stbi_load(path, &image->width, &image->height, &image->channels, 4); image->channels = 4; // always RGBA after forced conversion if (!image->data) { chroma_log("ERROR", "Failed to load image %s: %s", path, stbi_failure_reason()); return CHROMA_ERROR_IMAGE; } // Validate image dimensions if (image->width <= 0 || image->height <= 0) { chroma_log("ERROR", "Invalid image dimensions: %dx%d", image->width, image->height); chroma_image_free(image); return CHROMA_ERROR_IMAGE; } // Validate we have RGBA data (should always be true with forced conversion) if (image->channels != 4) { chroma_log("ERROR", "Failed to load image as RGBA: got %d channels", image->channels); chroma_image_free(image); return CHROMA_ERROR_IMAGE; } // Store original dimensions before potential downsampling int original_width = image->width; int original_height = image->height; // Apply intelligent downsampling if enabled bool should_downsample = false; int optimal_width = original_width; int optimal_height = original_height; if (config && config->enable_downsampling) { calculate_optimal_size(original_width, original_height, config->max_output_width, config->max_output_height, &optimal_width, &optimal_height); // Apply minimum scale factor constraint float scale_x = (float)optimal_width / original_width; float scale_y = (float)optimal_height / original_height; float scale = (scale_x < scale_y) ? scale_x : scale_y; if (scale < config->min_scale_factor) { scale = config->min_scale_factor; optimal_width = (int)(original_width * scale); optimal_height = (int)(original_height * scale); // Ensure even dimensions optimal_width = (optimal_width / 2) * 2; optimal_height = (optimal_height / 2) * 2; } should_downsample = (optimal_width < original_width || optimal_height < original_height); } // Downsamp if needed and enabled if (should_downsample) { chroma_log("INFO", "Downsampling image from %dx%d to %dx%d (%.1f%% of original)", original_width, original_height, optimal_width, optimal_height, (float)(optimal_width * optimal_height) / (original_width * original_height) * 100.0f); size_t optimal_size = (size_t)optimal_width * optimal_height * 4; unsigned char *downsampled_data = malloc(optimal_size); if (!downsampled_data) { chroma_log("ERROR", "Failed to allocate memory for downsampled image"); chroma_image_free(image); return CHROMA_ERROR_MEMORY; } if (downsample_image(image->data, original_width, original_height, downsampled_data, optimal_width, optimal_height) != 0) { chroma_log("ERROR", "Failed to downsample image"); free(downsampled_data); chroma_image_free(image); return CHROMA_ERROR_IMAGE; } stbi_image_free(image->data); image->data = downsampled_data; image->width = optimal_width; image->height = optimal_height; chroma_log("DEBUG", "Successfully downsampled image to %dx%d", optimal_width, optimal_height); } else if (config && !config->enable_downsampling) { chroma_log("DEBUG", "Downsampling disabled, keeping original resolution %dx%d", original_width, original_height); } image->loaded = true; // Calculate and log memory allocation size_t image_size = (size_t)image->width * image->height * image->channels; chroma_log_resource_allocation("image_data", image_size, path); chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)%s", path, image->width, image->height, image->channels, (double)image_size / (1024.0 * 1024.0), should_downsample ? " (downsampled)" : ""); return CHROMA_OK; } // Free image data void chroma_image_free(chroma_image_t *image) { if (!image) { return; } if (image->data) { // Log memory deallocation before freeing size_t image_size = (size_t)image->width * image->height * image->channels; chroma_log_resource_deallocation("image_data", image_size, image->path); // Always use stbi_image_free since we load directly with stbi_load stbi_image_free(image->data); image->data = NULL; } image->width = 0; image->height = 0; image->channels = 0; image->loaded = false; if (strlen(image->path) > 0) { chroma_log("DEBUG", "Freed image: %s", image->path); } memset(image->path, 0, sizeof(image->path)); } // Find image by path in state chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, const char *path) { if (!state || !path) { return NULL; } for (int i = 0; i < state->image_count; i++) { if (strcmp(state->images[i].path, path) == 0) { return &state->images[i]; } } return NULL; } // Load image if not already loaded chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, const char *path) { if (!state || !path) { return NULL; } // Check if already loaded chroma_image_t *existing = chroma_image_find_by_path(state, path); if (existing && existing->loaded) { chroma_log("DEBUG", "Using cached image: %s", path); return existing; } // Find empty slot or reuse existing chroma_image_t *image = existing; if (!image) { if (state->image_count >= MAX_OUTPUTS) { chroma_log("ERROR", "Maximum number of images reached"); return NULL; } image = &state->images[state->image_count]; state->image_count++; } // Load the image with configuration if (chroma_image_load(image, path, &state->config) != CHROMA_OK) { // If this was a new slot, decrement count if (!existing) { state->image_count--; } return NULL; } return image; } // Validate image file format int chroma_image_validate(const char *path) { if (!path || !file_exists(path)) { return CHROMA_ERROR_IMAGE; } // Check file extension (basic validation) const char *ext = strrchr(path, '.'); if (!ext) { return CHROMA_ERROR_IMAGE; } ext++; // Skip the dot // Check supported extensions if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 || strcasecmp(ext, "png") == 0 || strcasecmp(ext, "bmp") == 0 || strcasecmp(ext, "tga") == 0 || strcasecmp(ext, "psd") == 0 || strcasecmp(ext, "gif") == 0 || strcasecmp(ext, "hdr") == 0 || strcasecmp(ext, "pic") == 0 || strcasecmp(ext, "ppm") == 0 || strcasecmp(ext, "pgm") == 0) { return CHROMA_OK; } chroma_log("WARN", "Potentially unsupported image format: %s", ext); return CHROMA_ERROR_IMAGE; } // Get image info without loading full data int chroma_image_get_info(const char *path, int *width, int *height, int *channels) { if (!path || !width || !height || !channels) { return CHROMA_ERROR_INIT; } if (!file_exists(path)) { return CHROMA_ERROR_IMAGE; } if (!stbi_info(path, width, height, channels)) { chroma_log("ERROR", "Failed to get image info for %s: %s", path, stbi_failure_reason()); return CHROMA_ERROR_IMAGE; } return CHROMA_OK; } // Cleanup all images in state void chroma_images_cleanup(chroma_state_t *state) { if (!state) { return; } chroma_log("DEBUG", "Cleaning up %d images", state->image_count); for (int i = 0; i < state->image_count; i++) { chroma_image_free(&state->images[i]); } state->image_count = 0; chroma_log("INFO", "Cleaned up all images"); chroma_log_memory_stats("post-image-cleanup"); } // Preload common image formats for validation void chroma_image_init_stb(void) { // Set stb_image options stbi_set_flip_vertically_on_load(0); // FIXME: these could be made configurable stbi_ldr_to_hdr_gamma(2.2f); stbi_ldr_to_hdr_scale(1.0f); chroma_log("DEBUG", "Initialized stb_image library"); }