notashelf /
308c82f479ec29a9681e1702478bf2123fcaf0d5
chroma
publicLightweight wallpaper daemon for Wayland
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.gitCommit 308c82f479ec
tarballunverified3 files changed+828-5
@@ -71,7 +71,21 @@ typedef struct { char path[MAX_PATH_LEN]; bool loaded; bool data_from_stbi; // true if data was allocated by stbi_load- int ref_count; // Number of outputs using this image+ int ref_count; // number of outputs using this image++ // Solid color support+ bool is_solid_color; // true if this is a generated solid color+ uint32_t solid_color_rgba; // RGBA color value (native byte order)++ // Animation support (for animated GIFs)+ bool is_animated; // true if this image has multiple frames+ int frame_count; // number of frames+ int current_frame; // current frame index+ long long last_frame_ms; // timestamp of last frame advance+ unsigned char **frames; // array of frame data pointers+ int *frame_delays_ms; // delay per frame in milliseconds+ int *frame_widths; // width per frame+ int *frame_heights; // height per frame } chroma_image_t; // Wayland output information@@ -111,6 +125,15 @@ typedef struct { float anchor_y; // custom Y offset (0-100, 50=center) bool config_loaded; bool configured; // track if initial configure received++ // Transition support+ bool in_transition; // whether crossfade transition is active+ long long transition_start_ms; // when transition began+ unsigned char *transition_prev; // previous frame pixel data (RGBA)+ size_t transition_prev_size; // size of previous frame data+ int transition_prev_width; // width of previous frame+ int transition_prev_height; // height of previous frame+ int transition_duration_ms; // duration for this output's transition } chroma_output_t; // Config mapping structure@@ -143,6 +166,24 @@ typedef struct { int max_output_width; // maximum expected output width int max_output_height; // maximum expected output height float min_scale_factor; // minimum scale factor (don't scale below this)++ // Solid color background support+ // When set via config or IPC, generates a solid color instead of loading an+ // image If non-empty, overrides image loading (format: "rrggbb" or+ // "rrggbbaa")+ char solid_color[10];++ // Transition settings+ bool transition_enabled; // enable smooth crossfade transitions+ int transition_duration_ms; // crossfade duration in milliseconds++ // Animation settings+ bool animation_enabled; // enable animated GIF playback+ int animation_max_fps; // max frames per second for animations++ // IPC settings+ bool ipc_enabled; // enable IPC command socket+ char ipc_socket_path[MAX_PATH_LEN]; // path to Unix domain socket } chroma_config_t; // Main application state@@ -168,9 +209,14 @@ typedef struct chroma_state { // State flags bool running; bool initialized;-} chroma_state_t; -// Function declarations+ // IPC+ int ipc_fd; // IPC socket file descriptor+ char config_file[MAX_PATH_LEN]; // active config file path++ // Animation timing+ long long last_anim_ms; // timestamp of last animation frame advance+} chroma_state_t; // Initialization and cleanup int chroma_init(chroma_state_t *state);@@ -240,6 +286,42 @@ int chroma_image_get_info(const char *path, int *width, int *height, int *channels); void chroma_images_cleanup(chroma_state_t *state); +// Solid color image generation+int chroma_image_load_color(chroma_image_t *image, uint32_t color_rgba,+ int width, int height);++// Additional image format loaders+int chroma_image_load_farbfeld(chroma_image_t *image, const char *path);+int chroma_image_load_svg(chroma_image_t *image, const char *path,+ int out_width, int out_height);+int chroma_image_load_webp(chroma_image_t *image, const char *path);+int chroma_image_load_tiff(chroma_image_t *image, const char *path);+int chroma_image_load_avif(chroma_image_t *image, const char *path);+int chroma_image_load_jpegxl(chroma_image_t *image, const char *path);++// Animation functions+int chroma_anim_load_gif(chroma_image_t *image, const char *path);+void chroma_anim_advance_frame(chroma_image_t *image,+ long long current_time_ms);+void chroma_anim_free_frames(chroma_image_t *image);+bool chroma_anim_needs_redraw(chroma_state_t *state);+int chroma_anim_update_outputs(chroma_state_t *state);++// Transition functions+void chroma_transition_init_output(chroma_output_t *output, int duration_ms);+void chroma_transition_cleanup_output(chroma_output_t *output);+bool chroma_transition_is_active(chroma_output_t *output);+float chroma_transition_get_alpha(chroma_output_t *output,+ long long current_time_ms);+bool chroma_transition_needs_redraw(chroma_state_t *state);+int chroma_transition_update_outputs(chroma_state_t *state);++// IPC functions+int chroma_ipc_init(chroma_state_t *state);+void chroma_ipc_cleanup(chroma_state_t *state);+int chroma_ipc_process(chroma_state_t *state);+int chroma_ipc_get_fd(chroma_state_t *state);+ // Configuration int chroma_config_load(chroma_config_t *config, const char *config_file); int chroma_config_load_toml(chroma_config_t *config, const char *config_file);@@ -264,6 +346,17 @@ void chroma_get_stats(chroma_state_t *state, int *active_outputs, int *loaded_images); void handle_output_done(chroma_state_t *state, chroma_output_t *output); +// Runtime wallpaper control+int chroma_set_wallpaper_for_output(chroma_state_t *state,+ const char *output_name,+ const char *image_path);+int chroma_set_wallpaper_for_all(chroma_state_t *state, const char *image_path);+int chroma_set_solid_color_for_output(chroma_state_t *state,+ const char *output_name,+ const char *hex_color);+int chroma_set_solid_color_for_all(chroma_state_t *state,+ const char *hex_color);+ // Utilities void chroma_log(const char *level, const char *format, ...); const char *chroma_error_string(chroma_error_t error);@@ -96,6 +96,79 @@ static int downsample_image(unsigned char *src_data, int src_width, return 0; } +// Load a solid color image+int chroma_image_load_color(chroma_image_t *image, uint32_t color_rgba,+ int width, int height) {+ if (!image || width <= 0 || height <= 0) {+ return CHROMA_ERROR_INIT;+ }++ memset(image, 0, sizeof(chroma_image_t));+ snprintf(image->path, sizeof(image->path), "color:#%08X", color_rgba);++ size_t size = (size_t)width * (size_t)height * 4;+ image->data = malloc(size);+ if (!image->data) {+ return CHROMA_ERROR_MEMORY;+ }+ image->data_from_stbi = false;++ unsigned char r = (unsigned char)((color_rgba >> 16) & 0xFF);+ unsigned char g = (unsigned char)((color_rgba >> 8) & 0xFF);+ unsigned char b = (unsigned char)(color_rgba & 0xFF);+ unsigned char a = (unsigned char)((color_rgba >> 24) & 0xFF);++ for (size_t i = 0; i < size; i += 4) {+ image->data[i + 0] = r;+ image->data[i + 1] = g;+ image->data[i + 2] = b;+ image->data[i + 3] = a;+ }++ image->width = width;+ image->height = height;+ image->channels = 4;+ image->loaded = true;+ image->ref_count = 1;+ image->is_solid_color = true;+ image->solid_color_rgba = color_rgba;++ chroma_log("DEBUG", "Generated solid color image: %dx%d #%06X", width, height,+ color_rgba & 0xFFFFFF);+ return CHROMA_OK;+}++// Try loading with a different format loader based on file extension+static int load_with_alternate_loader(chroma_image_t *image, const char *path,+ int output_width, int output_height) {+ const char *ext = strrchr(path, '.');+ if (!ext) {+ return -1;+ }+ ext++; // skip dot++ if (strcasecmp(ext, "ff") == 0 || strcasecmp(ext, "farbfeld") == 0) {+ return chroma_image_load_farbfeld(image, path);+ }+ if (strcasecmp(ext, "svg") == 0) {+ return chroma_image_load_svg(image, path, output_width, output_height);+ }+ if (strcasecmp(ext, "webp") == 0) {+ return chroma_image_load_webp(image, path);+ }+ if (strcasecmp(ext, "tif") == 0 || strcasecmp(ext, "tiff") == 0) {+ return chroma_image_load_tiff(image, path);+ }+ if (strcasecmp(ext, "avif") == 0) {+ return chroma_image_load_avif(image, path);+ }+ if (strcasecmp(ext, "jxl") == 0 || strcasecmp(ext, "jpegxl") == 0) {+ return chroma_image_load_jpegxl(image, path);+ }++ return -1; // no alternate loader matched+}+ // Load image from file with configurable downsampling // output_width/output_height: actual output dimensions for intelligent // downsampling@@ -131,11 +204,37 @@ int chroma_image_load(chroma_image_t *image, const char *path, int actual_channels = 0; if (!stbi_info(path, &image->width, &image->height, &actual_channels)) {+ // stb_image doesn't support this format; try alternate loaders+ int alt_ret =+ load_with_alternate_loader(image, path, output_width, output_height);+ if (alt_ret == CHROMA_OK) {+ image->loaded = true;+ image->ref_count = 1;+ chroma_log("INFO", "Loaded image via alternate loader: %s", path);+ return CHROMA_OK;+ } chroma_log("ERROR", "Failed to get image info for %s: %s", path, stbi_failure_reason()); return CHROMA_ERROR_IMAGE; } + // Try animated GIF first for .gif files+ const char *ext_check = strrchr(path, '.');+ if (ext_check && strcasecmp(ext_check + 1, "gif") == 0) {+ int gif_ret = chroma_anim_load_gif(image, path);+ if (gif_ret == CHROMA_OK) {+ // Animated GIF loaded successfully+ image->loaded = true;+ chroma_log("INFO", "Loaded animated GIF: %s", path);+ return CHROMA_OK;+ }+ if (gif_ret == CHROMA_ERROR_MEMORY) {+ return CHROMA_ERROR_MEMORY;+ }+ // gif_ret == 1 means single-frame, fall through to stb_image loading+ // gif_ret == CHROMA_ERROR_IMAGE means parsing failed, also fall through+ }+ // Load with actual channels or force RGBA if image has alpha // For wallpapers, we typically don't need alpha unless the image has it int desired_channels = (actual_channels == 4 || actual_channels == 2) ? 4 : 3;@@ -262,6 +361,11 @@ void chroma_image_free(chroma_image_t *image) { return; } + // Free animated frames first+ if (image->is_animated) {+ chroma_anim_free_frames(image);+ }+ if (image->data) { // Log memory deallocation before freeing size_t image_size =@@ -273,7 +377,9 @@ void chroma_image_free(chroma_image_t *image) { chroma_log_resource_deallocation("image_data", image_size, image->path); - if (image->data_from_stbi) {+ if (image->is_animated) {+ // frames already freed above; data is just a pointer into frames array+ } else if (image->data_from_stbi) { stbi_image_free(image->data); } else { free(image->data);@@ -385,7 +491,11 @@ int chroma_image_validate(const char *path) { 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) {+ strcasecmp(ext, "pgm") == 0 || strcasecmp(ext, "ff") == 0 ||+ strcasecmp(ext, "farbfeld") == 0 || strcasecmp(ext, "svg") == 0 ||+ strcasecmp(ext, "webp") == 0 || strcasecmp(ext, "tif") == 0 ||+ strcasecmp(ext, "tiff") == 0 || strcasecmp(ext, "avif") == 0 ||+ strcasecmp(ext, "jxl") == 0 || strcasecmp(ext, "jpegxl") == 0) { return CHROMA_OK; } @@ -0,0 +1,620 @@+// Image format loaders for formats not supported by stb_image.+// Each loader converts its format to RGBA8 and fills a chroma_image_t.+// Loaders requiring optional libraries are guarded by #ifdef CHROMA_HAS_*;+// when absent the function returns CHROMA_ERROR_IMAGE with a build hint.++#include <stdio.h>+#include <stdlib.h>+#include <string.h>++#include "../include/chroma.h"++static uint32_t read_be32(const unsigned char *buf) {+ return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |+ ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];+}++static uint16_t read_be16(const unsigned char *buf) {+ return (uint16_t)(((uint16_t)buf[0] << 8) | (uint16_t)buf[1]);+}++// Farbfeld is a lossless image format: 16-byte magic + RGBA16 big-endian.+// Spec: https://tools.suckless.org/farbfeld/+int chroma_image_load_farbfeld(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open farbfeld file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ unsigned char header[16];+ if (fread(header, 1, 16, fp) != 16) {+ chroma_log("ERROR", "Failed to read farbfeld header: %s", path);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ if (memcmp(header, "farbfeld", 8) != 0) {+ chroma_log("ERROR", "Invalid farbfeld signature in: %s", path);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ int width = (int)read_be32(header + 8);+ int height = (int)read_be32(header + 12);++ if (width <= 0 || height <= 0 || width > 16384 || height > 16384) {+ chroma_log("ERROR", "Invalid farbfeld dimensions: %dx%d", width, height);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ size_t pixel_count = (size_t)width * (size_t)height;+ size_t src_size = pixel_count * 8;+ size_t dst_size = pixel_count * 4;++ unsigned char *src_data = malloc(src_size);+ if (!src_data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }++ if (fread(src_data, 1, src_size, fp) != src_size) {+ chroma_log("ERROR", "Truncated farbfeld data: %s", path);+ free(src_data);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ fclose(fp);++ unsigned char *dst_data = malloc(dst_size);+ if (!dst_data) {+ free(src_data);+ return CHROMA_ERROR_MEMORY;+ }++ for (size_t i = 0; i < pixel_count; i++) {+ size_t s = i * 8;+ size_t d = i * 4;+ dst_data[d + 0] = (unsigned char)(read_be16(src_data + s + 0) >> 8);+ dst_data[d + 1] = (unsigned char)(read_be16(src_data + s + 2) >> 8);+ dst_data[d + 2] = (unsigned char)(read_be16(src_data + s + 4) >> 8);+ dst_data[d + 3] = (unsigned char)(read_be16(src_data + s + 6) >> 8);+ }++ free(src_data);++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = dst_data;+ image->width = width;+ image->height = height;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded farbfeld image: %s (%dx%d)", path, width, height);+ return CHROMA_OK;+}++// SVG rasterization via nanosvg (header-only). Enable with+// -DCHROMA_HAS_NANOSVG. Renders at output resolution if provided, otherwise+// uses the SVG viewport size.+#ifdef CHROMA_HAS_NANOSVG++#define NANOSVG_IMPLEMENTATION+#define NANOSVGRAST_IMPLEMENTATION+#include "../include/vendor/nanosvg.h"+#include "../include/vendor/nanosvgrast.h"++int chroma_image_load_svg(chroma_image_t *image, const char *path,+ int out_width, int out_height) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open SVG file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ fseek(fp, 0, SEEK_END);+ long size = ftell(fp);+ fseek(fp, 0, SEEK_SET);++ char *svg_data = malloc((size_t)size + 1);+ if (!svg_data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }+ if (fread(svg_data, 1, (size_t)size, fp) != (size_t)size) {+ chroma_log("ERROR", "Failed to read SVG file: %s", path);+ free(svg_data);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ svg_data[size] = '\0';+ fclose(fp);++ struct NSVGimage *nsvg = nsvgParse(svg_data, "px", 96);+ free(svg_data);++ if (!nsvg) {+ chroma_log("ERROR", "Failed to parse SVG: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ int w = out_width > 0 ? out_width : (int)nsvg->width;+ int h = out_height > 0 ? out_height : (int)nsvg->height;+ if (w <= 0)+ w = 800;+ if (h <= 0)+ h = 600;++ struct NSVGrasterizer *rast = nsvgCreateRasterizer();+ if (!rast) {+ nsvgDelete(nsvg);+ return CHROMA_ERROR_MEMORY;+ }++ unsigned char *data = malloc((size_t)w * (size_t)h * 4);+ if (!data) {+ nsvgDeleteRasterizer(rast);+ nsvgDelete(nsvg);+ return CHROMA_ERROR_MEMORY;+ }++ float scale_x = (float)w / nsvg->width;+ float scale_y = (float)h / nsvg->height;+ float scale = (scale_x < scale_y) ? scale_x : scale_y;+ nsvgRasterize(rast, nsvg, 0, 0, scale, data, w, h, w * 4);++ nsvgDeleteRasterizer(rast);+ nsvgDelete(nsvg);++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = data;+ image->width = w;+ image->height = h;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded SVG image: %s (%dx%d)", path, w, h);+ return CHROMA_OK;+}++#else++int chroma_image_load_svg(chroma_image_t *image, const char *path,+ int out_width, int out_height) {+ (void)image;+ (void)path;+ (void)out_width;+ (void)out_height;+ chroma_log("ERROR", "SVG support requires nanosvg. Rebuild with: "+ "make CHROMA_FEATURES=svg");+ return CHROMA_ERROR_IMAGE;+}++#endif++// WebP decoding via libwebp. Enable with -DCHROMA_HAS_WEBP -lwebp.+#ifdef CHROMA_HAS_WEBP++#include <webp/decode.h>++int chroma_image_load_webp(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open WebP file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ fseek(fp, 0, SEEK_END);+ long size = ftell(fp);+ fseek(fp, 0, SEEK_SET);++ if (size <= 0 || size > 256 * 1024 * 1024) {+ chroma_log("ERROR", "WebP file too large: %s", path);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ unsigned char *webp_data = malloc((size_t)size);+ if (!webp_data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }+ if (fread(webp_data, 1, (size_t)size, fp) != (size_t)size) {+ chroma_log("ERROR", "Failed to read WebP file: %s", path);+ free(webp_data);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ fclose(fp);++ int w, h;+ if (!WebPGetInfo(webp_data, (size_t)size, &w, &h)) {+ chroma_log("ERROR", "Invalid WebP data: %s", path);+ free(webp_data);+ return CHROMA_ERROR_IMAGE;+ }++ unsigned char *data = WebPDecodeRGBA(webp_data, (size_t)size, &w, &h);+ free(webp_data);++ if (!data) {+ chroma_log("ERROR", "Failed to decode WebP: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = data;+ image->width = w;+ image->height = h;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded WebP image: %s (%dx%d)", path, w, h);+ return CHROMA_OK;+}++#else++int chroma_image_load_webp(chroma_image_t *image, const char *path) {+ (void)image;+ (void)path;+ chroma_log("ERROR", "WebP support requires libwebp. Rebuild with: "+ "make CHROMA_FEATURES=webp");+ return CHROMA_ERROR_IMAGE;+}++#endif++// TIFF decoding via libtiff. Enable with -DCHROMA_HAS_TIFF -ltiff.+#ifdef CHROMA_HAS_TIFF++#include <tiffio.h>++int chroma_image_load_tiff(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ TIFF *tif = TIFFOpen(path, "r");+ if (!tif) {+ chroma_log("ERROR", "Cannot open TIFF file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ uint32_t w, h;+ TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w);+ TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h);++ if (w == 0 || h == 0 || w > 32768 || h > 32768) {+ chroma_log("ERROR", "Invalid TIFF dimensions: %ux%u", w, h);+ TIFFClose(tif);+ return CHROMA_ERROR_IMAGE;+ }++ size_t pixel_count = (size_t)w * (size_t)h;+ size_t buf_size = pixel_count * 4;+ unsigned char *data = malloc(buf_size);+ if (!data) {+ TIFFClose(tif);+ return CHROMA_ERROR_MEMORY;+ }++ if (!TIFFReadRGBAImageOriented(tif, w, h, (uint32_t *)data,+ ORIENTATION_TOPLEFT, 0)) {+ chroma_log("ERROR", "Failed to decode TIFF: %s", path);+ free(data);+ TIFFClose(tif);+ return CHROMA_ERROR_IMAGE;+ }++ TIFFClose(tif);++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = data;+ image->width = (int)w;+ image->height = (int)h;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded TIFF image: %s (%dx%d)", path, (int)w, (int)h);+ return CHROMA_OK;+}++#else++int chroma_image_load_tiff(chroma_image_t *image, const char *path) {+ (void)image;+ (void)path;+ chroma_log("ERROR", "TIFF support requires libtiff. Rebuild with: "+ "make CHROMA_FEATURES=tiff");+ return CHROMA_ERROR_IMAGE;+}++#endif++// AVIF decoding via libavif. Enable with -DCHROMA_HAS_AVIF -lavif.+#ifdef CHROMA_HAS_AVIF++#include <avif/avif.h>++int chroma_image_load_avif(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open AVIF file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ fseek(fp, 0, SEEK_END);+ long size = ftell(fp);+ fseek(fp, 0, SEEK_SET);++ if (size <= 0 || size > 256 * 1024 * 1024) {+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ avifRWData raw = AVIF_DATA_EMPTY;+ avifRWDataRealloc(&raw, (size_t)size);+ if (!raw.data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }+ if (fread(raw.data, 1, (size_t)size, fp) != (size_t)size) {+ chroma_log("ERROR", "Failed to read AVIF file: %s", path);+ avifRWDataFree(&raw);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ fclose(fp);++ avifDecoder *decoder = avifDecoderCreate();+ avifResult result = avifDecoderSetIOMemory(decoder, raw.data, raw.size);+ if (result != AVIF_RESULT_OK) {+ chroma_log("ERROR", "Failed to set AVIF IO: %s", path);+ avifDecoderDestroy(decoder);+ avifRWDataFree(&raw);+ return CHROMA_ERROR_IMAGE;+ }++ result = avifDecoderParse(decoder);+ if (result != AVIF_RESULT_OK) {+ chroma_log("ERROR", "Failed to parse AVIF: %s", path);+ avifDecoderDestroy(decoder);+ avifRWDataFree(&raw);+ return CHROMA_ERROR_IMAGE;+ }++ result = avifDecoderNextImage(decoder);+ if (result != AVIF_RESULT_OK) {+ chroma_log("ERROR", "Failed to decode AVIF: %s", path);+ avifDecoderDestroy(decoder);+ avifRWDataFree(&raw);+ return CHROMA_ERROR_IMAGE;+ }++ int w = (int)decoder->image->width;+ int h = (int)decoder->image->height;++ avifRGBImage rgb;+ avifRGBImageSetDefaults(&rgb, decoder->image);+ rgb.format = AVIF_RGB_FORMAT_RGBA;+ rgb.depth = 8;++ avifRGBImageAllocatePixels(&rgb);+ avifImageYUVToRGB(decoder->image, &rgb);++ unsigned char *data = malloc(rgb.rowBytes * (size_t)h);+ if (!data) {+ avifRGBImageFreePixels(&rgb);+ avifDecoderDestroy(decoder);+ avifRWDataFree(&raw);+ return CHROMA_ERROR_MEMORY;+ }+ memcpy(data, rgb.pixels, rgb.rowBytes * (size_t)h);++ avifRGBImageFreePixels(&rgb);+ avifDecoderDestroy(decoder);+ avifRWDataFree(&raw);++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = data;+ image->width = w;+ image->height = h;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded AVIF image: %s (%dx%d)", path, w, h);+ return CHROMA_OK;+}++#else++int chroma_image_load_avif(chroma_image_t *image, const char *path) {+ (void)image;+ (void)path;+ chroma_log("ERROR", "AVIF support requires libavif. Rebuild with: "+ "make CHROMA_FEATURES=avif");+ return CHROMA_ERROR_IMAGE;+}++#endif++// JPEG XL decoding via libjxl. Enable with -DCHROMA_HAS_JPEGXL -ljxl.+#ifdef CHROMA_HAS_JPEGXL++#include <jxl/decode.h>+#include <jxl/decode_cxx.h>++int chroma_image_load_jpegxl(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open JPEG XL file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ fseek(fp, 0, SEEK_END);+ long size = ftell(fp);+ fseek(fp, 0, SEEK_SET);++ if (size <= 0 || size > 256 * 1024 * 1024) {+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ unsigned char *jxl_data = malloc((size_t)size);+ if (!jxl_data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }+ if (fread(jxl_data, 1, (size_t)size, fp) != (size_t)size) {+ chroma_log("ERROR", "Failed to read JPEG XL file: %s", path);+ free(jxl_data);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ fclose(fp);++ JxlDecoder *dec = JxlDecoderCreate(NULL);+ if (!dec) {+ free(jxl_data);+ return CHROMA_ERROR_MEMORY;+ }++ JxlDecoderStatus status = JxlDecoderSetInput(dec, jxl_data, (size_t)size);+ if (status != JXL_DEC_SUCCESS) {+ chroma_log("ERROR", "Failed to set JPEG XL input: %s", path);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ status =+ JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE);+ if (status != JXL_DEC_SUCCESS) {+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ JxlBasicInfo info;+ memset(&info, 0, sizeof(info));+ status = JxlDecoderProcessInput(dec);+ if (status != JXL_DEC_BASIC_INFO) {+ chroma_log("ERROR", "Failed to get JPEG XL basic info: %s", path);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ status = JxlDecoderGetBasicInfo(dec, &info);+ if (status != JXL_DEC_SUCCESS) {+ chroma_log("ERROR", "Failed to parse JPEG XL info: %s", path);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ int w = (int)info.xsize;+ int h = (int)info.ysize;++ size_t buffer_size = (size_t)w * (size_t)h * 4;+ unsigned char *data = malloc(buffer_size);+ if (!data) {+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_MEMORY;+ }++ JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};++ status = JxlDecoderProcessInput(dec);+ if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {+ chroma_log("ERROR", "Unexpected JPEG XL decoder state: %s", path);+ free(data);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ status = JxlDecoderSetImageOutBuffer(dec, &format, data, buffer_size);+ if (status != JXL_DEC_SUCCESS) {+ chroma_log("ERROR", "Failed to set JPEG XL output buffer: %s", path);+ free(data);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ status = JxlDecoderProcessInput(dec);+ if (status != JXL_DEC_FULL_IMAGE) {+ chroma_log("ERROR", "Failed to decode JPEG XL image: %s", path);+ free(data);+ JxlDecoderDestroy(dec);+ free(jxl_data);+ return CHROMA_ERROR_IMAGE;+ }++ JxlDecoderDestroy(dec);+ free(jxl_data);++ memset(image, 0, sizeof(chroma_image_t));+ strncpy(image->path, path, MAX_PATH_LEN - 1);+ image->data = data;+ image->width = w;+ image->height = h;+ image->channels = 4;+ image->loaded = true;+ image->data_from_stbi = false;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded JPEG XL image: %s (%dx%d)", path, w, h);+ return CHROMA_OK;+}++#else++int chroma_image_load_jpegxl(chroma_image_t *image, const char *path) {+ (void)image;+ (void)path;+ chroma_log("ERROR", "JPEG XL support requires libjxl. Rebuild with: "+ "make CHROMA_FEATURES=jpegxl");+ return CHROMA_ERROR_IMAGE;+}++#endif