1use crate::error::{CodecError, CodecResult};
19use crate::webp::vp8l_encoder::Vp8lEncoder;
20
21const RIFF_MAGIC: &[u8; 4] = b"RIFF";
24const WEBP_MAGIC: &[u8; 4] = b"WEBP";
25
26const FOURCC_VP8X: [u8; 4] = *b"VP8X";
27const FOURCC_ANIM: [u8; 4] = *b"ANIM";
28const FOURCC_ANMF: [u8; 4] = *b"ANMF";
29const FOURCC_VP8L: [u8; 4] = *b"VP8L";
30
31const VP8X_FLAG_ANIMATION: u8 = 1 << 1;
33const VP8X_FLAG_ALPHA: u8 = 1 << 4;
35
36const RIFF_HEADER_SIZE: usize = 12;
38const CHUNK_HEADER_SIZE: usize = 8;
40const ANMF_HEADER_SIZE: usize = 16;
43const ANIM_PAYLOAD_SIZE: usize = 6;
45const VP8X_PAYLOAD_SIZE: usize = 10;
47
48#[derive(Debug, Clone)]
52pub struct WebpAnimConfig {
53 pub loop_count: u16,
55 pub background_color: u32,
57}
58
59impl Default for WebpAnimConfig {
60 fn default() -> Self {
61 Self {
62 loop_count: 0,
63 background_color: 0xFF000000, }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct WebpAnimFrame {
71 pub pixels: Vec<u8>,
73 pub width: u32,
75 pub height: u32,
77 pub timestamp_ms: u32,
79 pub x_offset: u32,
81 pub y_offset: u32,
83 pub blend: bool,
85 pub dispose: bool,
87}
88
89impl WebpAnimFrame {
90 fn validate(&self) -> CodecResult<()> {
92 if self.x_offset % 2 != 0 {
93 return Err(CodecError::InvalidParameter(format!(
94 "x_offset {} must be divisible by 2",
95 self.x_offset
96 )));
97 }
98 if self.y_offset % 2 != 0 {
99 return Err(CodecError::InvalidParameter(format!(
100 "y_offset {} must be divisible by 2",
101 self.y_offset
102 )));
103 }
104 if self.width == 0 || self.height == 0 {
105 return Err(CodecError::InvalidParameter(
106 "Frame dimensions must be non-zero".into(),
107 ));
108 }
109 let expected = (self.width as usize)
110 .checked_mul(self.height as usize)
111 .and_then(|px| px.checked_mul(4))
112 .ok_or_else(|| {
113 CodecError::InvalidParameter("Frame pixel buffer size overflow".into())
114 })?;
115 if self.pixels.len() != expected {
116 return Err(CodecError::InvalidParameter(format!(
117 "pixels length {} does not match {}×{}×4 = {}",
118 self.pixels.len(),
119 self.width,
120 self.height,
121 expected
122 )));
123 }
124 Ok(())
125 }
126}
127
128pub struct WebpAnimEncoder;
132
133impl WebpAnimEncoder {
134 pub fn encode(frames: &[WebpAnimFrame], config: &WebpAnimConfig) -> CodecResult<Vec<u8>> {
140 if frames.is_empty() {
141 return Err(CodecError::InvalidParameter(
142 "Animation must contain at least one frame".into(),
143 ));
144 }
145
146 for (i, frame) in frames.iter().enumerate() {
148 frame
149 .validate()
150 .map_err(|e| CodecError::InvalidParameter(format!("Frame {i}: {e}")))?;
151 }
152
153 let canvas_width = frames
155 .iter()
156 .map(|f| f.x_offset + f.width)
157 .max()
158 .unwrap_or(1);
159 let canvas_height = frames
160 .iter()
161 .map(|f| f.y_offset + f.height)
162 .max()
163 .unwrap_or(1);
164
165 let has_alpha = frames.iter().any(|f| has_non_opaque_alpha(&f.pixels));
167
168 let vp8x_payload = encode_vp8x(canvas_width, canvas_height, true, has_alpha);
170
171 let anim_payload = encode_anim_chunk(config);
173
174 let anmf_chunks: Vec<Vec<u8>> = frames
176 .iter()
177 .enumerate()
178 .map(|(i, frame)| encode_anmf_chunk(frame, i))
179 .collect::<CodecResult<_>>()?;
180
181 let mut body_size: usize = 4; body_size += chunk_wire_size(VP8X_PAYLOAD_SIZE);
185 body_size += chunk_wire_size(ANIM_PAYLOAD_SIZE);
186 for anmf in &anmf_chunks {
187 body_size += chunk_wire_size(anmf.len());
188 }
189
190 let mut out = Vec::with_capacity(RIFF_HEADER_SIZE + body_size);
191
192 out.extend_from_slice(RIFF_MAGIC);
194 write_u32_le(&mut out, body_size as u32);
195 out.extend_from_slice(WEBP_MAGIC);
196
197 write_chunk(&mut out, &FOURCC_VP8X, &vp8x_payload);
199
200 write_chunk(&mut out, &FOURCC_ANIM, &anim_payload);
202
203 for anmf in &anmf_chunks {
205 write_chunk(&mut out, &FOURCC_ANMF, anmf);
206 }
207
208 Ok(out)
209 }
210}
211
212pub struct WebpAnimDecoder;
216
217impl WebpAnimDecoder {
218 pub fn decode(data: &[u8]) -> CodecResult<(Vec<WebpAnimFrame>, WebpAnimConfig)> {
222 validate_riff_header(data)?;
223
224 let chunks = parse_chunks(&data[RIFF_HEADER_SIZE..], data.len() - RIFF_HEADER_SIZE)?;
225
226 let anim_payload = chunks
228 .iter()
229 .find(|(cc, _)| cc == &FOURCC_ANIM)
230 .map(|(_, d)| d.as_slice())
231 .ok_or_else(|| CodecError::InvalidBitstream("Missing ANIM chunk".into()))?;
232
233 let config = decode_anim_chunk(anim_payload)?;
234
235 let frames: Vec<WebpAnimFrame> = chunks
237 .iter()
238 .filter(|(cc, _)| cc == &FOURCC_ANMF)
239 .map(|(_, d)| decode_anmf_chunk(d))
240 .collect::<CodecResult<_>>()?;
241
242 if frames.is_empty() {
243 return Err(CodecError::InvalidBitstream(
244 "Animated WebP contains no ANMF frames".into(),
245 ));
246 }
247
248 Ok((frames, config))
249 }
250
251 pub fn frame_count(data: &[u8]) -> CodecResult<u32> {
253 if !Self::is_webp_anim(data) {
254 return Err(CodecError::InvalidBitstream(
255 "Data is not an animated WebP".into(),
256 ));
257 }
258 let chunks = parse_chunks(&data[RIFF_HEADER_SIZE..], data.len() - RIFF_HEADER_SIZE)?;
259 let count = chunks.iter().filter(|(cc, _)| cc == &FOURCC_ANMF).count();
260 Ok(count as u32)
261 }
262
263 pub fn is_webp_anim(data: &[u8]) -> bool {
265 if data.len() < RIFF_HEADER_SIZE {
266 return false;
267 }
268 if &data[0..4] != RIFF_MAGIC || &data[8..12] != WEBP_MAGIC {
269 return false;
270 }
271 let body = &data[RIFF_HEADER_SIZE..];
273 has_chunk_fourcc(body, &FOURCC_ANIM)
274 }
275}
276
277fn encode_vp8x(
281 canvas_width: u32,
282 canvas_height: u32,
283 has_anim: bool,
284 has_alpha: bool,
285) -> [u8; VP8X_PAYLOAD_SIZE] {
286 let mut buf = [0u8; VP8X_PAYLOAD_SIZE];
287 let mut flags: u8 = 0;
288 if has_anim {
289 flags |= VP8X_FLAG_ANIMATION;
290 }
291 if has_alpha {
292 flags |= VP8X_FLAG_ALPHA;
293 }
294 buf[0] = flags;
295 let w = canvas_width.saturating_sub(1);
297 buf[4] = (w & 0xFF) as u8;
298 buf[5] = ((w >> 8) & 0xFF) as u8;
299 buf[6] = ((w >> 16) & 0xFF) as u8;
300 let h = canvas_height.saturating_sub(1);
301 buf[7] = (h & 0xFF) as u8;
302 buf[8] = ((h >> 8) & 0xFF) as u8;
303 buf[9] = ((h >> 16) & 0xFF) as u8;
304 buf
305}
306
307fn encode_anim_chunk(config: &WebpAnimConfig) -> [u8; ANIM_PAYLOAD_SIZE] {
311 let mut buf = [0u8; ANIM_PAYLOAD_SIZE];
312 let aa = ((config.background_color >> 24) & 0xFF) as u8;
314 let rr = ((config.background_color >> 16) & 0xFF) as u8;
315 let gg = ((config.background_color >> 8) & 0xFF) as u8;
316 let bb = (config.background_color & 0xFF) as u8;
317 buf[0] = bb;
318 buf[1] = gg;
319 buf[2] = rr;
320 buf[3] = aa;
321 let lc = config.loop_count.to_le_bytes();
322 buf[4] = lc[0];
323 buf[5] = lc[1];
324 buf
325}
326
327fn encode_anmf_chunk(frame: &WebpAnimFrame, _index: usize) -> CodecResult<Vec<u8>> {
338 let vp8l_data = encode_frame_vp8l(frame)?;
340
341 let inner_chunk_size =
344 CHUNK_HEADER_SIZE + vp8l_data.len() + if vp8l_data.len() % 2 != 0 { 1 } else { 0 };
345 let mut payload = Vec::with_capacity(ANMF_HEADER_SIZE + inner_chunk_size);
346
347 let x2 = frame.x_offset / 2;
349 let y2 = frame.y_offset / 2;
350 write_u24_le(&mut payload, x2);
351 write_u24_le(&mut payload, y2);
352
353 write_u24_le(&mut payload, frame.width.saturating_sub(1));
355 write_u24_le(&mut payload, frame.height.saturating_sub(1));
356
357 write_u24_le(&mut payload, frame.timestamp_ms.min(0x00FF_FFFF));
359
360 let mut flags: u8 = 0;
364 if frame.dispose {
365 flags |= 0x01;
366 }
367 if !frame.blend {
368 flags |= 0x02;
369 }
370 payload.push(flags);
371
372 write_chunk(&mut payload, &FOURCC_VP8L, &vp8l_data);
374
375 Ok(payload)
376}
377
378fn rgba_to_argb_u32(pixels: &[u8], width: u32, height: u32) -> CodecResult<Vec<u32>> {
380 let expected = (width as usize)
381 .checked_mul(height as usize)
382 .and_then(|n| n.checked_mul(4))
383 .ok_or_else(|| CodecError::InvalidParameter("Pixel buffer size overflow".into()))?;
384 if pixels.len() < expected {
385 return Err(CodecError::InvalidParameter(format!(
386 "Pixel buffer too small: need {expected}, have {}",
387 pixels.len()
388 )));
389 }
390 let count = (width as usize) * (height as usize);
391 let mut argb = Vec::with_capacity(count);
392 for i in 0..count {
393 let r = pixels[i * 4] as u32;
394 let g = pixels[i * 4 + 1] as u32;
395 let b = pixels[i * 4 + 2] as u32;
396 let a = pixels[i * 4 + 3] as u32;
397 argb.push((a << 24) | (r << 16) | (g << 8) | b);
398 }
399 Ok(argb)
400}
401
402fn encode_frame_vp8l(frame: &WebpAnimFrame) -> CodecResult<Vec<u8>> {
404 let argb = rgba_to_argb_u32(&frame.pixels, frame.width, frame.height)?;
405 let has_alpha = has_non_opaque_alpha(&frame.pixels);
406 let encoder = Vp8lEncoder::new(0);
407 encoder.encode(&argb, frame.width, frame.height, has_alpha)
408}
409
410fn has_non_opaque_alpha(pixels: &[u8]) -> bool {
412 pixels.chunks_exact(4).any(|px| px[3] < 255)
413}
414
415fn validate_riff_header(data: &[u8]) -> CodecResult<()> {
419 if data.len() < RIFF_HEADER_SIZE {
420 return Err(CodecError::InvalidBitstream(
421 "Data too small for RIFF header".into(),
422 ));
423 }
424 if &data[0..4] != RIFF_MAGIC {
425 return Err(CodecError::InvalidBitstream(
426 "Missing RIFF magic bytes".into(),
427 ));
428 }
429 if &data[8..12] != WEBP_MAGIC {
430 return Err(CodecError::InvalidBitstream(
431 "Missing WEBP form type magic".into(),
432 ));
433 }
434 Ok(())
435}
436
437fn parse_chunks(body: &[u8], _body_len: usize) -> CodecResult<Vec<([u8; 4], Vec<u8>)>> {
444 let mut offset = 0usize;
445 let mut chunks = Vec::new();
446
447 while offset + CHUNK_HEADER_SIZE <= body.len() {
448 let mut fourcc = [0u8; 4];
449 fourcc.copy_from_slice(&body[offset..offset + 4]);
450 let chunk_size = read_u32_le(&body[offset + 4..offset + 8]) as usize;
451 offset += CHUNK_HEADER_SIZE;
452
453 if offset + chunk_size > body.len() {
454 return Err(CodecError::InvalidBitstream(format!(
455 "Chunk '{}' at offset {} declares size {} but only {} bytes remain",
456 String::from_utf8_lossy(&fourcc),
457 offset - CHUNK_HEADER_SIZE,
458 chunk_size,
459 body.len().saturating_sub(offset),
460 )));
461 }
462
463 let payload = body[offset..offset + chunk_size].to_vec();
464 chunks.push((fourcc, payload));
465
466 offset += chunk_size;
467 if chunk_size % 2 != 0 {
468 offset += 1; }
470 }
471
472 Ok(chunks)
473}
474
475fn decode_anim_chunk(data: &[u8]) -> CodecResult<WebpAnimConfig> {
477 if data.len() < ANIM_PAYLOAD_SIZE {
478 return Err(CodecError::InvalidBitstream(format!(
479 "ANIM chunk too small: need {ANIM_PAYLOAD_SIZE}, got {}",
480 data.len()
481 )));
482 }
483 let bb = data[0] as u32;
485 let gg = data[1] as u32;
486 let rr = data[2] as u32;
487 let aa = data[3] as u32;
488 let background_color = (aa << 24) | (rr << 16) | (gg << 8) | bb;
489 let loop_count = u16::from_le_bytes([data[4], data[5]]);
490 Ok(WebpAnimConfig {
491 loop_count,
492 background_color,
493 })
494}
495
496fn decode_anmf_chunk(data: &[u8]) -> CodecResult<WebpAnimFrame> {
498 if data.len() < ANMF_HEADER_SIZE {
499 return Err(CodecError::InvalidBitstream(format!(
500 "ANMF chunk too small: need {ANMF_HEADER_SIZE} bytes for header, got {}",
501 data.len()
502 )));
503 }
504
505 let x_offset = read_u24_le(&data[0..3]) * 2;
506 let y_offset = read_u24_le(&data[3..6]) * 2;
507 let width = read_u24_le(&data[6..9]) + 1;
508 let height = read_u24_le(&data[9..12]) + 1;
509 let timestamp_ms = read_u24_le(&data[12..15]);
510 let flags = data[15];
511
512 let dispose = (flags & 0x01) != 0;
513 let blend = (flags & 0x02) == 0;
514
515 let frame_data = &data[ANMF_HEADER_SIZE..];
517 let pixels = decode_vp8l_subchunk(frame_data, width, height)?;
518
519 Ok(WebpAnimFrame {
520 pixels,
521 width,
522 height,
523 timestamp_ms,
524 x_offset,
525 y_offset,
526 blend,
527 dispose,
528 })
529}
530
531fn decode_vp8l_subchunk(data: &[u8], width: u32, height: u32) -> CodecResult<Vec<u8>> {
533 if data.len() < CHUNK_HEADER_SIZE {
535 return Err(CodecError::InvalidBitstream(
536 "ANMF frame data too small for sub-chunk header".into(),
537 ));
538 }
539 let fourcc = &data[0..4];
540 if fourcc != FOURCC_VP8L {
541 return Err(CodecError::InvalidBitstream(format!(
542 "Expected VP8L sub-chunk in ANMF, got '{}'",
543 String::from_utf8_lossy(fourcc)
544 )));
545 }
546 let chunk_size = read_u32_le(&data[4..8]) as usize;
547 if data.len() < CHUNK_HEADER_SIZE + chunk_size {
548 return Err(CodecError::InvalidBitstream(
549 "VP8L sub-chunk data truncated".into(),
550 ));
551 }
552 let vp8l_data = &data[CHUNK_HEADER_SIZE..CHUNK_HEADER_SIZE + chunk_size];
553 decode_vp8l_to_rgba(vp8l_data, width, height)
554}
555
556fn decode_vp8l_to_rgba(vp8l_data: &[u8], _width: u32, _height: u32) -> CodecResult<Vec<u8>> {
558 use crate::webp::vp8l_decoder::Vp8lDecoder;
559
560 let decoded = Vp8lDecoder::new()
561 .decode(vp8l_data)
562 .map_err(|e| CodecError::DecoderError(format!("VP8L decode failed: {e}")))?;
563
564 let mut rgba = Vec::with_capacity(decoded.pixels.len() * 4);
566 for argb in &decoded.pixels {
567 let a = (argb >> 24) as u8;
568 let r = (argb >> 16) as u8;
569 let g = (argb >> 8) as u8;
570 let b = *argb as u8;
571 rgba.push(r);
572 rgba.push(g);
573 rgba.push(b);
574 rgba.push(a);
575 }
576 Ok(rgba)
577}
578
579fn has_chunk_fourcc(body: &[u8], target: &[u8; 4]) -> bool {
584 let mut offset = 0usize;
585 while offset + CHUNK_HEADER_SIZE <= body.len() {
586 let fourcc = &body[offset..offset + 4];
587 if fourcc == target.as_ref() {
588 return true;
589 }
590 let chunk_size = read_u32_le(&body[offset + 4..offset + 8]) as usize;
591 offset += CHUNK_HEADER_SIZE + chunk_size;
592 if chunk_size % 2 != 0 {
593 offset += 1;
594 }
595 }
596 false
597}
598
599fn write_chunk(buf: &mut Vec<u8>, fourcc: &[u8; 4], data: &[u8]) {
603 buf.extend_from_slice(fourcc);
604 write_u32_le(buf, data.len() as u32);
605 buf.extend_from_slice(data);
606 if data.len() % 2 != 0 {
607 buf.push(0);
608 }
609}
610
611fn chunk_wire_size(payload_len: usize) -> usize {
613 CHUNK_HEADER_SIZE + payload_len + (payload_len % 2)
614}
615
616fn write_u32_le(buf: &mut Vec<u8>, v: u32) {
617 buf.extend_from_slice(&v.to_le_bytes());
618}
619
620fn write_u24_le(buf: &mut Vec<u8>, v: u32) {
621 buf.push((v & 0xFF) as u8);
622 buf.push(((v >> 8) & 0xFF) as u8);
623 buf.push(((v >> 16) & 0xFF) as u8);
624}
625
626fn read_u32_le(data: &[u8]) -> u32 {
627 let mut b = [0u8; 4];
628 b.copy_from_slice(&data[..4]);
629 u32::from_le_bytes(b)
630}
631
632fn read_u24_le(data: &[u8]) -> u32 {
633 u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16)
634}
635
636#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn make_solid_frame(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> WebpAnimFrame {
644 let pixels = (0..width * height)
645 .flat_map(|_| [r, g, b, a])
646 .collect::<Vec<u8>>();
647 WebpAnimFrame {
648 pixels,
649 width,
650 height,
651 timestamp_ms: 0,
652 x_offset: 0,
653 y_offset: 0,
654 blend: true,
655 dispose: false,
656 }
657 }
658
659 fn make_colour_frames() -> Vec<WebpAnimFrame> {
661 let colours: &[(u8, u8, u8, u8, u32)] = &[
662 (255, 0, 0, 255, 0),
663 (0, 255, 0, 255, 100),
664 (0, 0, 255, 255, 200),
665 ];
666 colours
667 .iter()
668 .map(|&(r, g, b, a, ts)| {
669 let mut frame = make_solid_frame(4, 4, r, g, b, a);
670 frame.timestamp_ms = ts;
671 frame
672 })
673 .collect()
674 }
675
676 #[test]
679 fn test_is_webp_anim_true_after_encode() {
680 let frames = make_colour_frames();
681 let config = WebpAnimConfig::default();
682 let data = WebpAnimEncoder::encode(&frames, &config).expect("encode");
683 assert!(WebpAnimDecoder::is_webp_anim(&data));
684 }
685
686 #[test]
687 fn test_is_webp_anim_false_for_empty() {
688 assert!(!WebpAnimDecoder::is_webp_anim(&[]));
689 }
690
691 #[test]
692 fn test_is_webp_anim_false_for_garbage() {
693 let junk = vec![0xFFu8; 64];
694 assert!(!WebpAnimDecoder::is_webp_anim(&junk));
695 }
696
697 #[test]
698 fn test_is_webp_anim_false_for_truncated_riff() {
699 let mut data = vec![0u8; 20];
701 data[0..4].copy_from_slice(RIFF_MAGIC);
702 data[8..12].copy_from_slice(WEBP_MAGIC);
703 assert!(!WebpAnimDecoder::is_webp_anim(&data));
704 }
705
706 #[test]
709 fn test_frame_count_single() {
710 let frames = vec![make_solid_frame(2, 2, 128, 128, 128, 255)];
711 let config = WebpAnimConfig::default();
712 let data = WebpAnimEncoder::encode(&frames, &config).expect("encode");
713 let count = WebpAnimDecoder::frame_count(&data).expect("count");
714 assert_eq!(count, 1);
715 }
716
717 #[test]
718 fn test_frame_count_multiple() {
719 let frames = make_colour_frames();
720 let config = WebpAnimConfig::default();
721 let data = WebpAnimEncoder::encode(&frames, &config).expect("encode");
722 let count = WebpAnimDecoder::frame_count(&data).expect("count");
723 assert_eq!(count, 3);
724 }
725
726 #[test]
727 fn test_frame_count_error_on_non_anim() {
728 let data = b"RIFF\x00\x00\x00\x00WEBPnothing-here-at-all";
729 assert!(WebpAnimDecoder::frame_count(data).is_err());
730 }
731
732 #[test]
735 fn test_roundtrip_single_frame() {
736 let frame = make_solid_frame(4, 4, 200, 100, 50, 255);
737 let config = WebpAnimConfig {
738 loop_count: 3,
739 background_color: 0xFF_FF0000,
740 };
741 let data = WebpAnimEncoder::encode(&[frame.clone()], &config).expect("encode");
742 let (decoded_frames, decoded_config) = WebpAnimDecoder::decode(&data).expect("decode");
743
744 assert_eq!(decoded_config.loop_count, 3);
745 assert_eq!(decoded_config.background_color, 0xFF_FF0000);
746 assert_eq!(decoded_frames.len(), 1);
747
748 let df = &decoded_frames[0];
749 assert_eq!(df.width, 4);
750 assert_eq!(df.height, 4);
751 assert_eq!(df.timestamp_ms, 0);
752 assert_eq!(df.x_offset, 0);
753 assert_eq!(df.y_offset, 0);
754 assert_eq!(df.pixels.len(), 4 * 4 * 4);
755 }
756
757 #[test]
758 fn test_roundtrip_multiple_frames() {
759 let frames = make_colour_frames();
760 let config = WebpAnimConfig {
761 loop_count: 0,
762 background_color: 0xFF_000000,
763 };
764 let data = WebpAnimEncoder::encode(&frames, &config).expect("encode");
765 let (decoded_frames, decoded_config) = WebpAnimDecoder::decode(&data).expect("decode");
766
767 assert_eq!(decoded_config.loop_count, 0);
768 assert_eq!(decoded_frames.len(), 3);
769
770 for (orig, decoded) in frames.iter().zip(decoded_frames.iter()) {
771 assert_eq!(decoded.width, orig.width);
772 assert_eq!(decoded.height, orig.height);
773 assert_eq!(decoded.timestamp_ms, orig.timestamp_ms);
774 assert_eq!(decoded.x_offset, orig.x_offset);
775 assert_eq!(decoded.y_offset, orig.y_offset);
776 assert_eq!(decoded.blend, orig.blend);
777 assert_eq!(decoded.dispose, orig.dispose);
778 assert_eq!(decoded.pixels.len(), orig.pixels.len());
779 }
780 }
781
782 #[test]
783 fn test_roundtrip_with_alpha() {
784 let frame = make_solid_frame(8, 8, 100, 150, 200, 128);
785 let config = WebpAnimConfig::default();
786 let data = WebpAnimEncoder::encode(&[frame], &config).expect("encode");
787 let (decoded_frames, _) = WebpAnimDecoder::decode(&data).expect("decode");
788 assert_eq!(decoded_frames.len(), 1);
789 assert_eq!(decoded_frames[0].pixels.len(), 8 * 8 * 4);
790 }
791
792 #[test]
793 fn test_roundtrip_dispose_and_blend_flags() {
794 let mut frame = make_solid_frame(4, 4, 0, 0, 0, 255);
795 frame.dispose = true;
796 frame.blend = false;
797 let config = WebpAnimConfig::default();
798 let data = WebpAnimEncoder::encode(&[frame], &config).expect("encode");
799 let (decoded_frames, _) = WebpAnimDecoder::decode(&data).expect("decode");
800 assert_eq!(decoded_frames[0].dispose, true);
801 assert_eq!(decoded_frames[0].blend, false);
802 }
803
804 #[test]
805 fn test_roundtrip_offsets() {
806 let mut frame = make_solid_frame(4, 4, 0, 255, 0, 255);
807 frame.x_offset = 4;
808 frame.y_offset = 6;
809 let config = WebpAnimConfig::default();
810 let data = WebpAnimEncoder::encode(&[frame], &config).expect("encode");
811 let (decoded_frames, _) = WebpAnimDecoder::decode(&data).expect("decode");
812 assert_eq!(decoded_frames[0].x_offset, 4);
813 assert_eq!(decoded_frames[0].y_offset, 6);
814 }
815
816 #[test]
819 fn test_encode_empty_frames_error() {
820 let config = WebpAnimConfig::default();
821 let result = WebpAnimEncoder::encode(&[], &config);
822 assert!(result.is_err());
823 }
824
825 #[test]
826 fn test_encode_odd_x_offset_error() {
827 let mut frame = make_solid_frame(4, 4, 0, 0, 0, 255);
828 frame.x_offset = 3;
829 let config = WebpAnimConfig::default();
830 assert!(WebpAnimEncoder::encode(&[frame], &config).is_err());
831 }
832
833 #[test]
834 fn test_encode_odd_y_offset_error() {
835 let mut frame = make_solid_frame(4, 4, 0, 0, 0, 255);
836 frame.y_offset = 1;
837 let config = WebpAnimConfig::default();
838 assert!(WebpAnimEncoder::encode(&[frame], &config).is_err());
839 }
840
841 #[test]
842 fn test_encode_zero_dimension_error() {
843 let frame = WebpAnimFrame {
844 pixels: vec![],
845 width: 0,
846 height: 4,
847 timestamp_ms: 0,
848 x_offset: 0,
849 y_offset: 0,
850 blend: true,
851 dispose: false,
852 };
853 let config = WebpAnimConfig::default();
854 assert!(WebpAnimEncoder::encode(&[frame], &config).is_err());
855 }
856
857 #[test]
858 fn test_encode_wrong_pixel_length_error() {
859 let frame = WebpAnimFrame {
860 pixels: vec![0u8; 10], width: 4,
862 height: 4,
863 timestamp_ms: 0,
864 x_offset: 0,
865 y_offset: 0,
866 blend: true,
867 dispose: false,
868 };
869 let config = WebpAnimConfig::default();
870 assert!(WebpAnimEncoder::encode(&[frame], &config).is_err());
871 }
872
873 #[test]
876 fn test_decode_too_short() {
877 assert!(WebpAnimDecoder::decode(&[0u8; 4]).is_err());
878 }
879
880 #[test]
881 fn test_decode_bad_magic() {
882 let mut data = vec![0u8; 32];
883 data[0..4].copy_from_slice(b"RIFT"); assert!(WebpAnimDecoder::decode(&data).is_err());
885 }
886
887 #[test]
888 fn test_canvas_dimensions_from_multiple_frames() {
889 let mut f1 = make_solid_frame(4, 4, 255, 0, 0, 255);
891 f1.x_offset = 0;
892 f1.y_offset = 0;
893 let mut f2 = make_solid_frame(4, 4, 0, 255, 0, 255);
894 f2.x_offset = 4;
895 f2.y_offset = 4;
896
897 let config = WebpAnimConfig::default();
898 let data = WebpAnimEncoder::encode(&[f1, f2], &config).expect("encode");
899
900 let payload_offset = RIFF_HEADER_SIZE + CHUNK_HEADER_SIZE;
904 let w = u32::from(data[payload_offset + 4])
905 | (u32::from(data[payload_offset + 5]) << 8)
906 | (u32::from(data[payload_offset + 6]) << 16);
907 let h = u32::from(data[payload_offset + 7])
908 | (u32::from(data[payload_offset + 8]) << 8)
909 | (u32::from(data[payload_offset + 9]) << 16);
910 assert_eq!(w + 1, 8); assert_eq!(h + 1, 8); let count = WebpAnimDecoder::frame_count(&data).expect("count");
914 assert_eq!(count, 2);
915 }
916
917 #[test]
918 fn test_pixel_fidelity_solid_colour() {
919 let frame = make_solid_frame(2, 2, 0, 255, 0, 255);
921 let config = WebpAnimConfig::default();
922 let data = WebpAnimEncoder::encode(&[frame.clone()], &config).expect("encode");
923 let (decoded, _) = WebpAnimDecoder::decode(&data).expect("decode");
924 assert_eq!(decoded[0].pixels, frame.pixels);
925 }
926}