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