1use std::collections::VecDeque;
41
42use oxideav_core::{
43 CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag,
44 ContainerRegistry, Decoder, Encoder, Error as CoreError, Frame, MediaType, Packet, PixelFormat,
45 RuntimeContext, TimeBase, VideoFrame, VideoPlane,
46};
47
48use crate::{
49 decode_webp_image, encode_vp8l_argb_with_metadata, DecodedWebp, Error, UnsupportedKind,
50 WebpError, WebpMetadata, WebpMetadataOwned, CODEC_ID_VP8L,
51};
52
53pub const CODEC_ID_STR: &str = "webp";
57
58impl From<Error> for CoreError {
67 fn from(e: Error) -> Self {
68 match e {
69 Error::Unsupported(kind) => CoreError::Unsupported(match kind {
70 UnsupportedKind::LossyVp8 => {
71 "oxideav-webp: VP8 lossy bitstream (route to a VP8 decoder)".to_string()
72 }
73 UnsupportedKind::NoImageData => {
74 "oxideav-webp: no VP8L/VP8 image-data chunk (animation or header-only)"
75 .to_string()
76 }
77 }),
78 Error::NotImplemented => {
79 CoreError::Unsupported("oxideav-webp: code path not implemented yet".to_string())
80 }
81 Error::Vp8(ref v) => match WebpError::from(v.clone()) {
85 WebpError::Unsupported => CoreError::Unsupported(e.to_string()),
86 _ => CoreError::InvalidData(e.to_string()),
87 },
88 other => CoreError::InvalidData(other.to_string()),
89 }
90 }
91}
92
93fn decoded_webp_to_video_frame(img: DecodedWebp, pts: Option<i64>) -> VideoFrame {
103 let stride = (img.width as usize).saturating_mul(4);
104 VideoFrame {
105 pts,
106 planes: vec![VideoPlane {
107 stride,
108 data: img.rgba,
109 }],
110 }
111}
112
113pub fn decode_webp_to_frame(bytes: &[u8], pts: Option<i64>) -> oxideav_core::Result<VideoFrame> {
119 let img = decode_webp_image(bytes)?;
120 Ok(decoded_webp_to_video_frame(img, pts))
121}
122
123pub fn make_decoder(params: &CodecParameters) -> oxideav_core::Result<Box<dyn Decoder>> {
129 Ok(Box::new(WebpDecoder::new(params.clone())))
130}
131
132#[derive(Debug)]
145pub struct WebpDecoder {
146 params: CodecParameters,
151 pending: Option<Packet>,
156 eof: bool,
159}
160
161impl WebpDecoder {
162 pub fn new(params: CodecParameters) -> Self {
168 let mut p = params;
169 p.media_type = MediaType::Video;
170 p.codec_id = CodecId::new(CODEC_ID_STR);
171 p.pixel_format = Some(PixelFormat::Rgba);
172 Self {
173 params: p,
174 pending: None,
175 eof: false,
176 }
177 }
178
179 pub fn params(&self) -> &CodecParameters {
184 &self.params
185 }
186}
187
188impl Decoder for WebpDecoder {
189 fn codec_id(&self) -> &CodecId {
190 &self.params.codec_id
191 }
192
193 fn send_packet(&mut self, packet: &Packet) -> oxideav_core::Result<()> {
194 if self.pending.is_some() {
195 return Err(CoreError::other(
196 "oxideav-webp decoder: receive_frame must be called before sending another packet",
197 ));
198 }
199 self.pending = Some(packet.clone());
200 Ok(())
201 }
202
203 fn receive_frame(&mut self) -> oxideav_core::Result<Frame> {
204 let Some(pkt) = self.pending.take() else {
205 return if self.eof {
206 Err(CoreError::Eof)
207 } else {
208 Err(CoreError::NeedMore)
209 };
210 };
211 let img = decode_webp_image(&pkt.data)?;
212 self.params.width = Some(img.width);
217 self.params.height = Some(img.height);
218 self.params.pixel_format = Some(PixelFormat::Rgba);
219 let vf = decoded_webp_to_video_frame(img, pkt.pts);
220 Ok(Frame::Video(vf))
221 }
222
223 fn flush(&mut self) -> oxideav_core::Result<()> {
224 self.eof = true;
225 Ok(())
226 }
227}
228
229fn video_frame_to_argb(
240 frame: &VideoFrame,
241 width: u32,
242 height: u32,
243 pix: PixelFormat,
244) -> oxideav_core::Result<(Vec<u32>, bool)> {
245 let plane = frame
246 .planes
247 .first()
248 .ok_or_else(|| CoreError::invalid("webp_vp8l encoder: frame has no planes"))?;
249 let w = width as usize;
250 let h = height as usize;
251 let stride = plane.stride;
252 let mut pixels = Vec::with_capacity(w * h);
253 let mut alpha_is_used = false;
254 match pix {
255 PixelFormat::Rgba => {
256 for y in 0..h {
257 let row = &plane.data[y * stride..];
258 for x in 0..w {
259 let p = &row[x * 4..x * 4 + 4];
260 let (r, g, b, a) = (p[0] as u32, p[1] as u32, p[2] as u32, p[3] as u32);
261 if a != 0xff {
262 alpha_is_used = true;
263 }
264 pixels.push((a << 24) | (r << 16) | (g << 8) | b);
265 }
266 }
267 }
268 PixelFormat::Rgb24 => {
269 for y in 0..h {
270 let row = &plane.data[y * stride..];
271 for x in 0..w {
272 let p = &row[x * 3..x * 3 + 3];
273 let (r, g, b) = (p[0] as u32, p[1] as u32, p[2] as u32);
274 pixels.push((0xff << 24) | (r << 16) | (g << 8) | b);
275 }
276 }
277 }
278 other => {
279 return Err(CoreError::invalid(format!(
280 "webp_vp8l encoder: unsupported input pixel format {other:?} (want Rgba or Rgb24)"
281 )));
282 }
283 }
284 Ok((pixels, alpha_is_used))
285}
286
287pub fn make_encoder(params: &CodecParameters) -> oxideav_core::Result<Box<dyn Encoder>> {
298 make_encoder_with_metadata(params, WebpMetadataOwned::default())
299}
300
301pub fn make_encoder_with_metadata(
308 params: &CodecParameters,
309 metadata: WebpMetadataOwned,
310) -> oxideav_core::Result<Box<dyn Encoder>> {
311 let width = params
312 .width
313 .ok_or_else(|| CoreError::invalid("webp_vp8l encoder: missing width"))?;
314 let height = params
315 .height
316 .ok_or_else(|| CoreError::invalid("webp_vp8l encoder: missing height"))?;
317 let pix = params.pixel_format.unwrap_or(PixelFormat::Rgba);
318 if !matches!(pix, PixelFormat::Rgba | PixelFormat::Rgb24) {
319 return Err(CoreError::invalid(format!(
320 "webp_vp8l encoder: unsupported input pixel format {pix:?} (want Rgba or Rgb24)"
321 )));
322 }
323
324 let mut output_params = params.clone();
325 output_params.media_type = MediaType::Video;
326 output_params.codec_id = CodecId::new(CODEC_ID_VP8L);
327 output_params.width = Some(width);
328 output_params.height = Some(height);
329 output_params.pixel_format = Some(pix);
330
331 Ok(Box::new(WebpVp8lEncoder {
332 output_params,
333 width,
334 height,
335 pix,
336 metadata,
337 pending_out: VecDeque::new(),
338 eof: false,
339 }))
340}
341
342#[derive(Debug)]
350pub struct WebpVp8lEncoder {
351 output_params: CodecParameters,
352 width: u32,
353 height: u32,
354 pix: PixelFormat,
355 metadata: WebpMetadataOwned,
356 pending_out: VecDeque<Packet>,
357 eof: bool,
358}
359
360impl Encoder for WebpVp8lEncoder {
361 fn codec_id(&self) -> &CodecId {
362 &self.output_params.codec_id
363 }
364
365 fn output_params(&self) -> &CodecParameters {
366 &self.output_params
367 }
368
369 fn send_frame(&mut self, frame: &Frame) -> oxideav_core::Result<()> {
370 let Frame::Video(v) = frame else {
371 return Err(CoreError::invalid("webp_vp8l encoder: video frames only"));
372 };
373 let (argb, frame_alpha) = video_frame_to_argb(v, self.width, self.height, self.pix)?;
374 let has_alpha = frame_alpha;
375 let meta = self.metadata.as_borrowed();
376 let bytes =
377 encode_vp8l_argb_with_metadata(self.width, self.height, &argb, has_alpha, &meta)
378 .map_err(|e| CoreError::InvalidData(e.to_string()))?;
379 let mut pkt = Packet::new(0, TimeBase::new(1, 1000), bytes);
380 pkt.pts = v.pts;
381 pkt.dts = v.pts;
382 pkt.flags.keyframe = true;
383 self.pending_out.push_back(pkt);
384 Ok(())
385 }
386
387 fn receive_packet(&mut self) -> oxideav_core::Result<Packet> {
388 if let Some(p) = self.pending_out.pop_front() {
389 return Ok(p);
390 }
391 if self.eof {
392 Err(CoreError::Eof)
393 } else {
394 Err(CoreError::NeedMore)
395 }
396 }
397
398 fn flush(&mut self) -> oxideav_core::Result<()> {
399 self.eof = true;
400 Ok(())
401 }
402}
403
404pub fn encode_vp8l_frame(
411 frame: &VideoFrame,
412 width: u32,
413 height: u32,
414 pix: PixelFormat,
415 metadata: &WebpMetadata<'_>,
416) -> oxideav_core::Result<Vec<u8>> {
417 let (argb, alpha_is_used) = video_frame_to_argb(frame, width, height, pix)?;
418 encode_vp8l_argb_with_metadata(width, height, &argb, alpha_is_used, metadata)
419 .map_err(|e| CoreError::InvalidData(e.to_string()))
420}
421
422pub fn register_codecs(reg: &mut CodecRegistry) {
433 let caps = CodecCapabilities::video("webp_sw")
434 .with_intra_only(true)
435 .with_lossless(true)
436 .with_max_size(16384, 16384)
437 .with_pixel_formats(vec![PixelFormat::Rgba]);
438 reg.register(
439 CodecInfo::new(CodecId::new(CODEC_ID_STR))
440 .capabilities(caps)
441 .decoder(make_decoder)
442 .tag(CodecTag::fourcc(b"WEBP")),
443 );
444
445 let vp8l_caps = CodecCapabilities::video("webp_vp8l_sw")
449 .with_intra_only(true)
450 .with_lossless(true)
451 .with_max_size(16384, 16384)
452 .with_pixel_formats(vec![PixelFormat::Rgba, PixelFormat::Rgb24]);
453 reg.register(
454 CodecInfo::new(CodecId::new(CODEC_ID_VP8L))
455 .capabilities(vp8l_caps)
456 .decoder(make_decoder)
457 .encoder(make_encoder),
458 );
459}
460
461pub fn register_containers(reg: &mut ContainerRegistry) {
469 reg.register_extension("webp", CODEC_ID_STR);
470}
471
472pub fn register(ctx: &mut RuntimeContext) {
476 register_codecs(&mut ctx.codecs);
477 register_containers(&mut ctx.containers);
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use oxideav_core::TimeBase;
484
485 const LOSSLESS_1X1: &[u8] = include_bytes!("../tests/data/lossless-1x1.webp");
486 const LOSSY_1X1: &[u8] = include_bytes!("../tests/data/lossy-1x1.webp");
487
488 #[test]
489 fn register_via_runtime_context_installs_decoder_factory() {
490 let mut ctx = RuntimeContext::new();
491 register(&mut ctx);
492 let id = CodecId::new(CODEC_ID_STR);
493 assert!(
494 ctx.codecs.has_decoder(&id),
495 "webp decoder factory not installed via RuntimeContext"
496 );
497 assert!(!ctx.codecs.has_encoder(&id));
499 assert_eq!(ctx.containers.container_for_extension("webp"), Some("webp"));
501 assert_eq!(ctx.containers.container_for_extension("WEBP"), Some("webp"));
502 }
503
504 #[test]
505 fn register_via_runtime_context_resolves_webp_fourcc_tag() {
506 use oxideav_core::ProbeContext;
510 let mut ctx = RuntimeContext::new();
511 register(&mut ctx);
512 let tag = CodecTag::fourcc(b"WEBP");
513 let id = ctx
514 .codecs
515 .resolve_tag_ref(&ProbeContext::new(&tag))
516 .expect("WEBP fourcc resolves to a registered codec");
517 assert_eq!(id.as_str(), CODEC_ID_STR);
518 }
519
520 #[test]
521 fn first_decoder_returns_a_webp_decoder() {
522 let mut ctx = RuntimeContext::new();
523 register(&mut ctx);
524 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
525 let dec = ctx
526 .codecs
527 .first_decoder(¶ms)
528 .expect("webp decoder factory");
529 assert_eq!(dec.codec_id().as_str(), CODEC_ID_STR);
530 }
531
532 #[test]
533 fn end_to_end_lossless_decode_via_runtime_context() {
534 let mut ctx = RuntimeContext::new();
537 register(&mut ctx);
538 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
539 let mut dec = ctx
540 .codecs
541 .first_decoder(¶ms)
542 .expect("webp decoder factory");
543
544 let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
545 dec.send_packet(&pkt).expect("send_packet accepts file");
546 let frame = dec.receive_frame().expect("receive_frame yields a frame");
547 let v = match frame {
548 Frame::Video(v) => v,
549 other => panic!("expected Frame::Video, got {other:?}"),
550 };
551 assert_eq!(v.planes.len(), 1, "RGBA is a single interleaved plane");
552 assert_eq!(v.planes[0].stride, 4, "1px-wide × 4 bytes/pixel");
553 assert_eq!(v.planes[0].data.len(), 4, "1×1 image × 4 bytes/pixel");
554 assert_eq!(v.planes[0].data, [0xB4, 0x3C, 0x5A, 0xFF]);
557
558 let again = dec.receive_frame();
560 assert!(matches!(again, Err(CoreError::NeedMore)));
561 }
562
563 #[test]
564 fn vp8_lossy_packet_decodes_via_registered_decoder() {
565 let mut ctx = RuntimeContext::new();
570 register(&mut ctx);
571 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
572 let mut dec = ctx
573 .codecs
574 .first_decoder(¶ms)
575 .expect("webp decoder factory");
576 let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSY_1X1.to_vec());
577 dec.send_packet(&pkt).expect("send_packet accepts file");
578 let frame = dec
579 .receive_frame()
580 .expect("VP8 lossy now decodes via oxideav-vp8");
581 let v = match frame {
582 Frame::Video(v) => v,
583 other => panic!("expected Frame::Video, got {other:?}"),
584 };
585 assert_eq!(v.planes.len(), 1, "RGBA is a single interleaved plane");
586 assert_eq!(v.planes[0].data.len(), 4, "1×1 image × 4 bytes/pixel");
587 assert_eq!(v.planes[0].data[3], 0xff);
589 let again = dec.receive_frame();
591 assert!(matches!(again, Err(CoreError::NeedMore)));
592 }
593
594 #[test]
595 fn decoder_params_carry_dims_and_pixel_format_after_first_frame() {
596 let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
600 assert_eq!(dec.params().pixel_format, Some(PixelFormat::Rgba));
601 assert_eq!(dec.params().width, None);
602 assert_eq!(dec.params().height, None);
603
604 let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
607 dec.send_packet(&pkt).unwrap();
608 let _ = dec.receive_frame().expect("decodes");
609 assert_eq!(dec.params().width, Some(1));
610 assert_eq!(dec.params().height, Some(1));
611 assert_eq!(dec.params().pixel_format, Some(PixelFormat::Rgba));
612 assert_eq!(dec.params().codec_id.as_str(), CODEC_ID_STR);
615 assert_eq!(dec.params().media_type, MediaType::Video);
616 }
617
618 #[test]
619 fn double_send_packet_without_receive_is_rejected() {
620 let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
624 let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
625 dec.send_packet(&pkt).unwrap();
626 let err = dec
627 .send_packet(&pkt)
628 .expect_err("second send_packet without receive_frame must fail");
629 let s = err.to_string();
633 assert!(
634 s.contains("receive_frame"),
635 "error message should mention receive_frame: {s}"
636 );
637 }
638
639 #[test]
640 fn flush_then_receive_with_no_pending_returns_eof() {
641 let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
642 dec.flush().unwrap();
643 let err = dec
644 .receive_frame()
645 .expect_err("post-flush, no pending packet → Eof");
646 assert!(matches!(err, CoreError::Eof));
647 }
648
649 #[test]
650 fn decode_webp_to_frame_returns_rgba_video_frame() {
651 let frame = decode_webp_to_frame(LOSSLESS_1X1, Some(123)).expect("decodes");
654 assert_eq!(frame.pts, Some(123));
655 assert_eq!(frame.planes.len(), 1);
656 assert_eq!(frame.planes[0].stride, 4);
657 assert_eq!(frame.planes[0].data, [0xB4, 0x3C, 0x5A, 0xFF]);
658 }
659
660 #[test]
661 fn unsupported_error_conversion_maps_to_core_unsupported() {
662 let lossy: CoreError = Error::Unsupported(UnsupportedKind::LossyVp8).into();
664 assert!(matches!(lossy, CoreError::Unsupported(_)));
665 let none: CoreError = Error::Unsupported(UnsupportedKind::NoImageData).into();
666 assert!(matches!(none, CoreError::Unsupported(_)));
667 }
668
669 fn rgba_frame(width: u32, height: u32, fill: impl Fn(u32, u32) -> [u8; 4]) -> Frame {
673 let mut data = Vec::with_capacity((width * height * 4) as usize);
674 for y in 0..height {
675 for x in 0..width {
676 data.extend_from_slice(&fill(x, y));
677 }
678 }
679 Frame::Video(VideoFrame {
680 pts: Some(0),
681 planes: vec![VideoPlane {
682 stride: (width * 4) as usize,
683 data,
684 }],
685 })
686 }
687
688 fn vp8l_params(width: u32, height: u32, pix: PixelFormat) -> CodecParameters {
689 let mut p = CodecParameters::video(CodecId::new(CODEC_ID_VP8L));
690 p.width = Some(width);
691 p.height = Some(height);
692 p.pixel_format = Some(pix);
693 p
694 }
695
696 #[test]
697 fn register_installs_vp8l_encoder_factory() {
698 let mut ctx = RuntimeContext::new();
699 register(&mut ctx);
700 let id = CodecId::new(CODEC_ID_VP8L);
701 assert!(
702 ctx.codecs.has_encoder(&id),
703 "webp_vp8l encoder factory not installed"
704 );
705 assert!(
706 ctx.codecs.has_decoder(&id),
707 "webp_vp8l decoder factory not installed"
708 );
709 }
710
711 #[test]
712 fn vp8l_encoder_round_trips_rgba_through_registry() {
713 let (w, h) = (4u32, 3u32);
716 let frame = rgba_frame(w, h, |x, y| {
717 [(x * 40) as u8, (y * 60) as u8, ((x + y) * 25) as u8, 0xff]
718 });
719
720 let mut ctx = RuntimeContext::new();
721 register(&mut ctx);
722 let mut enc = ctx
723 .codecs
724 .first_encoder(&vp8l_params(w, h, PixelFormat::Rgba))
725 .expect("webp_vp8l encoder factory");
726 enc.send_frame(&frame).expect("send_frame");
727 let pkt = enc.receive_packet().expect("one packet out");
728
729 let img = crate::decode_webp(&pkt.data).expect("decode our own webp");
730 assert_eq!(img.frames.len(), 1);
731 assert_eq!(img.frames[0].width, w);
732 assert_eq!(img.frames[0].height, h);
733 let Frame::Video(v) = &frame else {
735 unreachable!()
736 };
737 assert_eq!(img.frames[0].rgba, v.planes[0].data);
738 }
739
740 #[test]
741 fn vp8l_encoder_streams_rgb24_as_opaque() {
742 let (w, h) = (3u32, 2u32);
745 let mut data = Vec::new();
746 for y in 0..h {
747 for x in 0..w {
748 data.extend_from_slice(&[(x * 50) as u8, (y * 70) as u8, 0x33]);
749 }
750 }
751 let frame = Frame::Video(VideoFrame {
752 pts: Some(0),
753 planes: vec![VideoPlane {
754 stride: (w * 3) as usize,
755 data,
756 }],
757 });
758
759 let mut enc =
760 make_encoder(&vp8l_params(w, h, PixelFormat::Rgb24)).expect("make_encoder rgb24");
761 enc.send_frame(&frame).unwrap();
762 let pkt = enc.receive_packet().unwrap();
763
764 let c = crate::parse_container(&pkt.data).unwrap();
766 assert!(c
767 .first_chunk_with_fourcc(crate::container::fourcc::VP8X)
768 .is_none());
769 let img = crate::decode_webp(&pkt.data).unwrap();
770 for px in img.frames[0].rgba.chunks_exact(4) {
772 assert_eq!(px[3], 0xff);
773 }
774 }
775
776 #[test]
777 fn vp8l_encoder_with_metadata_promotes_to_vp8x() {
778 let (w, h) = (2u32, 2u32);
779 let frame = rgba_frame(w, h, |x, _| [(x * 100) as u8, 0x10, 0x20, 0x80]);
780
781 let meta = WebpMetadataOwned {
782 icc: Some(b"icc-profile".to_vec()),
783 exif: Some(b"Exif\x00\x00II".to_vec()),
784 xmp: None,
785 };
786 let mut enc = make_encoder_with_metadata(&vp8l_params(w, h, PixelFormat::Rgba), meta)
787 .expect("make_encoder_with_metadata");
788 enc.send_frame(&frame).unwrap();
789 let pkt = enc.receive_packet().unwrap();
790
791 let c = crate::parse_container(&pkt.data).unwrap();
793 assert!(c
794 .first_chunk_with_fourcc(crate::container::fourcc::VP8X)
795 .is_some());
796 let read = crate::extract_metadata(&pkt.data).unwrap();
797 assert_eq!(read.icc.as_deref(), Some(&b"icc-profile"[..]));
798 assert_eq!(read.exif.as_deref(), Some(&b"Exif\x00\x00II"[..]));
799 assert_eq!(read.xmp, None);
800
801 let img = crate::decode_webp(&pkt.data).unwrap();
803 let Frame::Video(v) = &frame else {
804 unreachable!()
805 };
806 assert_eq!(img.frames[0].rgba, v.planes[0].data);
807 }
808
809 #[test]
810 fn vp8l_encoder_receive_before_send_is_need_more() {
811 let mut enc = make_encoder(&vp8l_params(1, 1, PixelFormat::Rgba)).unwrap();
812 assert!(matches!(enc.receive_packet(), Err(CoreError::NeedMore)));
813 enc.flush().unwrap();
814 assert!(matches!(enc.receive_packet(), Err(CoreError::Eof)));
815 }
816}