brewery
notashelf /
172c36ba42b26c85d310f1e55773d63642061ada

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/animation.cC363 lines9.7 KB
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <string.h>
4
5
#include "../include/chroma.h"
6
#include "../include/vendor/stb_image.h"
7
8
// GIF block types
9
#define GIF_EXT_INTRODUCER 0x21
10
#define GIF_GRAPHIC_CONTROL 0xF9
11
#define GIF_IMAGE_SEPARATOR 0x2C
12
#define GIF_TRAILER 0x3B
13
14
// Read a little-endian uint16 from a buffer
15
static uint16_t read_le16(const unsigned char *buf) {
16
  return (uint16_t)(buf[0] | (buf[1] << 8));
17
}
18
19
// Parse a GIF file to extract frame count and timing
20
static int gif_parse_header(const unsigned char *data, size_t size, int *width,
21
                            int *height, int *frame_count, int **frame_delays) {
22
  if (size < 13)
23
    return -1;
24
25
  // Verify GIF87a or GIF89a signature
26
  if (memcmp(data, "GIF87a", 6) != 0 && memcmp(data, "GIF89a", 6) != 0) {
27
    return -1;
28
  }
29
30
  *width = (int)read_le16(data + 6);
31
  *height = (int)read_le16(data + 8);
32
33
  // Skip logical screen descriptor fields
34
  size_t pos = 10;
35
  unsigned char packed = data[pos++];
36
  int has_gct = (packed & 0x80) >> 7;
37
  int bpp = (packed & 0x07) + 1;
38
  int gct_size = has_gct ? (3 << bpp) : 0;
39
40
  pos += 1; // background color index
41
  pos += 1; // pixel aspect ratio
42
  pos += (size_t)gct_size;
43
44
  // Count frames and parse frame delays
45
  int count = 0;
46
  int *delays = NULL;
47
48
  if (frame_count && frame_delays) {
49
    delays = malloc(1024 * sizeof(int));
50
    if (!delays)
51
      return -1;
52
  }
53
54
  while (pos < size) {
55
    unsigned char block_type = data[pos];
56
57
    if (block_type == GIF_TRAILER) {
58
      break;
59
    }
60
61
    if (block_type == GIF_EXT_INTRODUCER && pos + 1 < size) {
62
      unsigned char ext_type = data[pos + 1];
63
      pos += 2;
64
65
      if (ext_type == GIF_GRAPHIC_CONTROL && pos + 7 < size) {
66
        unsigned char block_size = data[pos];
67
        pos += 1 + (size_t)block_size + 1;
68
69
        if (delays && count < 1024) {
70
          // GCE block: [size:1][flags:1][delay:2][transparent:1][terminator:1]
71
          // pos has moved past the block, so data[pos - block_size] is the
72
          // start
73
          uint16_t delay_cs = read_le16(data + pos - block_size);
74
          delays[count] = (int)delay_cs * 10;
75
        }
76
      } else {
77
        // Skip other extensions
78
        while (pos < size && data[pos] != 0x00) {
79
          unsigned char sub_size = data[pos];
80
          pos += 1 + sub_size;
81
        }
82
        if (pos < size && data[pos] == 0x00) {
83
          pos++;
84
        }
85
      }
86
    } else if (block_type == GIF_IMAGE_SEPARATOR) {
87
      count++;
88
      pos += 10; // skip image descriptor header
89
90
      // Skip local color table if present
91
      unsigned char img_packed = data[pos - 1];
92
      int has_lct = (img_packed & 0x80) >> 7;
93
      int img_bpp = (img_packed & 0x07) + 1;
94
      int lct_size = has_lct ? (3 << img_bpp) : 0;
95
      pos += (size_t)lct_size;
96
97
      // Skip LZW minimum code size
98
      pos += 1;
99
100
      // Skip image data sub-blocks
101
      while (pos < size && data[pos] != 0x00) {
102
        unsigned char sub_size = data[pos];
103
        pos += 1 + sub_size;
104
      }
105
      if (pos < size && data[pos] == 0x00) {
106
        pos++;
107
      }
108
    } else {
109
      pos++;
110
    }
111
  }
112
113
  if (delays) {
114
    *frame_count = count;
115
    *frame_delays = delays;
116
  }
117
118
  return 0;
119
}
120
121
// Load an animated GIF into chroma_image_t
122
// Uses stb_image to decode individual frames
123
int chroma_anim_load_gif(chroma_image_t *image, const char *path) {
124
  if (!image || !path) {
125
    return CHROMA_ERROR_INIT;
126
  }
127
128
  // Read the entire GIF file into memory
129
  FILE *fp = fopen(path, "rb");
130
  if (!fp) {
131
    chroma_log("ERROR", "Cannot open GIF file: %s", path);
132
    return CHROMA_ERROR_IMAGE;
133
  }
134
135
  fseek(fp, 0, SEEK_END);
136
  long file_size = ftell(fp);
137
  fseek(fp, 0, SEEK_SET);
138
139
  if (file_size <= 0 || file_size > 100 * 1024 * 1024) {
140
    chroma_log("ERROR", "GIF file too large or invalid: %s", path);
141
    fclose(fp);
142
    return CHROMA_ERROR_IMAGE;
143
  }
144
145
  unsigned char *gif_data = malloc((size_t)file_size);
146
  if (!gif_data) {
147
    fclose(fp);
148
    return CHROMA_ERROR_MEMORY;
149
  }
150
151
  if (fread(gif_data, 1, (size_t)file_size, fp) != (size_t)file_size) {
152
    chroma_log("ERROR", "Failed to read GIF file: %s", path);
153
    free(gif_data);
154
    fclose(fp);
155
    return CHROMA_ERROR_IMAGE;
156
  }
157
  fclose(fp);
158
159
  int gif_width, gif_height, frame_count;
160
  int *frame_delays = NULL;
161
162
  if (gif_parse_header(gif_data, (size_t)file_size, &gif_width, &gif_height,
163
                       &frame_count, &frame_delays) != 0) {
164
    chroma_log("ERROR", "Failed to parse GIF header: %s", path);
165
    free(gif_data);
166
    return CHROMA_ERROR_IMAGE;
167
  }
168
169
  if (frame_count < 2) {
170
    // Single-frame GIF - use stb_image to load as regular image
171
    free(gif_data);
172
    free(frame_delays);
173
    return 1; // signal to caller: not animated
174
  }
175
176
  chroma_log("DEBUG", "Animated GIF: %dx%d, %d frames", gif_width, gif_height,
177
             frame_count);
178
179
  // Load frames using stb_image
180
  int *delays_stbi = NULL;
181
  int frame_w, frame_h, comp;
182
  unsigned char *frames_data =
183
      stbi_load_gif_from_memory(gif_data, (int)file_size, &delays_stbi,
184
                                &frame_w, &frame_h, &frame_count, &comp, 4);
185
186
  free(gif_data);
187
188
  if (!frames_data || frame_count < 2) {
189
    chroma_log("ERROR", "Failed to decode GIF frames: %s", path);
190
    if (frames_data)
191
      stbi_image_free(frames_data);
192
    free(frame_delays);
193
    if (delays_stbi)
194
      stbi_image_free(delays_stbi);
195
    return CHROMA_ERROR_IMAGE;
196
  }
197
198
  image->is_animated = true;
199
  image->frame_count = frame_count;
200
  image->current_frame = 0;
201
  image->width = frame_w;
202
  image->height = frame_h;
203
  image->channels = 4;
204
205
  image->frames = malloc((size_t)frame_count * sizeof(unsigned char *));
206
  image->frame_delays_ms = malloc((size_t)frame_count * sizeof(int));
207
  image->frame_widths = malloc((size_t)frame_count * sizeof(int));
208
  image->frame_heights = malloc((size_t)frame_count * sizeof(int));
209
210
  if (!image->frames || !image->frame_delays_ms || !image->frame_widths ||
211
      !image->frame_heights) {
212
    free(image->frames);
213
    free(image->frame_delays_ms);
214
    free(image->frame_widths);
215
    free(image->frame_heights);
216
    stbi_image_free(frames_data);
217
    free(frame_delays);
218
    if (delays_stbi)
219
      stbi_image_free(delays_stbi);
220
    return CHROMA_ERROR_MEMORY;
221
  }
222
223
  size_t frame_size = (size_t)frame_w * (size_t)frame_h * 4;
224
225
  for (int i = 0; i < frame_count; i++) {
226
    image->frames[i] = malloc(frame_size);
227
    if (!image->frames[i]) {
228
      for (int j = 0; j < i; j++)
229
        free(image->frames[j]);
230
      free(image->frames);
231
      free(image->frame_delays_ms);
232
      free(image->frame_widths);
233
      free(image->frame_heights);
234
      free(frames_data);
235
      free(frame_delays);
236
      if (delays_stbi)
237
        stbi_image_free(delays_stbi);
238
      return CHROMA_ERROR_MEMORY;
239
    }
240
    memcpy(image->frames[i], frames_data + (size_t)i * frame_size, frame_size);
241
    image->frame_delays_ms[i] =
242
        delays_stbi ? delays_stbi[i] : (frame_delays ? frame_delays[i] : 100);
243
    image->frame_widths[i] = frame_w;
244
    image->frame_heights[i] = frame_h;
245
  }
246
247
  stbi_image_free(frames_data);
248
  free(frame_delays);
249
  if (delays_stbi)
250
    stbi_image_free(delays_stbi);
251
252
  // Set initial data to first frame
253
  image->data = image->frames[0];
254
  image->loaded = true;
255
  image->ref_count = 1;
256
257
  chroma_log("INFO", "Loaded animated GIF: %s (%d frames, %dx%d)", path,
258
             frame_count, frame_w, frame_h);
259
260
  return CHROMA_OK;
261
}
262
263
// Advance to the next frame if enough time has passed
264
void chroma_anim_advance_frame(chroma_image_t *image,
265
                               long long current_time_ms) {
266
  if (!image || !image->is_animated || image->frame_count < 2) {
267
    return;
268
  }
269
270
  int delay = image->frame_delays_ms[image->current_frame];
271
  if (delay < 16)
272
    delay = 100;
273
274
  if (image->last_frame_ms == 0) {
275
    image->last_frame_ms = current_time_ms;
276
    return;
277
  }
278
279
  long long elapsed = current_time_ms - image->last_frame_ms;
280
  if (elapsed < (long long)delay) {
281
    return;
282
  }
283
284
  image->current_frame = (image->current_frame + 1) % image->frame_count;
285
  image->data = image->frames[image->current_frame];
286
  image->last_frame_ms = current_time_ms;
287
}
288
289
// Free animated frame data
290
void chroma_anim_free_frames(chroma_image_t *image) {
291
  if (!image || !image->is_animated) {
292
    return;
293
  }
294
295
  for (int i = 0; i < image->frame_count; i++) {
296
    free(image->frames[i]);
297
  }
298
  free(image->frames);
299
  free(image->frame_delays_ms);
300
  free(image->frame_widths);
301
  free(image->frame_heights);
302
303
  image->frames = NULL;
304
  image->frame_delays_ms = NULL;
305
  image->frame_widths = NULL;
306
  image->frame_heights = NULL;
307
  image->frame_count = 0;
308
  image->is_animated = false;
309
}
310
311
// Check if any output has an animated image that needs frame advance
312
bool chroma_anim_needs_redraw(chroma_state_t *state) {
313
  if (!state || !state->config.animation_enabled) {
314
    return false;
315
  }
316
317
  for (int i = 0; i < state->output_count; i++) {
318
    chroma_output_t *output = &state->outputs[i];
319
    if (output->active && output->image && output->image->is_animated) {
320
      return true;
321
    }
322
  }
323
324
  return false;
325
}
326
327
// Advance animation frames and re-render
328
int chroma_anim_update_outputs(chroma_state_t *state) {
329
  if (!state || !state->config.animation_enabled) {
330
    return CHROMA_OK;
331
  }
332
333
  long long now = chroma_get_time_ms();
334
  int updated = 0;
335
336
  for (int i = 0; i < state->output_count; i++) {
337
    chroma_output_t *output = &state->outputs[i];
338
    if (!output->active || !output->image || !output->image->is_animated) {
339
      continue;
340
    }
341
342
    if (!output->surface) {
343
      continue;
344
    }
345
346
    int old_frame = output->image->current_frame;
347
    chroma_anim_advance_frame(output->image, now);
348
349
    if (output->image->current_frame != old_frame) {
350
      int ret = chroma_render_wallpaper(state, output);
351
      if (ret == CHROMA_OK) {
352
        updated++;
353
      }
354
    }
355
  }
356
357
  if (updated > 0) {
358
    chroma_log("DEBUG", "Animation frames advanced: %d outputs", updated);
359
  }
360
361
  state->last_anim_ms = now;
362
  return CHROMA_OK;
363
}