Repository: image-rs/image (language: rust)

Issue:
WebPDecoder.into_frames() always returns an error at the end of the file instead of ending iteration normally
Broken in 0.25.x and git head, unsure about when it started since I was hoping to replace the webp-animation crate with this, but it's probably been unused and broken for a while.

## Expected

Frames iterator returns None after the last frame.

## Actual behaviour

Returns either a `DecodingError { format: Exact(WebP), underlying: Some(NoMoreFrames) }` or an IO Error from a BufReader as it tries to read past the end of the file.

## Reproduction steps

Call `into_frames` on an WebPDecoder for an animated image. Here's a test that runs on your own test data.

```
#[test]
fn webp_decode() {
    let decoder = WebPDecoder::new(std::io::Cursor::new(
        std::fs::read("tests/images/webp/extended_images/anim.webp").unwrap(),
    ))
    .unwrap();
    let frames = decoder.into_frames();
    for result in frames {
        result.unwrap();
    }
}
```

```
thread 'webp_decode' panicked at tests/limits_anim.rs:20:16:
called `Result::unwrap()` on an `Err` value: Decoding(DecodingError { format: Exact(WebP), underlying: Some(NoMoreFrames) })
```


Discussion / hints:
Probably happened in the switch to using `image-webp`. The fix should be to call [`num_frames`](https://docs.rs/image-webp/latest/image_webp/struct.WebPDecoder.html#method.num_frames) at the start and then return None once that many frames have been decoded. Or maybe once `num_frames * loop_count` many frames have been decoded 

Fix patch:
diff --git a/src/codecs/webp/decoder.rs b/src/codecs/webp/decoder.rs
index 7dd174346a..dd148a820b 100644
--- a/src/codecs/webp/decoder.rs
+++ b/src/codecs/webp/decoder.rs
@@ -68,11 +68,16 @@ impl<'a, R: 'a + Read + Seek> AnimationDecoder<'a> for WebPDecoder<R> {
     fn into_frames(self) -> Frames<'a> {
         struct FramesInner<R: Read + Seek> {
             decoder: WebPDecoder<R>,
+            current: u32,
         }
         impl<R: Read + Seek> Iterator for FramesInner<R> {
             type Item = ImageResult<Frame>;
 
             fn next(&mut self) -> Option<Self::Item> {
+                if self.current == self.decoder.inner.num_frames() {
+                    return None;
+                }
+                self.current += 1;
                 let (width, height) = self.decoder.inner.dimensions();
 
                 let (img, delay) = if self.decoder.inner.has_alpha() {
@@ -98,7 +103,10 @@ impl<'a, R: 'a + Read + Seek> AnimationDecoder<'a> for WebPDecoder<R> {
             }
         }
 
-        Frames::new(Box::new(FramesInner { decoder: self }))
+        Frames::new(Box::new(FramesInner {
+            decoder: self,
+            current: 0,
+        }))
     }
 }
 


Test patch:
diff --git a/tests/regression.rs b/tests/regression.rs
index 32cbdb6e73..a86fa220dc 100644
--- a/tests/regression.rs
+++ b/tests/regression.rs
@@ -1,4 +1,11 @@
-use std::{fs::File, io::BufReader, path::PathBuf};
+use std::{
+    fs::{self, File},
+    io::{BufReader, Cursor},
+    path::PathBuf,
+};
+
+#[cfg(feature = "webp")]
+use image::{codecs::webp::WebPDecoder, AnimationDecoder};
 
 const BASE_PATH: [&str; 2] = [".", "tests"];
 const IMAGE_DIR: &str = "images";
@@ -37,6 +44,30 @@ fn check_regressions() {
         let _ = image::open(path);
     })
 }
+/// Check that the WEBP frames iterator returns the right amount of frames.
+#[test]
+#[cfg(feature = "webp")]
+fn check_webp_frames_regressions() {
+    let path: PathBuf = BASE_PATH
+        .iter()
+        .collect::<PathBuf>()
+        .join(IMAGE_DIR)
+        .join("webp/extended_images")
+        .join("*.webp");
+    let pattern = &*format!("{}", path.display());
+    for path in glob::glob(pattern).unwrap().filter_map(Result::ok) {
+        let bytes = fs::read(path).unwrap();
+        let cursor = Cursor::new(&bytes);
+        let frame_count = image_webp::WebPDecoder::new(cursor.clone())
+            .unwrap()
+            .num_frames() as usize;
+        let decoder = WebPDecoder::new(cursor).unwrap();
+        // The `take` guards against a potentially infinitely running iterator.
+        // Since we take `frame_count + 1`, we can assume that the last iteration already returns `None`.
+        let actual_frame_count = decoder.into_frames().take(frame_count + 1).count();
+        assert_eq!(actual_frame_count, frame_count);
+    }
+}
 
 /// Check that BMP files with large values could cause OOM issues are rejected.
 ///