1use crate::error::{CodecError, CodecResult};
11
12const RIFF_MAGIC: &[u8; 4] = b"RIFF";
16const WEBP_MAGIC: &[u8; 4] = b"WEBP";
18const RIFF_HEADER_SIZE: usize = 12;
20const CHUNK_HEADER_SIZE: usize = 8;
22const VP8_SYNC_CODE: [u8; 3] = [0x9D, 0x01, 0x2A];
24const VP8L_SIGNATURE: u8 = 0x2F;
26const VP8X_CHUNK_DATA_SIZE: usize = 10;
28
29const FOURCC_VP8: [u8; 4] = *b"VP8 ";
32const FOURCC_VP8L: [u8; 4] = *b"VP8L";
33const FOURCC_VP8X: [u8; 4] = *b"VP8X";
34const FOURCC_ALPH: [u8; 4] = *b"ALPH";
35const FOURCC_ANIM: [u8; 4] = *b"ANIM";
36const FOURCC_ANMF: [u8; 4] = *b"ANMF";
37const FOURCC_ICCP: [u8; 4] = *b"ICCP";
38const FOURCC_EXIF: [u8; 4] = *b"EXIF";
39const FOURCC_XMP: [u8; 4] = *b"XMP ";
40
41const VP8X_FLAG_ANIMATION: u8 = 1 << 1;
44const VP8X_FLAG_XMP: u8 = 1 << 2;
45const VP8X_FLAG_EXIF: u8 = 1 << 3;
46const VP8X_FLAG_ALPHA: u8 = 1 << 4;
47const VP8X_FLAG_ICC: u8 = 1 << 5;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ChunkType {
54 Vp8,
56 Vp8L,
58 Vp8X,
60 Alph,
62 Anim,
64 Anmf,
66 Iccp,
68 Exif,
70 Xmp,
72 Unknown([u8; 4]),
74}
75
76impl ChunkType {
77 fn from_fourcc(fourcc: [u8; 4]) -> Self {
79 match fourcc {
80 FOURCC_VP8 => ChunkType::Vp8,
81 FOURCC_VP8L => ChunkType::Vp8L,
82 FOURCC_VP8X => ChunkType::Vp8X,
83 FOURCC_ALPH => ChunkType::Alph,
84 FOURCC_ANIM => ChunkType::Anim,
85 FOURCC_ANMF => ChunkType::Anmf,
86 FOURCC_ICCP => ChunkType::Iccp,
87 FOURCC_EXIF => ChunkType::Exif,
88 FOURCC_XMP => ChunkType::Xmp,
89 other => ChunkType::Unknown(other),
90 }
91 }
92
93 fn to_fourcc(self) -> [u8; 4] {
95 match self {
96 ChunkType::Vp8 => FOURCC_VP8,
97 ChunkType::Vp8L => FOURCC_VP8L,
98 ChunkType::Vp8X => FOURCC_VP8X,
99 ChunkType::Alph => FOURCC_ALPH,
100 ChunkType::Anim => FOURCC_ANIM,
101 ChunkType::Anmf => FOURCC_ANMF,
102 ChunkType::Iccp => FOURCC_ICCP,
103 ChunkType::Exif => FOURCC_EXIF,
104 ChunkType::Xmp => FOURCC_XMP,
105 ChunkType::Unknown(cc) => cc,
106 }
107 }
108}
109
110impl std::fmt::Display for ChunkType {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 let fourcc = self.to_fourcc();
113 let s = String::from_utf8_lossy(&fourcc);
114 write!(f, "{s}")
115 }
116}
117
118#[derive(Debug, Clone)]
122pub struct RiffChunk {
123 pub chunk_type: ChunkType,
125 pub data: Vec<u8>,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum WebPEncoding {
134 Lossy,
136 Lossless,
138 Extended,
140}
141
142#[derive(Debug, Clone, Copy, Default)]
146pub struct Vp8xFeatures {
147 pub has_animation: bool,
149 pub has_xmp: bool,
151 pub has_exif: bool,
153 pub has_alpha: bool,
155 pub has_icc: bool,
157 pub canvas_width: u32,
159 pub canvas_height: u32,
161}
162
163impl Vp8xFeatures {
164 fn parse(data: &[u8]) -> CodecResult<Self> {
166 if data.len() < VP8X_CHUNK_DATA_SIZE {
167 return Err(CodecError::InvalidBitstream(format!(
168 "VP8X chunk too small: expected at least {VP8X_CHUNK_DATA_SIZE} bytes, got {}",
169 data.len()
170 )));
171 }
172
173 let flags = data[0];
174
175 let canvas_width =
177 u32::from(data[4]) | (u32::from(data[5]) << 8) | (u32::from(data[6]) << 16);
178 let canvas_width = canvas_width + 1;
179
180 let canvas_height =
182 u32::from(data[7]) | (u32::from(data[8]) << 8) | (u32::from(data[9]) << 16);
183 let canvas_height = canvas_height + 1;
184
185 Ok(Self {
186 has_animation: (flags & VP8X_FLAG_ANIMATION) != 0,
187 has_xmp: (flags & VP8X_FLAG_XMP) != 0,
188 has_exif: (flags & VP8X_FLAG_EXIF) != 0,
189 has_alpha: (flags & VP8X_FLAG_ALPHA) != 0,
190 has_icc: (flags & VP8X_FLAG_ICC) != 0,
191 canvas_width,
192 canvas_height,
193 })
194 }
195
196 fn encode(&self) -> [u8; VP8X_CHUNK_DATA_SIZE] {
198 let mut buf = [0u8; VP8X_CHUNK_DATA_SIZE];
199
200 let mut flags: u8 = 0;
201 if self.has_animation {
202 flags |= VP8X_FLAG_ANIMATION;
203 }
204 if self.has_xmp {
205 flags |= VP8X_FLAG_XMP;
206 }
207 if self.has_exif {
208 flags |= VP8X_FLAG_EXIF;
209 }
210 if self.has_alpha {
211 flags |= VP8X_FLAG_ALPHA;
212 }
213 if self.has_icc {
214 flags |= VP8X_FLAG_ICC;
215 }
216 buf[0] = flags;
217 let w = self.canvas_width.saturating_sub(1);
220 buf[4] = (w & 0xFF) as u8;
221 buf[5] = ((w >> 8) & 0xFF) as u8;
222 buf[6] = ((w >> 16) & 0xFF) as u8;
223
224 let h = self.canvas_height.saturating_sub(1);
225 buf[7] = (h & 0xFF) as u8;
226 buf[8] = ((h >> 8) & 0xFF) as u8;
227 buf[9] = ((h >> 16) & 0xFF) as u8;
228
229 buf
230 }
231}
232
233#[derive(Debug, Clone)]
237pub struct WebPContainer {
238 pub encoding: WebPEncoding,
240 pub features: Option<Vp8xFeatures>,
242 pub chunks: Vec<RiffChunk>,
244}
245
246impl WebPContainer {
247 pub fn parse(data: &[u8]) -> CodecResult<Self> {
251 if data.len() < RIFF_HEADER_SIZE {
252 return Err(CodecError::InvalidBitstream(
253 "Data too small for RIFF header".into(),
254 ));
255 }
256
257 if &data[0..4] != RIFF_MAGIC {
259 return Err(CodecError::InvalidBitstream(
260 "Missing RIFF magic bytes".into(),
261 ));
262 }
263
264 let file_size = read_u32_le(&data[4..8]);
266 let declared_total = file_size as usize + 8; if &data[8..12] != WEBP_MAGIC {
270 return Err(CodecError::InvalidBitstream(
271 "Missing WEBP form type".into(),
272 ));
273 }
274
275 let payload_end = declared_total.min(data.len());
277
278 let mut offset = RIFF_HEADER_SIZE;
280 let mut chunks = Vec::new();
281
282 while offset + CHUNK_HEADER_SIZE <= payload_end {
283 let mut fourcc = [0u8; 4];
284 fourcc.copy_from_slice(&data[offset..offset + 4]);
285 let chunk_size = read_u32_le(&data[offset + 4..offset + 8]) as usize;
286 offset += CHUNK_HEADER_SIZE;
287
288 if offset + chunk_size > payload_end {
290 return Err(CodecError::InvalidBitstream(format!(
291 "Chunk '{}' at offset {} declares size {} but only {} bytes remain",
292 String::from_utf8_lossy(&fourcc),
293 offset - CHUNK_HEADER_SIZE,
294 chunk_size,
295 payload_end.saturating_sub(offset),
296 )));
297 }
298
299 let chunk_data = data[offset..offset + chunk_size].to_vec();
300 chunks.push(RiffChunk {
301 chunk_type: ChunkType::from_fourcc(fourcc),
302 data: chunk_data,
303 });
304
305 offset += chunk_size;
307 if chunk_size % 2 != 0 {
308 offset += 1;
309 }
310 }
311
312 if chunks.is_empty() {
313 return Err(CodecError::InvalidBitstream(
314 "No chunks found in WebP container".into(),
315 ));
316 }
317
318 let encoding = match chunks[0].chunk_type {
320 ChunkType::Vp8 => WebPEncoding::Lossy,
321 ChunkType::Vp8L => WebPEncoding::Lossless,
322 ChunkType::Vp8X => WebPEncoding::Extended,
323 other => {
324 return Err(CodecError::InvalidBitstream(format!(
325 "Unexpected first chunk type: {other}"
326 )));
327 }
328 };
329
330 let features = if encoding == WebPEncoding::Extended {
332 Some(Vp8xFeatures::parse(&chunks[0].data)?)
333 } else {
334 None
335 };
336
337 Ok(Self {
338 encoding,
339 features,
340 chunks,
341 })
342 }
343
344 pub fn bitstream_chunk(&self) -> Option<&RiffChunk> {
349 self.chunks
350 .iter()
351 .find(|c| c.chunk_type == ChunkType::Vp8 || c.chunk_type == ChunkType::Vp8L)
352 }
353
354 pub fn alpha_chunk(&self) -> Option<&RiffChunk> {
356 self.chunks.iter().find(|c| c.chunk_type == ChunkType::Alph)
357 }
358
359 pub fn icc_chunk(&self) -> Option<&RiffChunk> {
361 self.chunks.iter().find(|c| c.chunk_type == ChunkType::Iccp)
362 }
363
364 pub fn exif_chunk(&self) -> Option<&RiffChunk> {
366 self.chunks.iter().find(|c| c.chunk_type == ChunkType::Exif)
367 }
368
369 pub fn xmp_chunk(&self) -> Option<&RiffChunk> {
371 self.chunks.iter().find(|c| c.chunk_type == ChunkType::Xmp)
372 }
373
374 pub fn anim_chunk(&self) -> Option<&RiffChunk> {
376 self.chunks.iter().find(|c| c.chunk_type == ChunkType::Anim)
377 }
378
379 pub fn animation_frames(&self) -> Vec<&RiffChunk> {
381 self.chunks
382 .iter()
383 .filter(|c| c.chunk_type == ChunkType::Anmf)
384 .collect()
385 }
386
387 pub fn dimensions(&self) -> CodecResult<(u32, u32)> {
393 if let Some(ref features) = self.features {
395 return Ok((features.canvas_width, features.canvas_height));
396 }
397
398 let bs = self
400 .bitstream_chunk()
401 .ok_or_else(|| CodecError::InvalidBitstream("No bitstream chunk found".into()))?;
402
403 match bs.chunk_type {
404 ChunkType::Vp8 => parse_vp8_dimensions(&bs.data),
405 ChunkType::Vp8L => parse_vp8l_dimensions(&bs.data),
406 _ => Err(CodecError::InvalidBitstream(
407 "Bitstream chunk is neither VP8 nor VP8L".into(),
408 )),
409 }
410 }
411}
412
413fn parse_vp8_dimensions(data: &[u8]) -> CodecResult<(u32, u32)> {
423 if data.len() < 10 {
424 return Err(CodecError::InvalidBitstream(
425 "VP8 bitstream too small for frame header".into(),
426 ));
427 }
428
429 if data[3] != VP8_SYNC_CODE[0] || data[4] != VP8_SYNC_CODE[1] || data[5] != VP8_SYNC_CODE[2] {
431 return Err(CodecError::InvalidBitstream(
432 "VP8 sync code not found (expected 0x9D 0x01 0x2A)".into(),
433 ));
434 }
435
436 let raw_width = u16::from_le_bytes([data[6], data[7]]);
437 let raw_height = u16::from_le_bytes([data[8], data[9]]);
438
439 let width = u32::from(raw_width & 0x3FFF);
441 let height = u32::from(raw_height & 0x3FFF);
442
443 if width == 0 || height == 0 {
444 return Err(CodecError::InvalidBitstream(
445 "VP8 dimensions cannot be zero".into(),
446 ));
447 }
448
449 Ok((width, height))
450}
451
452fn parse_vp8l_dimensions(data: &[u8]) -> CodecResult<(u32, u32)> {
462 if data.len() < 5 {
463 return Err(CodecError::InvalidBitstream(
464 "VP8L bitstream too small for header".into(),
465 ));
466 }
467
468 if data[0] != VP8L_SIGNATURE {
469 return Err(CodecError::InvalidBitstream(format!(
470 "VP8L signature mismatch: expected 0x{VP8L_SIGNATURE:02X}, got 0x{:02X}",
471 data[0]
472 )));
473 }
474
475 let bits = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
476
477 let width = (bits & 0x3FFF) + 1; let height = ((bits >> 14) & 0x3FFF) + 1; if width == 0 || height == 0 {
481 return Err(CodecError::InvalidBitstream(
482 "VP8L dimensions cannot be zero".into(),
483 ));
484 }
485
486 Ok((width, height))
487}
488
489pub struct WebPWriter;
493
494impl WebPWriter {
495 pub fn write_lossy(vp8_data: &[u8]) -> Vec<u8> {
497 Self::write_single_chunk(&FOURCC_VP8, vp8_data)
498 }
499
500 pub fn write_lossless(vp8l_data: &[u8]) -> Vec<u8> {
502 Self::write_single_chunk(&FOURCC_VP8L, vp8l_data)
503 }
504
505 pub fn write_extended(
509 vp8_data: &[u8],
510 alpha_data: Option<&[u8]>,
511 width: u32,
512 height: u32,
513 ) -> Vec<u8> {
514 let features = Vp8xFeatures {
515 has_alpha: alpha_data.is_some(),
516 canvas_width: width,
517 canvas_height: height,
518 ..Vp8xFeatures::default()
519 };
520
521 let vp8x_payload = features.encode();
522
523 let mut body_size: usize = 4; body_size += chunk_wire_size(&vp8x_payload);
526 if let Some(alpha) = alpha_data {
527 body_size += chunk_wire_size(alpha);
528 }
529 body_size += chunk_wire_size(vp8_data);
530
531 let mut buf = Vec::with_capacity(8 + body_size);
532
533 buf.extend_from_slice(RIFF_MAGIC);
535 buf.extend_from_slice(&(body_size as u32).to_le_bytes());
536 buf.extend_from_slice(WEBP_MAGIC);
537
538 write_chunk(&mut buf, &FOURCC_VP8X, &vp8x_payload);
540
541 if let Some(alpha) = alpha_data {
543 write_chunk(&mut buf, &FOURCC_ALPH, alpha);
544 }
545
546 write_chunk(&mut buf, &FOURCC_VP8, vp8_data);
548
549 buf
550 }
551
552 pub fn write_chunks(chunks: &[RiffChunk]) -> Vec<u8> {
556 let mut body_size: usize = 4; for chunk in chunks {
558 body_size += chunk_wire_size(&chunk.data);
559 }
560
561 let mut buf = Vec::with_capacity(8 + body_size);
562 buf.extend_from_slice(RIFF_MAGIC);
563 buf.extend_from_slice(&(body_size as u32).to_le_bytes());
564 buf.extend_from_slice(WEBP_MAGIC);
565
566 for chunk in chunks {
567 let fourcc = chunk.chunk_type.to_fourcc();
568 write_chunk(&mut buf, &fourcc, &chunk.data);
569 }
570
571 buf
572 }
573
574 fn write_single_chunk(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
576 let body_size = 4 + chunk_wire_size(payload); let mut buf = Vec::with_capacity(8 + body_size);
578
579 buf.extend_from_slice(RIFF_MAGIC);
580 buf.extend_from_slice(&(body_size as u32).to_le_bytes());
581 buf.extend_from_slice(WEBP_MAGIC);
582
583 write_chunk(&mut buf, fourcc, payload);
584 buf
585 }
586}
587
588fn read_u32_le(data: &[u8]) -> u32 {
592 let mut buf = [0u8; 4];
593 buf.copy_from_slice(&data[..4]);
594 u32::from_le_bytes(buf)
595}
596
597fn chunk_wire_size(payload: &[u8]) -> usize {
599 let padded = if payload.len() % 2 != 0 {
600 payload.len() + 1
601 } else {
602 payload.len()
603 };
604 CHUNK_HEADER_SIZE + padded
605}
606
607fn write_chunk(buf: &mut Vec<u8>, fourcc: &[u8; 4], data: &[u8]) {
609 buf.extend_from_slice(fourcc);
610 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
611 buf.extend_from_slice(data);
612 if data.len() % 2 != 0 {
613 buf.push(0); }
615}
616
617#[cfg(test)]
620mod tests {
621 use super::*;
622
623 fn make_vp8_header(width: u16, height: u16) -> Vec<u8> {
627 let mut data = vec![0u8; 10];
628 data[0] = 0x00;
630 data[1] = 0x00;
631 data[2] = 0x00;
632 data[3] = 0x9D;
634 data[4] = 0x01;
635 data[5] = 0x2A;
636 let w_bytes = width.to_le_bytes();
638 data[6] = w_bytes[0];
639 data[7] = w_bytes[1];
640 let h_bytes = height.to_le_bytes();
642 data[8] = h_bytes[0];
643 data[9] = h_bytes[1];
644 data
645 }
646
647 fn make_vp8l_header(width: u32, height: u32) -> Vec<u8> {
649 let mut data = vec![0u8; 5];
650 data[0] = VP8L_SIGNATURE;
651 let w_minus_1 = (width - 1) & 0x3FFF;
652 let h_minus_1 = (height - 1) & 0x3FFF;
653 let bits: u32 = w_minus_1 | (h_minus_1 << 14);
654 let b = bits.to_le_bytes();
655 data[1] = b[0];
656 data[2] = b[1];
657 data[3] = b[2];
658 data[4] = b[3];
659 data
660 }
661
662 fn make_simple_lossy(width: u16, height: u16) -> Vec<u8> {
664 let vp8 = make_vp8_header(width, height);
665 WebPWriter::write_lossy(&vp8)
666 }
667
668 fn make_simple_lossless(width: u32, height: u32) -> Vec<u8> {
670 let vp8l = make_vp8l_header(width, height);
671 WebPWriter::write_lossless(&vp8l)
672 }
673
674 #[test]
677 fn test_chunk_type_roundtrip() {
678 let types = [
679 ChunkType::Vp8,
680 ChunkType::Vp8L,
681 ChunkType::Vp8X,
682 ChunkType::Alph,
683 ChunkType::Anim,
684 ChunkType::Anmf,
685 ChunkType::Iccp,
686 ChunkType::Exif,
687 ChunkType::Xmp,
688 ChunkType::Unknown(*b"TEST"),
689 ];
690
691 for ct in &types {
692 let fourcc = ct.to_fourcc();
693 let recovered = ChunkType::from_fourcc(fourcc);
694 assert_eq!(*ct, recovered);
695 }
696 }
697
698 #[test]
699 fn test_chunk_type_display() {
700 assert_eq!(ChunkType::Vp8.to_string(), "VP8 ");
701 assert_eq!(ChunkType::Vp8L.to_string(), "VP8L");
702 assert_eq!(ChunkType::Xmp.to_string(), "XMP ");
703 assert_eq!(ChunkType::Unknown(*b"TSET").to_string(), "TSET");
704 }
705
706 #[test]
709 fn test_vp8x_features_parse_all_flags() {
710 let mut data = [0u8; 10];
711 data[0] =
712 VP8X_FLAG_ANIMATION | VP8X_FLAG_XMP | VP8X_FLAG_EXIF | VP8X_FLAG_ALPHA | VP8X_FLAG_ICC;
713 data[4] = 0x7F;
715 data[5] = 0x07;
716 data[6] = 0x00;
717 data[7] = 0x37;
719 data[8] = 0x04;
720 data[9] = 0x00;
721
722 let feat = Vp8xFeatures::parse(&data).expect("should parse");
723 assert!(feat.has_animation);
724 assert!(feat.has_xmp);
725 assert!(feat.has_exif);
726 assert!(feat.has_alpha);
727 assert!(feat.has_icc);
728 assert_eq!(feat.canvas_width, 1920);
729 assert_eq!(feat.canvas_height, 1080);
730 }
731
732 #[test]
733 fn test_vp8x_features_parse_no_flags() {
734 let data = [0u8; 10];
735 let feat = Vp8xFeatures::parse(&data).expect("should parse");
736 assert!(!feat.has_animation);
737 assert!(!feat.has_xmp);
738 assert!(!feat.has_exif);
739 assert!(!feat.has_alpha);
740 assert!(!feat.has_icc);
741 assert_eq!(feat.canvas_width, 1);
742 assert_eq!(feat.canvas_height, 1);
743 }
744
745 #[test]
746 fn test_vp8x_features_roundtrip() {
747 let original = Vp8xFeatures {
748 has_animation: true,
749 has_xmp: false,
750 has_exif: true,
751 has_alpha: true,
752 has_icc: false,
753 canvas_width: 3840,
754 canvas_height: 2160,
755 };
756
757 let encoded = original.encode();
758 let decoded = Vp8xFeatures::parse(&encoded).expect("should parse");
759
760 assert_eq!(original.has_animation, decoded.has_animation);
761 assert_eq!(original.has_xmp, decoded.has_xmp);
762 assert_eq!(original.has_exif, decoded.has_exif);
763 assert_eq!(original.has_alpha, decoded.has_alpha);
764 assert_eq!(original.has_icc, decoded.has_icc);
765 assert_eq!(original.canvas_width, decoded.canvas_width);
766 assert_eq!(original.canvas_height, decoded.canvas_height);
767 }
768
769 #[test]
770 fn test_vp8x_features_parse_too_small() {
771 let data = [0u8; 5];
772 let result = Vp8xFeatures::parse(&data);
773 assert!(result.is_err());
774 }
775
776 #[test]
777 fn test_vp8x_max_canvas_size() {
778 let feat = Vp8xFeatures {
780 canvas_width: 16_777_216,
781 canvas_height: 16_777_216,
782 ..Vp8xFeatures::default()
783 };
784 let encoded = feat.encode();
785 let decoded = Vp8xFeatures::parse(&encoded).expect("should parse");
786 assert_eq!(decoded.canvas_width, 16_777_216);
787 assert_eq!(decoded.canvas_height, 16_777_216);
788 }
789
790 #[test]
793 fn test_vp8_dimensions_basic() {
794 let data = make_vp8_header(640, 480);
795 let (w, h) = parse_vp8_dimensions(&data).expect("should parse");
796 assert_eq!(w, 640);
797 assert_eq!(h, 480);
798 }
799
800 #[test]
801 fn test_vp8_dimensions_with_scale_bits() {
802 let mut data = make_vp8_header(320, 240);
804 data[7] |= 0x40; data[9] |= 0x80; let (w, h) = parse_vp8_dimensions(&data).expect("should parse");
810 assert_eq!(w, 320);
812 assert_eq!(h, 240);
813 }
814
815 #[test]
816 fn test_vp8_dimensions_too_small() {
817 let data = [0u8; 5];
818 assert!(parse_vp8_dimensions(&data).is_err());
819 }
820
821 #[test]
822 fn test_vp8_dimensions_bad_sync() {
823 let mut data = make_vp8_header(100, 100);
824 data[3] = 0x00; assert!(parse_vp8_dimensions(&data).is_err());
826 }
827
828 #[test]
829 fn test_vp8_dimensions_zero_width() {
830 let mut data = make_vp8_header(0, 100);
831 data[6] = 0;
833 data[7] = 0;
834 assert!(parse_vp8_dimensions(&data).is_err());
835 }
836
837 #[test]
840 fn test_vp8l_dimensions_basic() {
841 let data = make_vp8l_header(800, 600);
842 let (w, h) = parse_vp8l_dimensions(&data).expect("should parse");
843 assert_eq!(w, 800);
844 assert_eq!(h, 600);
845 }
846
847 #[test]
848 fn test_vp8l_dimensions_one_pixel() {
849 let data = make_vp8l_header(1, 1);
850 let (w, h) = parse_vp8l_dimensions(&data).expect("should parse");
851 assert_eq!(w, 1);
852 assert_eq!(h, 1);
853 }
854
855 #[test]
856 fn test_vp8l_dimensions_max_14bit() {
857 let data = make_vp8l_header(16384, 16384);
859 let (w, h) = parse_vp8l_dimensions(&data).expect("should parse");
860 assert_eq!(w, 16384);
861 assert_eq!(h, 16384);
862 }
863
864 #[test]
865 fn test_vp8l_dimensions_too_small() {
866 let data = [VP8L_SIGNATURE, 0, 0];
867 assert!(parse_vp8l_dimensions(&data).is_err());
868 }
869
870 #[test]
871 fn test_vp8l_dimensions_bad_signature() {
872 let mut data = make_vp8l_header(100, 100);
873 data[0] = 0xFF;
874 assert!(parse_vp8l_dimensions(&data).is_err());
875 }
876
877 #[test]
880 fn test_parse_simple_lossy() {
881 let webp = make_simple_lossy(320, 240);
882 let container = WebPContainer::parse(&webp).expect("should parse");
883
884 assert_eq!(container.encoding, WebPEncoding::Lossy);
885 assert!(container.features.is_none());
886 assert_eq!(container.chunks.len(), 1);
887 assert_eq!(container.chunks[0].chunk_type, ChunkType::Vp8);
888
889 let (w, h) = container.dimensions().expect("should get dimensions");
890 assert_eq!(w, 320);
891 assert_eq!(h, 240);
892 }
893
894 #[test]
895 fn test_parse_simple_lossless() {
896 let webp = make_simple_lossless(1024, 768);
897 let container = WebPContainer::parse(&webp).expect("should parse");
898
899 assert_eq!(container.encoding, WebPEncoding::Lossless);
900 assert!(container.features.is_none());
901 assert_eq!(container.chunks.len(), 1);
902 assert_eq!(container.chunks[0].chunk_type, ChunkType::Vp8L);
903
904 let (w, h) = container.dimensions().expect("should get dimensions");
905 assert_eq!(w, 1024);
906 assert_eq!(h, 768);
907 }
908
909 #[test]
910 fn test_parse_extended_with_alpha() {
911 let vp8 = make_vp8_header(640, 480);
912 let alpha = vec![0xAA; 100];
913 let webp = WebPWriter::write_extended(&vp8, Some(&alpha), 640, 480);
914 let container = WebPContainer::parse(&webp).expect("should parse");
915
916 assert_eq!(container.encoding, WebPEncoding::Extended);
917 let features = container.features.expect("should have features");
918 assert!(features.has_alpha);
919 assert!(!features.has_animation);
920 assert_eq!(features.canvas_width, 640);
921 assert_eq!(features.canvas_height, 480);
922
923 assert_eq!(container.chunks.len(), 3); assert!(container.alpha_chunk().is_some());
925 assert_eq!(container.alpha_chunk().map(|c| c.data.len()), Some(100));
926
927 let bs = container.bitstream_chunk().expect("should have bitstream");
928 assert_eq!(bs.chunk_type, ChunkType::Vp8);
929 }
930
931 #[test]
932 fn test_parse_extended_no_alpha() {
933 let vp8 = make_vp8_header(1920, 1080);
934 let webp = WebPWriter::write_extended(&vp8, None, 1920, 1080);
935 let container = WebPContainer::parse(&webp).expect("should parse");
936
937 assert_eq!(container.encoding, WebPEncoding::Extended);
938 let features = container.features.expect("should have features");
939 assert!(!features.has_alpha);
940 assert_eq!(features.canvas_width, 1920);
941 assert_eq!(features.canvas_height, 1080);
942
943 assert_eq!(container.chunks.len(), 2); assert!(container.alpha_chunk().is_none());
945 }
946
947 #[test]
948 fn test_parse_too_small() {
949 let data = [0u8; 8];
950 assert!(WebPContainer::parse(&data).is_err());
951 }
952
953 #[test]
954 fn test_parse_bad_riff_magic() {
955 let mut webp = make_simple_lossy(10, 10);
956 webp[0] = b'X';
957 assert!(WebPContainer::parse(&webp).is_err());
958 }
959
960 #[test]
961 fn test_parse_bad_webp_magic() {
962 let mut webp = make_simple_lossy(10, 10);
963 webp[8] = b'X';
964 assert!(WebPContainer::parse(&webp).is_err());
965 }
966
967 #[test]
968 fn test_parse_empty_payload() {
969 let mut data = Vec::new();
971 data.extend_from_slice(RIFF_MAGIC);
972 data.extend_from_slice(&4u32.to_le_bytes()); data.extend_from_slice(WEBP_MAGIC);
974 assert!(WebPContainer::parse(&data).is_err());
975 }
976
977 #[test]
980 fn test_write_lossy_roundtrip() {
981 let vp8 = make_vp8_header(256, 256);
982 let webp = WebPWriter::write_lossy(&vp8);
983 let container = WebPContainer::parse(&webp).expect("should parse");
984
985 assert_eq!(container.encoding, WebPEncoding::Lossy);
986 let bs = container.bitstream_chunk().expect("bitstream");
987 assert_eq!(bs.data, vp8);
988 }
989
990 #[test]
991 fn test_write_lossless_roundtrip() {
992 let vp8l = make_vp8l_header(512, 512);
993 let webp = WebPWriter::write_lossless(&vp8l);
994 let container = WebPContainer::parse(&webp).expect("should parse");
995
996 assert_eq!(container.encoding, WebPEncoding::Lossless);
997 let bs = container.bitstream_chunk().expect("bitstream");
998 assert_eq!(bs.data, vp8l);
999 }
1000
1001 #[test]
1002 fn test_write_extended_roundtrip() {
1003 let vp8 = make_vp8_header(1280, 720);
1004 let alpha = vec![0xFF; 50];
1005 let webp = WebPWriter::write_extended(&vp8, Some(&alpha), 1280, 720);
1006 let container = WebPContainer::parse(&webp).expect("should parse");
1007
1008 assert_eq!(container.encoding, WebPEncoding::Extended);
1009 let feat = container.features.expect("features");
1010 assert!(feat.has_alpha);
1011 assert_eq!(feat.canvas_width, 1280);
1012 assert_eq!(feat.canvas_height, 720);
1013
1014 let bs = container.bitstream_chunk().expect("bitstream");
1015 assert_eq!(bs.data, vp8);
1016
1017 let alph = container.alpha_chunk().expect("alpha");
1018 assert_eq!(alph.data, alpha);
1019 }
1020
1021 #[test]
1022 fn test_write_odd_sized_payload_padding() {
1023 let vp8 = vec![
1025 0x9D, 0x01, 0x2A, 0x9D, 0x01, 0x2A, 0x01, 0x00, 0x01, 0x00, 0xAB,
1026 ];
1027 let webp = WebPWriter::write_lossy(&vp8);
1029
1030 assert_eq!(webp.len(), 32);
1032
1033 let container = WebPContainer::parse(&webp).expect("should parse padded");
1036 let bs = container.bitstream_chunk().expect("bitstream");
1037 assert_eq!(bs.data, vp8);
1038 }
1039
1040 #[test]
1041 fn test_write_chunks_custom() {
1042 let chunks = vec![
1043 RiffChunk {
1044 chunk_type: ChunkType::Vp8X,
1045 data: Vp8xFeatures {
1046 has_icc: true,
1047 canvas_width: 100,
1048 canvas_height: 100,
1049 ..Vp8xFeatures::default()
1050 }
1051 .encode()
1052 .to_vec(),
1053 },
1054 RiffChunk {
1055 chunk_type: ChunkType::Iccp,
1056 data: vec![0x01, 0x02, 0x03],
1057 },
1058 RiffChunk {
1059 chunk_type: ChunkType::Vp8,
1060 data: make_vp8_header(100, 100),
1061 },
1062 ];
1063
1064 let webp = WebPWriter::write_chunks(&chunks);
1065 let container = WebPContainer::parse(&webp).expect("should parse");
1066
1067 assert_eq!(container.encoding, WebPEncoding::Extended);
1068 assert_eq!(container.chunks.len(), 3);
1069 let feat = container.features.expect("features");
1070 assert!(feat.has_icc);
1071 assert_eq!(feat.canvas_width, 100);
1072 assert_eq!(feat.canvas_height, 100);
1073
1074 let icc = container.icc_chunk().expect("icc");
1075 assert_eq!(icc.data, vec![0x01, 0x02, 0x03]);
1076 }
1077
1078 #[test]
1081 fn test_accessor_methods_none() {
1082 let webp = make_simple_lossy(10, 10);
1083 let container = WebPContainer::parse(&webp).expect("should parse");
1084 assert!(container.alpha_chunk().is_none());
1085 assert!(container.icc_chunk().is_none());
1086 assert!(container.exif_chunk().is_none());
1087 assert!(container.xmp_chunk().is_none());
1088 assert!(container.anim_chunk().is_none());
1089 assert!(container.animation_frames().is_empty());
1090 }
1091
1092 #[test]
1093 fn test_dimensions_from_vp8x() {
1094 let vp8 = make_vp8_header(100, 100);
1095 let webp = WebPWriter::write_extended(&vp8, None, 640, 480);
1097 let container = WebPContainer::parse(&webp).expect("should parse");
1098 let (w, h) = container.dimensions().expect("dimensions");
1099 assert_eq!(w, 640);
1100 assert_eq!(h, 480);
1101 }
1102
1103 #[test]
1104 fn test_dimensions_from_vp8_bitstream() {
1105 let webp = make_simple_lossy(1920, 1080);
1106 let container = WebPContainer::parse(&webp).expect("should parse");
1107 let (w, h) = container.dimensions().expect("dimensions");
1108 assert_eq!(w, 1920);
1109 assert_eq!(h, 1080);
1110 }
1111
1112 #[test]
1113 fn test_dimensions_from_vp8l_bitstream() {
1114 let webp = make_simple_lossless(4096, 2048);
1115 let container = WebPContainer::parse(&webp).expect("should parse");
1116 let (w, h) = container.dimensions().expect("dimensions");
1117 assert_eq!(w, 4096);
1118 assert_eq!(h, 2048);
1119 }
1120
1121 #[test]
1124 fn test_unknown_chunk_type_preserved() {
1125 let chunks = vec![RiffChunk {
1126 chunk_type: ChunkType::Vp8,
1127 data: make_vp8_header(10, 10),
1128 }];
1129 let mut webp = WebPWriter::write_chunks(&chunks);
1130
1131 let old_file_size = read_u32_le(&webp[4..8]);
1134 let extra_chunk_size: u32 = 8 + 4; let new_file_size = old_file_size + extra_chunk_size;
1136 webp[4..8].copy_from_slice(&new_file_size.to_le_bytes());
1137 webp.extend_from_slice(b"ZZZZ");
1138 webp.extend_from_slice(&4u32.to_le_bytes());
1139 webp.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
1140
1141 let container = WebPContainer::parse(&webp).expect("should parse");
1142 assert_eq!(container.chunks.len(), 2);
1143 assert_eq!(container.chunks[1].chunk_type, ChunkType::Unknown(*b"ZZZZ"));
1144 assert_eq!(container.chunks[1].data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
1145 }
1146
1147 #[test]
1148 fn test_truncated_chunk_error() {
1149 let mut webp = make_simple_lossy(10, 10);
1150 let chunk_size_offset = RIFF_HEADER_SIZE + 4;
1152 webp[chunk_size_offset..chunk_size_offset + 4].copy_from_slice(&9999u32.to_le_bytes());
1153 assert!(WebPContainer::parse(&webp).is_err());
1154 }
1155
1156 #[test]
1157 fn test_multiple_chunks_with_metadata() {
1158 let vp8x_data = Vp8xFeatures {
1159 has_exif: true,
1160 has_xmp: true,
1161 canvas_width: 200,
1162 canvas_height: 150,
1163 ..Vp8xFeatures::default()
1164 }
1165 .encode();
1166
1167 let chunks = vec![
1168 RiffChunk {
1169 chunk_type: ChunkType::Vp8X,
1170 data: vp8x_data.to_vec(),
1171 },
1172 RiffChunk {
1173 chunk_type: ChunkType::Exif,
1174 data: vec![0x45, 0x78, 0x69, 0x66], },
1176 RiffChunk {
1177 chunk_type: ChunkType::Xmp,
1178 data: b"<x:xmpmeta>test</x:xmpmeta>".to_vec(),
1179 },
1180 RiffChunk {
1181 chunk_type: ChunkType::Vp8,
1182 data: make_vp8_header(200, 150),
1183 },
1184 ];
1185
1186 let webp = WebPWriter::write_chunks(&chunks);
1187 let container = WebPContainer::parse(&webp).expect("should parse");
1188
1189 assert_eq!(container.encoding, WebPEncoding::Extended);
1190 assert_eq!(container.chunks.len(), 4);
1191
1192 let feat = container.features.expect("features");
1193 assert!(feat.has_exif);
1194 assert!(feat.has_xmp);
1195
1196 let exif = container.exif_chunk().expect("exif");
1197 assert_eq!(exif.data, vec![0x45, 0x78, 0x69, 0x66]);
1198
1199 let xmp = container.xmp_chunk().expect("xmp");
1200 assert_eq!(xmp.data, b"<x:xmpmeta>test</x:xmpmeta>");
1201
1202 let (w, h) = container.dimensions().expect("dimensions");
1203 assert_eq!(w, 200);
1204 assert_eq!(h, 150);
1205 }
1206
1207 #[test]
1208 fn test_even_payload_no_padding() {
1209 let vp8 = make_vp8_header(10, 10); let webp = WebPWriter::write_lossy(&vp8);
1212 assert_eq!(webp.len(), 30);
1214 }
1215
1216 #[test]
1217 fn test_file_size_field_accuracy() {
1218 let vp8 = make_vp8_header(10, 10);
1219 let webp = WebPWriter::write_lossy(&vp8);
1220 let declared = read_u32_le(&webp[4..8]) as usize;
1221 assert_eq!(declared + 8, webp.len());
1223 }
1224
1225 #[test]
1226 fn test_extended_file_size_field_accuracy() {
1227 let vp8 = make_vp8_header(100, 100);
1228 let alpha = vec![0x42; 7]; let webp = WebPWriter::write_extended(&vp8, Some(&alpha), 100, 100);
1230 let declared = read_u32_le(&webp[4..8]) as usize;
1231 assert_eq!(declared + 8, webp.len());
1232 }
1233}