1use otf_pixels_compress::LzwEncoder;
17use otf_pixels_core::{
18 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
19};
20
21use crate::format::{SIGNATURE_89A, label, write_sub_blocks};
22use otf_pixels_compress::{build_palette, quantize_with};
23
24#[derive(Debug)]
26pub struct GifEncoder {
27 dither: f32,
29 colours: usize,
31 state: Option<State>,
32}
33
34#[derive(Debug)]
36struct State {
37 descriptor: ImageDescriptor,
38 rgb: Vec<u8>,
40 rows_written: u32,
41}
42
43impl Default for GifEncoder {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl GifEncoder {
50 #[must_use]
52 pub const fn new() -> Self {
53 Self {
54 dither: 1.0,
55 colours: 256,
56 state: None,
57 }
58 }
59
60 pub fn with_colours(mut self, colours: usize) -> Result<Self> {
66 if !(2..=256).contains(&colours) {
67 return Err(PixelsError::invalid_argument(
68 "colours",
69 format!("palette size must be in 2..=256, got {colours}"),
70 ));
71 }
72 self.colours = colours;
73 Ok(self)
74 }
75
76 #[must_use]
78 pub const fn without_dithering(mut self) -> Self {
79 self.dither = 0.0;
80 self
81 }
82
83 pub fn with_dither(mut self, dither: f32) -> Result<Self> {
90 if !(0.0..=1.0).contains(&dither) {
91 return Err(PixelsError::invalid_argument(
92 "dither",
93 format!("must be in 0..=1, got {dither}"),
94 ));
95 }
96 self.dither = dither;
97 Ok(self)
98 }
99
100 #[must_use]
109 pub fn from_options(options: &EncodeOptions) -> Self {
110 let colours = options.colours.map_or_else(
111 || {
112 let quality = u32::from(options.quality.clamp(1, 100));
113 (2 + (quality - 1) * 254 / 99) as usize
114 },
115 |colours| usize::from(colours).clamp(2, 256),
116 );
117 Self {
118 dither: options.dither.unwrap_or(1.0).clamp(0.0, 1.0),
119 colours,
120 state: None,
121 }
122 }
123
124 #[must_use]
126 pub const fn colours(&self) -> usize {
127 self.colours
128 }
129}
130
131fn append_as_rgb(row: &[u8], format: PixelFormat, out: &mut Vec<u8>) -> Result<()> {
138 let channels = format.channels();
139 match format {
140 PixelFormat::Rgb8 => out.extend_from_slice(row),
141 PixelFormat::Rgba8 => {
142 for pixel in row.chunks_exact(channels) {
143 let alpha = u32::from(pixel.get(3).copied().unwrap_or(255));
144 for channel in 0..3 {
145 let value = u32::from(pixel.get(channel).copied().unwrap_or(0));
146 out.push(((value * alpha + 127) / 255) as u8);
147 }
148 }
149 }
150 PixelFormat::Gray8 => {
151 for &value in row {
152 out.extend_from_slice(&[value, value, value]);
153 }
154 }
155 PixelFormat::GrayA8 => {
156 for pixel in row.chunks_exact(2) {
157 let alpha = u32::from(pixel.get(1).copied().unwrap_or(255));
158 let value = u32::from(pixel.first().copied().unwrap_or(0));
159 let blended = ((value * alpha + 127) / 255) as u8;
160 out.extend_from_slice(&[blended, blended, blended]);
161 }
162 }
163 other => {
164 return Err(PixelsError::unsupported(format!(
165 "GIF encoding needs an 8-bit format; got {other}. Convert first."
166 )));
167 }
168 }
169 Ok(())
170}
171
172impl Encoder for GifEncoder {
173 fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
174 if self.state.is_some() {
175 return Err(PixelsError::invalid_argument(
176 "descriptor",
177 "write_header called more than once",
178 ));
179 }
180 if desc.width > u32::from(u16::MAX) || desc.height > u32::from(u16::MAX) {
183 return Err(PixelsError::unsupported(format!(
184 "GIF dimensions are 16-bit; {}x{} does not fit",
185 desc.width, desc.height
186 )));
187 }
188 let mut probe = Vec::new();
191 append_as_rgb(&[], desc.pixel, &mut probe)?;
192
193 self.state = Some(State {
196 descriptor: *desc,
197 rgb: Vec::with_capacity(desc.width as usize * desc.height as usize * 3),
198 rows_written: 0,
199 });
200 Ok(())
201 }
202
203 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
204 let Some(state) = self.state.as_mut() else {
205 return Err(PixelsError::invalid_argument(
206 "row",
207 "write_row called before write_header",
208 ));
209 };
210 let expected = state.descriptor.row_bytes();
211 if row.len() != expected {
212 return Err(PixelsError::invalid_argument(
213 "row",
214 format!("row is {} bytes, expected {expected}", row.len()),
215 ));
216 }
217 if state.rows_written >= state.descriptor.height {
218 return Err(PixelsError::invalid_argument(
219 "row",
220 format!("more than {} rows written", state.descriptor.height),
221 ));
222 }
223 append_as_rgb(row, state.descriptor.pixel, &mut state.rgb)?;
224 state.rows_written += 1;
225 Ok(())
226 }
227
228 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
229 let Some(state) = self.state.as_mut() else {
230 return Err(PixelsError::malformed(
231 "gif",
232 "finish called before write_header",
233 ));
234 };
235 if state.rows_written != state.descriptor.height {
236 return Err(PixelsError::malformed(
237 "gif",
238 format!(
239 "{} of {} rows written; a partial image is never emitted",
240 state.rows_written, state.descriptor.height
241 ),
242 ));
243 }
244
245 let width = state.descriptor.width;
246 let height = state.descriptor.height;
247 let palette = build_palette(&state.rgb, self.colours);
248 let indices = quantize_with(&state.rgb, width as usize, &palette, self.dither);
249 let table = palette.padded();
250 let bits = palette.code_bits();
251
252 let mut out: Vec<u8> = Vec::new();
253 out.extend_from_slice(&SIGNATURE_89A);
254 out.extend_from_slice(&(width as u16).to_le_bytes());
255 out.extend_from_slice(&(height as u16).to_le_bytes());
256 out.push(0x80 | 0x70 | ((bits - 1) as u8));
258 out.push(0); out.push(0); for entry in &table {
261 out.extend_from_slice(entry);
262 }
263
264 out.push(label::IMAGE);
267 out.extend_from_slice(&0_u16.to_le_bytes());
268 out.extend_from_slice(&0_u16.to_le_bytes());
269 out.extend_from_slice(&(width as u16).to_le_bytes());
270 out.extend_from_slice(&(height as u16).to_le_bytes());
271 out.push(0);
272
273 let minimum_width = bits.max(2);
276 out.push(minimum_width as u8);
277 let compressed = LzwEncoder::gif(minimum_width)
278 .map_err(crate::compress_error)?
279 .encode(&indices);
280 write_sub_blocks(&mut out, &compressed);
281 out.push(label::TRAILER);
282
283 sink.write_all(&out)?;
284 sink.flush()?;
285
286 state.rgb = Vec::new();
288 Ok(())
289 }
290}
291
292#[cfg(test)]
293#[allow(
294 clippy::unwrap_used,
295 clippy::expect_used,
296 clippy::indexing_slicing,
297 clippy::panic,
298 reason = "tests operate on known-good values and assert shapes directly"
299)]
300mod tests {
301 use super::*;
302 use crate::decoder::GifDecoder;
303 use otf_pixels_core::{Decoder, Limits};
304
305 fn encode(desc: &ImageDescriptor, raster: &[u8], encoder: GifEncoder) -> Result<Vec<u8>> {
306 let mut encoder = encoder;
307 let mut out: Vec<u8> = Vec::new();
308 encoder.write_header(desc, &mut out)?;
309 for row in raster.chunks_exact(desc.row_bytes()) {
310 encoder.write_row(row, &mut out)?;
311 }
312 encoder.finish(&mut out)?;
313 Ok(out)
314 }
315
316 fn decode(bytes: &[u8]) -> Result<(ImageDescriptor, Vec<u8>)> {
317 let mut decoder = GifDecoder::new(bytes, Limits::default())?;
318 let descriptor = decoder.descriptor();
319 let mut raster = Vec::new();
320 let mut row = vec![0_u8; descriptor.row_bytes()];
321 for _ in 0..descriptor.height {
322 decoder.read_row(&mut row)?;
323 raster.extend_from_slice(&row);
324 }
325 Ok((descriptor, raster))
326 }
327
328 fn flat_art(width: u32, height: u32, colours: usize) -> (ImageDescriptor, Vec<u8>) {
331 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
332 let mut bytes = Vec::new();
333 for i in 0..(width * height) as usize {
334 let n = (i % colours) as u32;
337 let spread = (n * 255 / colours.max(1) as u32) as u8;
338 bytes.extend_from_slice(&[spread, 255 - spread, spread / 2]);
339 }
340 (descriptor, bytes)
341 }
342
343 #[test]
344 fn flat_colour_art_round_trips_exactly() {
345 for colours in [2_usize, 5, 16, 200] {
348 let (descriptor, raster) = flat_art(23, 17, colours);
349 let bytes = encode(&descriptor, &raster, GifEncoder::new())
350 .unwrap_or_else(|e| panic!("{colours} colours: {e}"));
351 let (out_desc, decoded) = decode(&bytes).unwrap();
352 assert_eq!((out_desc.width, out_desc.height), (23, 17));
353 assert_eq!(out_desc.pixel, PixelFormat::Rgba8);
354
355 for (index, (want, got)) in raster
356 .chunks_exact(3)
357 .zip(decoded.chunks_exact(4))
358 .enumerate()
359 {
360 assert_eq!(&got[..3], want, "{colours} colours: pixel {index} changed");
361 assert_eq!(got[3], 255, "pixel {index} lost opacity");
362 }
363 }
364 }
365
366 #[test]
367 fn the_output_starts_with_a_signature_and_ends_with_a_trailer() {
368 let (descriptor, raster) = flat_art(4, 4, 4);
369 let bytes = encode(&descriptor, &raster, GifEncoder::new()).unwrap();
370 assert_eq!(&bytes[..6], b"GIF89a");
371 assert_eq!(bytes.last().copied(), Some(label::TRAILER));
372 }
373
374 #[test]
375 fn a_photographic_image_round_trips_approximately() {
376 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
379 let mut raster = Vec::new();
380 for y in 0..64_u32 {
381 for x in 0..64_u32 {
382 raster.extend_from_slice(&[(x * 4) as u8, (y * 4) as u8, ((x + y) * 2) as u8]);
383 }
384 }
385 let bytes = encode(&descriptor, &raster, GifEncoder::new()).unwrap();
386 let (_, decoded) = decode(&bytes).unwrap();
387
388 let mut total = 0_u64;
389 for (want, got) in raster.chunks_exact(3).zip(decoded.chunks_exact(4)) {
390 for channel in 0..3 {
391 total += u64::from(want[channel].abs_diff(got[channel]));
392 }
393 }
394 let mean = total as f64 / (raster.len() as f64);
395 assert!(mean < 8.0, "mean per-channel error was {mean}");
396 }
397
398 #[test]
399 fn every_input_format_encodes() {
400 for format in [
401 PixelFormat::Gray8,
402 PixelFormat::GrayA8,
403 PixelFormat::Rgb8,
404 PixelFormat::Rgba8,
405 ] {
406 let descriptor = ImageDescriptor::new(8, 8, format).unwrap();
407 let raster = vec![128_u8; descriptor.byte_len().unwrap()];
408 let bytes = encode(&descriptor, &raster, GifEncoder::new())
409 .unwrap_or_else(|e| panic!("{format}: {e}"));
410 let (out_desc, _) = decode(&bytes).unwrap();
411 assert_eq!((out_desc.width, out_desc.height), (8, 8), "{format}");
412 }
413 }
414
415 #[test]
416 fn a_wide_format_is_unsupported_not_silently_narrowed() {
417 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Rgb16).unwrap();
418 let mut encoder = GifEncoder::new();
419 let mut out = Vec::new();
420 let error = encoder.write_header(&descriptor, &mut out).unwrap_err();
421 assert!(error.to_string().contains("8-bit"), "{error}");
422 assert!(
423 out.is_empty(),
424 "nothing should be written for a rejected format"
425 );
426 }
427
428 #[test]
429 fn a_short_image_is_an_error_not_a_truncated_gif() {
430 let (descriptor, _) = flat_art(4, 4, 4);
431 let mut encoder = GifEncoder::new();
432 let mut out = Vec::new();
433 encoder.write_header(&descriptor, &mut out).unwrap();
434 encoder.write_row(&[0; 12], &mut out).unwrap();
435 assert!(encoder.finish(&mut out).is_err());
436 }
437
438 #[test]
439 fn extra_rows_and_wrong_lengths_are_errors() {
440 let descriptor = ImageDescriptor::new(4, 1, PixelFormat::Rgb8).unwrap();
441 let mut encoder = GifEncoder::new();
442 let mut out = Vec::new();
443 encoder.write_header(&descriptor, &mut out).unwrap();
444 assert!(encoder.write_row(&[0; 11], &mut out).is_err(), "short row");
445 assert!(encoder.write_row(&[0; 13], &mut out).is_err(), "long row");
446 encoder.write_row(&[0; 12], &mut out).unwrap();
447 assert!(encoder.write_row(&[0; 12], &mut out).is_err(), "extra row");
448 }
449
450 #[test]
451 fn a_palette_size_outside_the_legal_range_is_rejected() {
452 assert!(GifEncoder::new().with_colours(1).is_err());
453 assert!(GifEncoder::new().with_colours(257).is_err());
454 assert!(GifEncoder::new().with_colours(2).is_ok());
455 assert!(GifEncoder::new().with_colours(256).is_ok());
456 }
457
458 #[test]
459 fn options_map_quality_onto_palette_size() {
460 assert_eq!(
461 GifEncoder::from_options(&EncodeOptions::default()).colours(),
462 204
463 );
464 let lowest = EncodeOptions::with_quality(1).unwrap();
465 assert_eq!(GifEncoder::from_options(&lowest).colours(), 2);
466 let highest = EncodeOptions::with_quality(100).unwrap();
467 assert_eq!(GifEncoder::from_options(&highest).colours(), 256);
468 }
469
470 #[test]
471 fn a_two_colour_image_uses_a_legal_code_width() {
472 let (descriptor, raster) = flat_art(3, 3, 2);
476 let bytes = encode(
477 &descriptor,
478 &raster,
479 GifEncoder::new().with_colours(2).unwrap(),
480 )
481 .unwrap();
482 let (_, decoded) = decode(&bytes).unwrap();
483 assert_eq!(decoded.len(), 3 * 3 * 4);
484 }
485
486 #[test]
487 fn encoding_is_deterministic() {
488 let descriptor = ImageDescriptor::new(40, 30, PixelFormat::Rgb8).unwrap();
490 let raster: Vec<u8> = (0..descriptor.byte_len().unwrap())
491 .map(|i| ((i * 31) % 251) as u8)
492 .collect();
493 let first = encode(&descriptor, &raster, GifEncoder::new()).unwrap();
494 for _ in 0..4 {
495 let again = encode(&descriptor, &raster, GifEncoder::new()).unwrap();
496 assert_eq!(again, first, "GIF encoding is not deterministic");
497 }
498 }
499
500 #[test]
501 fn a_single_pixel_image_round_trips() {
502 let descriptor = ImageDescriptor::new(1, 1, PixelFormat::Rgb8).unwrap();
503 let raster = vec![9_u8, 8, 7];
504 let bytes = encode(&descriptor, &raster, GifEncoder::new()).unwrap();
505 let (_, decoded) = decode(&bytes).unwrap();
506 assert_eq!(&decoded[..3], [9, 8, 7]);
507 }
508
509 #[test]
510 fn options_set_the_palette_size_and_the_dithering() {
511 let descriptor = ImageDescriptor::new(40, 30, PixelFormat::Rgb8).unwrap();
512 let raster: Vec<u8> = (0..40 * 30 * 3).map(|i| (i * 7 % 256) as u8).collect();
513 let options = EncodeOptions::default().with_colours(8).unwrap();
514 let bytes = encode(&descriptor, &raster, GifEncoder::from_options(&options)).unwrap();
515 let (_, decoded) = decode(&bytes).unwrap();
516 let mut distinct: Vec<&[u8]> = decoded.chunks_exact(4).collect();
517 distinct.sort_unstable();
518 distinct.dedup();
519 assert!(distinct.len() <= 8, "{} colours", distinct.len());
520
521 let undithered = options.with_dither(0.0).unwrap();
523 assert_eq!(
524 encode(&descriptor, &raster, GifEncoder::from_options(&undithered)).unwrap(),
525 encode(
526 &descriptor,
527 &raster,
528 GifEncoder::from_options(&options).without_dithering()
529 )
530 .unwrap()
531 );
532 let half = options.with_dither(0.5).unwrap();
533 assert_ne!(
534 encode(&descriptor, &raster, GifEncoder::from_options(&half)).unwrap(),
535 encode(&descriptor, &raster, GifEncoder::from_options(&undithered)).unwrap()
536 );
537 assert!(GifEncoder::new().with_dither(2.0).is_err());
538 }
539}