brewery
notashelf /
308c82f479ec29a9681e1702478bf2123fcaf0d5

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit 308c82f479ec

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-19 16:54 UTC

unverified3 files changed+828-5
image: add solid color and alternate format loaders

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifbd4fbb2efc6bb0830d7fbe20e058a856a6a6964
diff --git a/include/chroma.h b/include/chroma.hindex 843c2d1..9c1c9ef 100644--- a/include/chroma.h+++ b/include/chroma.h@@ -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);diff --git a/src/image.c b/src/image.cindex a424a7e..bafcf0e 100644--- a/src/image.c+++ b/src/image.c@@ -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;   } diff --git a/src/image_loaders.c b/src/image_loaders.cnew file mode 100644index 0000000..54019c4--- /dev/null+++ b/src/image_loaders.c@@ -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