1use crate::error::{FigifError, Result};
7use crate::traits::GifEncoder;
8use crate::types::{EncodableFrame, EncodeConfig};
9use gif::{DisposalMethod, Encoder, Frame, Repeat};
10use image::RgbaImage;
11use image::imageops::FilterType;
12use std::fs::File;
13use std::io::{BufWriter, Write};
14use std::path::Path;
15
16#[derive(Debug, Clone, Default)]
32pub struct StandardEncoder {
33 resize_filter: ResizeFilter,
35}
36
37#[derive(Debug, Clone, Copy, Default)]
39pub enum ResizeFilter {
40 Nearest,
42 #[default]
44 Triangle,
45 CatmullRom,
47 Lanczos3,
49}
50
51impl From<ResizeFilter> for FilterType {
52 fn from(filter: ResizeFilter) -> Self {
53 match filter {
54 ResizeFilter::Nearest => FilterType::Nearest,
55 ResizeFilter::Triangle => FilterType::Triangle,
56 ResizeFilter::CatmullRom => FilterType::CatmullRom,
57 ResizeFilter::Lanczos3 => FilterType::Lanczos3,
58 }
59 }
60}
61
62impl StandardEncoder {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn with_resize_filter(mut self, filter: ResizeFilter) -> Self {
70 self.resize_filter = filter;
71 self
72 }
73
74 fn encode_to<W: Write>(
76 &self,
77 frames: &[EncodableFrame],
78 mut writer: W,
79 config: &EncodeConfig,
80 ) -> Result<()> {
81 if frames.is_empty() {
82 return Err(FigifError::NoFrames);
83 }
84
85 let first_frame = &frames[0];
87 let (src_width, src_height) = first_frame.image.dimensions();
88
89 let (out_width, out_height) = match (config.width, config.height) {
90 (Some(w), Some(h)) => (w as u32, h as u32),
91 (Some(w), None) => {
92 let ratio = w as f64 / src_width as f64;
93 (w as u32, (src_height as f64 * ratio).round() as u32)
94 }
95 (None, Some(h)) => {
96 let ratio = h as f64 / src_height as f64;
97 ((src_width as f64 * ratio).round() as u32, h as u32)
98 }
99 (None, None) => (src_width, src_height),
100 };
101
102 let mut encoder = Encoder::new(&mut writer, out_width as u16, out_height as u16, &[])
104 .map_err(|e| FigifError::EncodeError {
105 reason: e.to_string(),
106 })?;
107
108 let repeat: Repeat = config.loop_count.into();
110 encoder
111 .set_repeat(repeat)
112 .map_err(|e| FigifError::EncodeError {
113 reason: e.to_string(),
114 })?;
115
116 let needs_resize = out_width != src_width || out_height != src_height;
118 let mut prev_image: Option<RgbaImage> = None;
119
120 for (idx, encodable) in frames.iter().enumerate() {
121 let image = if needs_resize {
122 image::imageops::resize(
123 &encodable.image,
124 out_width,
125 out_height,
126 self.resize_filter.into(),
127 )
128 } else {
129 encodable.image.clone()
130 };
131
132 let frame = if let Some(prev) = prev_image.as_ref().filter(|_| idx != 0) {
134 match compute_delta_frame(&image, prev, encodable.delay_centiseconds) {
136 Some(delta_frame) => delta_frame,
137 None => {
138 Frame {
141 width: 1,
142 height: 1,
143 left: 0,
144 top: 0,
145 delay: encodable.delay_centiseconds,
146 dispose: DisposalMethod::Keep,
147 transparent: Some(0),
148 palette: Some(vec![0, 0, 0]), buffer: std::borrow::Cow::Owned(vec![0]),
150 ..Default::default()
151 }
152 }
153 }
154 } else {
155 let mut f = rgba_to_gif_frame(&image, encodable.delay_centiseconds)?;
156 f.dispose = DisposalMethod::Keep;
157 f
158 };
159
160 encoder
161 .write_frame(&frame)
162 .map_err(|e| FigifError::EncodeError {
163 reason: e.to_string(),
164 })?;
165
166 prev_image = Some(image);
167 }
168
169 Ok(())
170 }
171}
172
173impl GifEncoder for StandardEncoder {
174 fn encode(&self, frames: &[EncodableFrame], config: &EncodeConfig) -> Result<Vec<u8>> {
175 let mut buffer = Vec::new();
176 self.encode_to(frames, &mut buffer, config)?;
177 Ok(buffer)
178 }
179
180 fn encode_to_file(
181 &self,
182 frames: &[EncodableFrame],
183 path: impl AsRef<Path>,
184 config: &EncodeConfig,
185 ) -> Result<()> {
186 let path = path.as_ref();
187 let file = File::create(path).map_err(|e| FigifError::FileWrite {
188 path: path.to_path_buf(),
189 source: e,
190 })?;
191 let writer = BufWriter::new(file);
192 self.encode_to(frames, writer, config)
193 }
194
195 fn encode_to_writer<W: Write>(
196 &self,
197 frames: &[EncodableFrame],
198 writer: W,
199 config: &EncodeConfig,
200 ) -> Result<()> {
201 self.encode_to(frames, writer, config)
202 }
203
204 fn supports_lossy(&self) -> bool {
205 false
206 }
207
208 fn name(&self) -> &'static str {
209 "standard"
210 }
211}
212
213fn compute_delta_frame(
216 current: &RgbaImage,
217 prev: &RgbaImage,
218 delay: u16,
219) -> Option<Frame<'static>> {
220 let (width, height) = current.dimensions();
221
222 let mut min_x = width;
224 let mut min_y = height;
225 let mut max_x = 0u32;
226 let mut max_y = 0u32;
227
228 for y in 0..height {
229 for x in 0..width {
230 let curr_pixel = current.get_pixel(x, y);
231 let prev_pixel = prev.get_pixel(x, y);
232 if curr_pixel != prev_pixel {
233 min_x = min_x.min(x);
234 min_y = min_y.min(y);
235 max_x = max_x.max(x);
236 max_y = max_y.max(y);
237 }
238 }
239 }
240
241 if max_x < min_x || max_y < min_y {
243 return None;
244 }
245
246 let delta_width = max_x - min_x + 1;
248 let delta_height = max_y - min_y + 1;
249
250 let mut palette: Vec<[u8; 3]> = Vec::new();
253 let mut indices: Vec<u8> = Vec::with_capacity((delta_width * delta_height) as usize);
254 let mut color_map: std::collections::HashMap<[u8; 3], u8> = std::collections::HashMap::new();
255
256 let transparent_index: u8 = 0;
258 palette.push([0, 0, 0]); for y in min_y..=max_y {
261 for x in min_x..=max_x {
262 let curr_pixel = current.get_pixel(x, y);
263 let prev_pixel = prev.get_pixel(x, y);
264
265 if curr_pixel == prev_pixel {
266 indices.push(transparent_index);
268 } else {
269 let [r, g, b, a] = curr_pixel.0;
270
271 if a < 128 {
272 indices.push(transparent_index);
274 } else {
275 let color = [r, g, b];
276 let index = if let Some(&idx) = color_map.get(&color) {
277 idx
278 } else if palette.len() < 256 {
279 let idx = palette.len() as u8;
280 color_map.insert(color, idx);
281 palette.push(color);
282 idx
283 } else {
284 find_closest_color(&palette, color)
286 };
287 indices.push(index);
288 }
289 }
290 }
291 }
292
293 while palette.len() < 2 {
295 palette.push([0, 0, 0]);
296 }
297
298 let palette_size = palette.len().next_power_of_two().max(2);
300 while palette.len() < palette_size {
301 palette.push([0, 0, 0]);
302 }
303
304 let flat_palette: Vec<u8> = palette.iter().flat_map(|c| c.iter().copied()).collect();
306
307 let mut frame = Frame::from_palette_pixels(
309 delta_width as u16,
310 delta_height as u16,
311 indices,
312 flat_palette,
313 Some(transparent_index),
314 );
315
316 frame.left = min_x as u16;
317 frame.top = min_y as u16;
318 frame.delay = delay;
319 frame.dispose = DisposalMethod::Keep;
320
321 Some(frame)
322}
323
324fn rgba_to_gif_frame(image: &RgbaImage, delay: u16) -> Result<Frame<'static>> {
326 let (width, height) = image.dimensions();
327
328 let mut palette: Vec<[u8; 3]> = Vec::new();
335 let mut indices: Vec<u8> = Vec::with_capacity((width * height) as usize);
336 let mut color_map: std::collections::HashMap<[u8; 3], u8> = std::collections::HashMap::new();
337 let mut transparent_index: Option<u8> = None;
338
339 for pixel in image.pixels() {
340 let [r, g, b, a] = pixel.0;
341
342 if a < 128 {
343 if transparent_index.is_none() && palette.len() < 256 {
345 transparent_index = Some(palette.len() as u8);
346 palette.push([0, 0, 0]); }
348 indices.push(transparent_index.unwrap_or(0));
349 } else {
350 let color = [r, g, b];
351 let index = if let Some(&idx) = color_map.get(&color) {
352 idx
353 } else if palette.len() < 256 {
354 let idx = palette.len() as u8;
355 color_map.insert(color, idx);
356 palette.push(color);
357 idx
358 } else {
359 find_closest_color(&palette, color)
361 };
362 indices.push(index);
363 }
364 }
365
366 while palette.len() < 2 {
368 palette.push([0, 0, 0]);
369 }
370
371 let palette_size = palette.len().next_power_of_two().max(2);
373 while palette.len() < palette_size {
374 palette.push([0, 0, 0]);
375 }
376
377 let flat_palette: Vec<u8> = palette.iter().flat_map(|c| c.iter().copied()).collect();
379
380 let mut frame = Frame::from_palette_pixels(
382 width as u16,
383 height as u16,
384 indices,
385 flat_palette,
386 transparent_index,
387 );
388
389 frame.delay = delay;
390
391 Ok(frame)
392}
393
394fn find_closest_color(palette: &[[u8; 3]], target: [u8; 3]) -> u8 {
396 let mut best_idx = 0u8;
397 let mut best_dist = u32::MAX;
398
399 for (idx, color) in palette.iter().enumerate() {
400 let dr = (color[0] as i32 - target[0] as i32).pow(2) as u32;
401 let dg = (color[1] as i32 - target[1] as i32).pow(2) as u32;
402 let db = (color[2] as i32 - target[2] as i32).pow(2) as u32;
403 let dist = dr + dg + db;
404
405 if dist < best_dist {
406 best_dist = dist;
407 best_idx = idx as u8;
408 }
409 }
410
411 best_idx
412}