From 46ea9402424d58c5b637008292a5c379e05f9c7b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 2 Nov 2025 12:37:10 +0300 Subject: [PATCH] image: initial downsampling implementation Signed-off-by: NotAShelf Change-Id: Icec8c434ecf480c644a6f6e6a3b8cd5b6a6a6964 --- src/image.c | 154 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 147 insertions(+), 7 deletions(-) diff --git a/src/image.c b/src/image.c index 6a22991..caea5e2 100644 --- a/src/image.c +++ b/src/image.c @@ -24,8 +24,76 @@ static long get_file_size(const char *path) { return st.st_size; } -// Load image from file -int chroma_image_load(chroma_image_t *image, const char *path) { +// 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; @@ -77,15 +145,87 @@ int chroma_image_load(chroma_image_t *image, const char *path) { 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)", 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)); + (double)image_size / (1024.0 * 1024.0), + should_downsample ? " (downsampled)" : ""); return CHROMA_OK; } @@ -159,8 +299,8 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, state->image_count++; } - // Load the image - if (chroma_image_load(image, path) != CHROMA_OK) { + // 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--; @@ -241,7 +381,7 @@ void chroma_image_init_stb(void) { // Set stb_image options stbi_set_flip_vertically_on_load(0); - // These could be made configurable + // FIXME: these could be made configurable stbi_ldr_to_hdr_gamma(2.2f); stbi_ldr_to_hdr_scale(1.0f);