1use std::sync::Arc;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use skia_safe::{Canvas, ColorType, ImageInfo, Paint, Rect};
6
7use rustmotion_core::css::CssStyle;
8use rustmotion_core::engine::animator::AnimatedProperties;
9use rustmotion_core::engine::layout_pass::BoxLayout;
10use rustmotion_core::engine::renderer::gif_cache;
11use rustmotion_core::schema::{ImageFit, TimelineStep};
12use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
13
14fn default_loop_true() -> bool {
15 true
16}
17
18#[derive(Debug, Serialize, Deserialize, JsonSchema)]
19pub struct Gif {
20 pub src: String,
21 #[serde(default)]
22 pub fit: ImageFit,
23 #[serde(default = "default_loop_true")]
24 pub loop_gif: bool,
25 #[serde(flatten)]
26 pub timing: TimingConfig,
27 #[serde(default)]
28 pub style: CssStyle,
29 #[serde(default)]
30 pub timeline: Vec<TimelineStep>,
31 #[serde(default)]
32 pub stagger: Option<f32>,
33}
34
35rustmotion_core::impl_traits!(Gif {
36 Animatable => animation,
37 Timed => timing,
38 Styled => style,
39});
40
41fn frame_rect(canvas_w: u32, canvas_h: u32, frame: &gif::Frame<'_>) -> (u32, u32, u32, u32) {
46 let left = frame.left.min(canvas_w as u16) as u32;
47 let top = frame.top.min(canvas_h as u16) as u32;
48 let w = (frame.width as u32).min(canvas_w.saturating_sub(left));
49 let h = (frame.height as u32).min(canvas_h.saturating_sub(top));
50 (left, top, w, h)
51}
52
53fn blit_frame(composed: &mut [u8], canvas_w: u32, canvas_h: u32, frame: &gif::Frame<'_>) {
59 let (left, top, w, h) = frame_rect(canvas_w, canvas_h, frame);
60 for y in 0..h {
61 for x in 0..w {
62 let src = ((y * frame.width as u32 + x) * 4) as usize;
63 let Some(px) = frame.buffer.get(src..src + 4) else {
64 return; };
66 if px[3] == 0 {
67 continue;
68 }
69 let dst = (((top + y) * canvas_w + (left + x)) * 4) as usize;
70 composed[dst..dst + 4].copy_from_slice(px);
71 }
72 }
73}
74
75fn clear_rect(composed: &mut [u8], canvas_w: u32, canvas_h: u32, frame: &gif::Frame<'_>) {
77 let (left, top, w, h) = frame_rect(canvas_w, canvas_h, frame);
78 for y in 0..h {
79 let row = (((top + y) * canvas_w + left) * 4) as usize;
80 composed[row..row + (w as usize * 4)].fill(0);
81 }
82}
83
84type DecodedGif = (Vec<(Vec<u8>, u32, u32)>, Vec<f64>, f64);
88
89fn decode_composed_frames(src: &str) -> Option<DecodedGif> {
101 let file = match std::fs::File::open(src) {
102 Ok(f) => f,
103 Err(e) => {
104 if crate::warn_once_for(&format!("gif-open:{src}")) {
105 eprintln!("rustmotion: gif '{src}' could not be opened: {e}");
106 }
107 return None;
108 }
109 };
110
111 let mut options = gif::DecodeOptions::new();
112 options.set_color_output(gif::ColorOutput::RGBA);
113 let mut decoder = match options.read_info(file) {
114 Ok(d) => d,
115 Err(e) => {
116 if crate::warn_once_for(&format!("gif-decode:{src}")) {
117 eprintln!("rustmotion: gif '{src}' could not be decoded: {e}");
118 }
119 return None;
120 }
121 };
122
123 let canvas_w = decoder.width() as u32;
124 let canvas_h = decoder.height() as u32;
125
126 let mut frames: Vec<(Vec<u8>, u32, u32)> = Vec::new();
127 let mut cumulative_times: Vec<f64> = Vec::new();
128 let mut accumulated = 0.0;
129 let mut composed = vec![0u8; canvas_w as usize * canvas_h as usize * 4];
130
131 while let Ok(Some(frame)) = decoder.read_next_frame() {
132 let restore = (frame.dispose == gif::DisposalMethod::Previous).then(|| composed.clone());
135
136 blit_frame(&mut composed, canvas_w, canvas_h, frame);
137
138 let delay = frame.delay as f64 / 100.0;
139 let delay = if delay < 0.01 { 0.1 } else { delay };
140 accumulated += delay;
141 frames.push((composed.clone(), canvas_w, canvas_h));
142 cumulative_times.push(accumulated);
143
144 match frame.dispose {
145 gif::DisposalMethod::Background => clear_rect(&mut composed, canvas_w, canvas_h, frame),
146 gif::DisposalMethod::Previous => {
147 if let Some(prev) = restore {
148 composed = prev;
149 }
150 }
151 _ => {}
153 }
154 }
155
156 if frames.is_empty() {
157 if crate::warn_once_for(&format!("gif-empty:{src}")) {
158 eprintln!("rustmotion: gif '{src}' decoded to zero frames");
159 }
160 return None;
161 }
162
163 Some((frames, cumulative_times, accumulated))
164}
165
166impl Painter for Gif {
167 fn paint_content(
168 &self,
169 canvas: &Canvas,
170 layout: &BoxLayout,
171 _props: &AnimatedProperties,
172 ctx: &PaintCtx,
173 ) {
174 let gcache = gif_cache();
175
176 let cached = if let Some(cached) = gcache.get(&self.src) {
177 cached.clone()
178 } else {
179 let Some(decoded) = decode_composed_frames(&self.src) else {
180 return;
181 };
182 let cached = Arc::new(decoded);
183 gcache.insert(self.src.clone(), cached.clone());
184 cached
185 };
186
187 let (ref frames, ref cumulative_times, total_duration) = *cached;
188
189 if frames.is_empty() {
190 return;
191 }
192
193 let effective_time = if self.loop_gif {
194 ctx.time % total_duration
195 } else {
196 ctx.time.min(total_duration)
197 };
198
199 let frame_idx = cumulative_times
200 .partition_point(|&t| t <= effective_time)
201 .min(frames.len() - 1);
202 let (ref frame_data, gif_width, gif_height) = frames[frame_idx];
203
204 let img_info = ImageInfo::new(
205 (gif_width as i32, gif_height as i32),
206 ColorType::RGBA8888,
207 skia_safe::AlphaType::Unpremul,
208 None,
209 );
210 let row_bytes = gif_width as usize * 4;
211 let data = skia_safe::Data::new_copy(frame_data);
212 if let Some(img) = skia_safe::images::raster_from_data(&img_info, data, row_bytes) {
213 let dst = Rect::from_xywh(0.0, 0.0, layout.width, layout.height);
214 let paint = Paint::default();
215 canvas.draw_image_rect(img, None, dst, &paint);
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 fn write_two_frame_gif(path: &std::path::Path) {
228 let palette: &[u8] = &[0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF];
229 let mut file = std::fs::File::create(path).expect("create gif fixture");
230 let mut encoder = gif::Encoder::new(&mut file, 4, 2, palette).expect("gif encoder");
231
232 let mut full = gif::Frame::from_indexed_pixels(4, 2, vec![0; 8], None);
233 full.delay = 10;
234 encoder.write_frame(&full).expect("write frame 0");
235
236 let mut patch = gif::Frame::from_indexed_pixels(2, 2, vec![1; 4], None);
237 patch.left = 2;
238 patch.top = 0;
239 patch.delay = 10;
240 patch.dispose = gif::DisposalMethod::Keep;
241 encoder.write_frame(&patch).expect("write frame 1");
242 }
243
244 fn px(frame: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
245 let i = ((y * w + x) * 4) as usize;
246 frame[i..i + 4].try_into().expect("pixel")
247 }
248
249 #[test]
252 fn a_subrectangle_frame_is_composed_onto_the_previous_one() {
253 let path = std::env::temp_dir().join(format!(
254 "rustmotion_gif_{}.gif",
255 std::process::id() as u64 * 31 + 7
256 ));
257 write_two_frame_gif(&path);
258
259 let (frames, times, total) =
260 decode_composed_frames(path.to_str().expect("utf-8 path")).expect("gif must decode");
261 std::fs::remove_file(&path).ok();
262
263 assert_eq!(frames.len(), 2, "both frames must be drawable");
264 for (buf, w, h) in &frames {
265 assert_eq!(
266 buf.len(),
267 (*w as usize) * (*h as usize) * 4,
268 "every stored frame must be full-canvas, or raster_from_data \
269 silently returns None and nothing is painted"
270 );
271 }
272 assert_ne!(frames[0].0, frames[1].0, "the two frames must differ");
273
274 assert_eq!(px(&frames[1].0, 4, 0, 0), [0xFF, 0x00, 0x00, 0xFF], "kept");
277 assert_eq!(
278 px(&frames[1].0, 4, 3, 0),
279 [0x00, 0x00, 0xFF, 0xFF],
280 "patched"
281 );
282
283 assert_eq!(times.len(), 2);
284 assert!((total - 0.2).abs() < 1e-9, "0.1s per frame, got {total}");
285 }
286
287 #[test]
288 fn a_missing_file_reports_instead_of_returning_nothing() {
289 let missing = std::env::temp_dir().join("rustmotion_gif_absent_xyz.gif");
290 assert!(decode_composed_frames(missing.to_str().expect("utf-8")).is_none());
291 assert!(
293 !crate::warn_once_for(&format!("gif-open:{}", missing.to_str().expect("utf-8"))),
294 "the open failure must have reported once"
295 );
296 }
297
298 #[test]
299 fn a_frame_rect_running_past_the_canvas_is_clamped() {
300 let mut frame = gif::Frame::from_indexed_pixels(4, 4, vec![0; 16], None);
301 frame.left = 3;
302 frame.top = 3;
303 assert_eq!(frame_rect(4, 4, &frame), (3, 3, 1, 1));
304 }
305}