notashelf /
0732ddd427d053e70468db8147f14e4a2eba127d
chroma
publicLightweight wallpaper daemon for Wayland
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.gitCommit 0732ddd427d0
tarballunverified3 files changed+522-7
@@ -0,0 +1,363 @@+#include <stdio.h>+#include <stdlib.h>+#include <string.h>++#include "../include/chroma.h"+#include "../include/vendor/stb_image.h"++// GIF block types+#define GIF_EXT_INTRODUCER 0x21+#define GIF_GRAPHIC_CONTROL 0xF9+#define GIF_IMAGE_SEPARATOR 0x2C+#define GIF_TRAILER 0x3B++// Read a little-endian uint16 from a buffer+static uint16_t read_le16(const unsigned char *buf) {+ return (uint16_t)(buf[0] | (buf[1] << 8));+}++// Parse a GIF file to extract frame count and timing+static int gif_parse_header(const unsigned char *data, size_t size, int *width,+ int *height, int *frame_count, int **frame_delays) {+ if (size < 13)+ return -1;++ // Verify GIF87a or GIF89a signature+ if (memcmp(data, "GIF87a", 6) != 0 && memcmp(data, "GIF89a", 6) != 0) {+ return -1;+ }++ *width = (int)read_le16(data + 6);+ *height = (int)read_le16(data + 8);++ // Skip logical screen descriptor fields+ size_t pos = 10;+ unsigned char packed = data[pos++];+ int has_gct = (packed & 0x80) >> 7;+ int bpp = (packed & 0x07) + 1;+ int gct_size = has_gct ? (3 << bpp) : 0;++ pos += 1; // background color index+ pos += 1; // pixel aspect ratio+ pos += (size_t)gct_size;++ // Count frames and parse frame delays+ int count = 0;+ int *delays = NULL;++ if (frame_count && frame_delays) {+ delays = malloc(1024 * sizeof(int));+ if (!delays)+ return -1;+ }++ while (pos < size) {+ unsigned char block_type = data[pos];++ if (block_type == GIF_TRAILER) {+ break;+ }++ if (block_type == GIF_EXT_INTRODUCER && pos + 1 < size) {+ unsigned char ext_type = data[pos + 1];+ pos += 2;++ if (ext_type == GIF_GRAPHIC_CONTROL && pos + 7 < size) {+ unsigned char block_size = data[pos];+ pos += 1 + (size_t)block_size + 1;++ if (delays && count < 1024) {+ // GCE block: [size:1][flags:1][delay:2][transparent:1][terminator:1]+ // pos has moved past the block, so data[pos - block_size] is the+ // start+ uint16_t delay_cs = read_le16(data + pos - block_size);+ delays[count] = (int)delay_cs * 10;+ }+ } else {+ // Skip other extensions+ while (pos < size && data[pos] != 0x00) {+ unsigned char sub_size = data[pos];+ pos += 1 + sub_size;+ }+ if (pos < size && data[pos] == 0x00) {+ pos++;+ }+ }+ } else if (block_type == GIF_IMAGE_SEPARATOR) {+ count++;+ pos += 10; // skip image descriptor header++ // Skip local color table if present+ unsigned char img_packed = data[pos - 1];+ int has_lct = (img_packed & 0x80) >> 7;+ int img_bpp = (img_packed & 0x07) + 1;+ int lct_size = has_lct ? (3 << img_bpp) : 0;+ pos += (size_t)lct_size;++ // Skip LZW minimum code size+ pos += 1;++ // Skip image data sub-blocks+ while (pos < size && data[pos] != 0x00) {+ unsigned char sub_size = data[pos];+ pos += 1 + sub_size;+ }+ if (pos < size && data[pos] == 0x00) {+ pos++;+ }+ } else {+ pos++;+ }+ }++ if (delays) {+ *frame_count = count;+ *frame_delays = delays;+ }++ return 0;+}++// Load an animated GIF into chroma_image_t+// Uses stb_image to decode individual frames+int chroma_anim_load_gif(chroma_image_t *image, const char *path) {+ if (!image || !path) {+ return CHROMA_ERROR_INIT;+ }++ // Read the entire GIF file into memory+ FILE *fp = fopen(path, "rb");+ if (!fp) {+ chroma_log("ERROR", "Cannot open GIF file: %s", path);+ return CHROMA_ERROR_IMAGE;+ }++ fseek(fp, 0, SEEK_END);+ long file_size = ftell(fp);+ fseek(fp, 0, SEEK_SET);++ if (file_size <= 0 || file_size > 100 * 1024 * 1024) {+ chroma_log("ERROR", "GIF file too large or invalid: %s", path);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }++ unsigned char *gif_data = malloc((size_t)file_size);+ if (!gif_data) {+ fclose(fp);+ return CHROMA_ERROR_MEMORY;+ }++ if (fread(gif_data, 1, (size_t)file_size, fp) != (size_t)file_size) {+ chroma_log("ERROR", "Failed to read GIF file: %s", path);+ free(gif_data);+ fclose(fp);+ return CHROMA_ERROR_IMAGE;+ }+ fclose(fp);++ int gif_width, gif_height, frame_count;+ int *frame_delays = NULL;++ if (gif_parse_header(gif_data, (size_t)file_size, &gif_width, &gif_height,+ &frame_count, &frame_delays) != 0) {+ chroma_log("ERROR", "Failed to parse GIF header: %s", path);+ free(gif_data);+ return CHROMA_ERROR_IMAGE;+ }++ if (frame_count < 2) {+ // Single-frame GIF - use stb_image to load as regular image+ free(gif_data);+ free(frame_delays);+ return 1; // signal to caller: not animated+ }++ chroma_log("DEBUG", "Animated GIF: %dx%d, %d frames", gif_width, gif_height,+ frame_count);++ // Load frames using stb_image+ int *delays_stbi = NULL;+ int frame_w, frame_h, comp;+ unsigned char *frames_data =+ stbi_load_gif_from_memory(gif_data, (int)file_size, &delays_stbi,+ &frame_w, &frame_h, &frame_count, &comp, 4);++ free(gif_data);++ if (!frames_data || frame_count < 2) {+ chroma_log("ERROR", "Failed to decode GIF frames: %s", path);+ if (frames_data)+ stbi_image_free(frames_data);+ free(frame_delays);+ if (delays_stbi)+ stbi_image_free(delays_stbi);+ return CHROMA_ERROR_IMAGE;+ }++ image->is_animated = true;+ image->frame_count = frame_count;+ image->current_frame = 0;+ image->width = frame_w;+ image->height = frame_h;+ image->channels = 4;++ image->frames = malloc((size_t)frame_count * sizeof(unsigned char *));+ image->frame_delays_ms = malloc((size_t)frame_count * sizeof(int));+ image->frame_widths = malloc((size_t)frame_count * sizeof(int));+ image->frame_heights = malloc((size_t)frame_count * sizeof(int));++ if (!image->frames || !image->frame_delays_ms || !image->frame_widths ||+ !image->frame_heights) {+ free(image->frames);+ free(image->frame_delays_ms);+ free(image->frame_widths);+ free(image->frame_heights);+ stbi_image_free(frames_data);+ free(frame_delays);+ if (delays_stbi)+ stbi_image_free(delays_stbi);+ return CHROMA_ERROR_MEMORY;+ }++ size_t frame_size = (size_t)frame_w * (size_t)frame_h * 4;++ for (int i = 0; i < frame_count; i++) {+ image->frames[i] = malloc(frame_size);+ if (!image->frames[i]) {+ for (int j = 0; j < i; j++)+ free(image->frames[j]);+ free(image->frames);+ free(image->frame_delays_ms);+ free(image->frame_widths);+ free(image->frame_heights);+ free(frames_data);+ free(frame_delays);+ if (delays_stbi)+ stbi_image_free(delays_stbi);+ return CHROMA_ERROR_MEMORY;+ }+ memcpy(image->frames[i], frames_data + (size_t)i * frame_size, frame_size);+ image->frame_delays_ms[i] =+ delays_stbi ? delays_stbi[i] : (frame_delays ? frame_delays[i] : 100);+ image->frame_widths[i] = frame_w;+ image->frame_heights[i] = frame_h;+ }++ stbi_image_free(frames_data);+ free(frame_delays);+ if (delays_stbi)+ stbi_image_free(delays_stbi);++ // Set initial data to first frame+ image->data = image->frames[0];+ image->loaded = true;+ image->ref_count = 1;++ chroma_log("INFO", "Loaded animated GIF: %s (%d frames, %dx%d)", path,+ frame_count, frame_w, frame_h);++ return CHROMA_OK;+}++// Advance to the next frame if enough time has passed+void chroma_anim_advance_frame(chroma_image_t *image,+ long long current_time_ms) {+ if (!image || !image->is_animated || image->frame_count < 2) {+ return;+ }++ int delay = image->frame_delays_ms[image->current_frame];+ if (delay < 16)+ delay = 100;++ if (image->last_frame_ms == 0) {+ image->last_frame_ms = current_time_ms;+ return;+ }++ long long elapsed = current_time_ms - image->last_frame_ms;+ if (elapsed < (long long)delay) {+ return;+ }++ image->current_frame = (image->current_frame + 1) % image->frame_count;+ image->data = image->frames[image->current_frame];+ image->last_frame_ms = current_time_ms;+}++// Free animated frame data+void chroma_anim_free_frames(chroma_image_t *image) {+ if (!image || !image->is_animated) {+ return;+ }++ for (int i = 0; i < image->frame_count; i++) {+ free(image->frames[i]);+ }+ free(image->frames);+ free(image->frame_delays_ms);+ free(image->frame_widths);+ free(image->frame_heights);++ image->frames = NULL;+ image->frame_delays_ms = NULL;+ image->frame_widths = NULL;+ image->frame_heights = NULL;+ image->frame_count = 0;+ image->is_animated = false;+}++// Check if any output has an animated image that needs frame advance+bool chroma_anim_needs_redraw(chroma_state_t *state) {+ if (!state || !state->config.animation_enabled) {+ return false;+ }++ for (int i = 0; i < state->output_count; i++) {+ chroma_output_t *output = &state->outputs[i];+ if (output->active && output->image && output->image->is_animated) {+ return true;+ }+ }++ return false;+}++// Advance animation frames and re-render+int chroma_anim_update_outputs(chroma_state_t *state) {+ if (!state || !state->config.animation_enabled) {+ return CHROMA_OK;+ }++ long long now = chroma_get_time_ms();+ int updated = 0;++ for (int i = 0; i < state->output_count; i++) {+ chroma_output_t *output = &state->outputs[i];+ if (!output->active || !output->image || !output->image->is_animated) {+ continue;+ }++ if (!output->surface) {+ continue;+ }++ int old_frame = output->image->current_frame;+ chroma_anim_advance_frame(output->image, now);++ if (output->image->current_frame != old_frame) {+ int ret = chroma_render_wallpaper(state, output);+ if (ret == CHROMA_OK) {+ updated++;+ }+ }+ }++ if (updated > 0) {+ chroma_log("DEBUG", "Animation frames advanced: %d outputs", updated);+ }++ state->last_anim_ms = now;+ return CHROMA_OK;+}@@ -161,6 +161,7 @@ static inline uint32_t sample_pixel(const chroma_image_t *image, int x, int y) { } // Draw scaled image into the shared memory buffer+// When a transition is active, blends between prev buffer and new image static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) { if (!output || !image || !output->shm_data) { return;@@ -172,6 +173,17 @@ static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) { int img_h = image->height; uint32_t *dst = (uint32_t *)output->shm_data; + // Save current buffer as previous frame for transition+ bool has_prev = false;+ unsigned char *prev_copy = NULL;+ if (output->in_transition && output->shm_size > 0) {+ prev_copy = malloc(output->shm_size);+ if (prev_copy) {+ memcpy(prev_copy, output->shm_data, output->shm_size);+ has_prev = true;+ }+ }+ // Pre-fill with black memset(output->shm_data, 0, output->shm_size); @@ -313,6 +325,45 @@ static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) { break; } }++ // Apply crossfade transition blend+ if (has_prev && prev_copy) {+ float alpha = chroma_transition_get_alpha(output, chroma_get_time_ms());+ uint32_t *prev = (uint32_t *)prev_copy;++ for (int i = 0; i < out_w * out_h; i++) {+ uint32_t new_px = dst[i];+ uint32_t old_px = prev[i];++ unsigned char na = (unsigned char)(new_px >> 24);+ unsigned char nr = (unsigned char)(new_px >> 16);+ unsigned char ng = (unsigned char)(new_px >> 8);+ unsigned char nb = (unsigned char)(new_px);++ unsigned char oa = (unsigned char)(old_px >> 24);+ unsigned char or_ = (unsigned char)(old_px >> 16);+ unsigned char og = (unsigned char)(old_px >> 8);+ unsigned char ob = (unsigned char)(old_px);++ unsigned char fa =+ (unsigned char)(oa ++ (unsigned char)((float)((int)na - (int)oa) * alpha));+ unsigned char fr =+ (unsigned char)(or_ ++ (unsigned char)((float)((int)nr - (int)or_) * alpha));+ unsigned char fg =+ (unsigned char)(og ++ (unsigned char)((float)((int)ng - (int)og) * alpha));+ unsigned char fb =+ (unsigned char)(ob ++ (unsigned char)((float)((int)nb - (int)ob) * alpha));++ dst[i] = ((uint32_t)fa << 24) | ((uint32_t)fr << 16) |+ ((uint32_t)fg << 8) | (uint32_t)fb;+ }++ free(prev_copy);+ } } // Create surface for output@@ -435,12 +486,5 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { // Unmap client-side memory; compositor keeps its own fd reference chroma_buffer_unmap(output); - // Release image reference and free CPU data if no longer needed- if (output->image) {- chroma_image_t *image = output->image;- chroma_image_release(image);- output->image = NULL;- }- return CHROMA_OK; }@@ -0,0 +1,108 @@+#include <stdio.h>+#include <stdlib.h>+#include <string.h>++#include "../include/chroma.h"++void chroma_transition_init_output(chroma_output_t *output, int duration_ms) {+ if (!output) {+ return;+ }++ if (duration_ms <= 0) {+ chroma_log("DEBUG", "Transition skipped for output %u: duration_ms=%d",+ output->id, duration_ms);+ return;+ }++ output->transition_duration_ms = duration_ms;+ output->transition_start_ms = chroma_get_time_ms();+ output->in_transition = true;++ chroma_log("DEBUG", "Transition started for output %u: %dms", output->id,+ duration_ms);+}++void chroma_transition_cleanup_output(chroma_output_t *output) {+ if (!output) {+ return;+ }++ if (output->transition_prev) {+ free(output->transition_prev);+ output->transition_prev = NULL;+ output->transition_prev_size = 0;+ }++ output->in_transition = false;+ output->transition_start_ms = 0;+}++bool chroma_transition_is_active(chroma_output_t *output) {+ if (!output || !output->in_transition) {+ return false;+ }++ long long now = chroma_get_time_ms();+ long long elapsed = now - output->transition_start_ms;++ if (elapsed >= output->transition_duration_ms) {+ chroma_transition_cleanup_output(output);+ return false;+ }++ return true;+}++float chroma_transition_get_alpha(chroma_output_t *output,+ long long current_time_ms) {+ if (!output || !output->in_transition) {+ return 1.0f;+ }++ long long elapsed = current_time_ms - output->transition_start_ms;+ if (elapsed <= 0)+ return 0.0f;+ if (elapsed >= output->transition_duration_ms)+ return 1.0f;++ float alpha = (float)elapsed / (float)output->transition_duration_ms;+ return alpha;+}++bool chroma_transition_needs_redraw(chroma_state_t *state) {+ if (!state) {+ return false;+ }++ for (int i = 0; i < state->output_count; i++) {+ if (chroma_transition_is_active(&state->outputs[i])) {+ return true;+ }+ }++ return false;+}++int chroma_transition_update_outputs(chroma_state_t *state) {+ if (!state) {+ return CHROMA_ERROR_INIT;+ }++ int updated = 0;+ for (int i = 0; i < state->output_count; i++) {+ chroma_output_t *output = &state->outputs[i];+ if (chroma_transition_is_active(output)) {+ int ret = chroma_render_wallpaper(state, output);+ if (ret == CHROMA_OK) {+ updated++;+ }+ }+ }++ if (updated > 0) {+ chroma_log("DEBUG", "Transition frames rendered: %d outputs", updated);+ }++ return CHROMA_OK;+}