[rust png] Implement `SkPngRustCodec::onGetRepetitionCount`.

This CL exposes `png::AnimationControl` over FFI, and then uses it to
implement `SkPngRustCodec::onGetRepetitionCount`.  After this CL, the
`RustEnabled/AnimatedPNGTests.repetitionCountTest/0` test starts to pass
(after patching in https://crrev.com/c/5786777 in Chromium).

Bug: chromium:356922876
Change-Id: I2e4b010609f31a860f6356b7552376d84f5d106f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/897176
Auto-Submit: Ɓukasz Anforowicz <lukasza@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/experimental/rust_png/ffi/FFI.rs b/experimental/rust_png/ffi/FFI.rs
index 7b0d5a5..e58380d 100644
--- a/experimental/rust_png/ffi/FFI.rs
+++ b/experimental/rust_png/ffi/FFI.rs
@@ -73,6 +73,9 @@
         ) -> bool;
         fn try_get_gama(self: &Reader, gamma: &mut f32) -> bool;
         unsafe fn try_get_iccp<'a>(self: &'a Reader, iccp: &mut &'a [u8]) -> bool;
+        fn has_actl_chunk(self: &Reader) -> bool;
+        fn get_actl_num_frames(self: &Reader) -> u32;
+        fn get_actl_num_plays(self: &Reader) -> u32;
         fn output_buffer_size(self: &Reader) -> usize;
         fn output_color_type(self: &Reader) -> ColorType;
         fn output_bits_per_component(self: &Reader) -> u8;
@@ -256,6 +259,34 @@
         }
     }
 
+    /// Returns whether the `aCTL` chunk exists.
+    fn has_actl_chunk(&self) -> bool {
+        self.0.info().animation_control.is_some()
+    }
+
+    /// Returns `num_frames` from the `aCTL` chunk.  Panics if there is no
+    /// `aCTL` chunk.
+    ///
+    /// The returned value is equal the number of `fcTL` chunks.  (Note that it
+    /// doesn't count `IDAT` nor `fDAT` chunks.  In particular, if an `fCTL`
+    /// chunk doesn't appear before an `IDAT` chunk then `IDAT` is not part
+    /// of the animation.)
+    ///
+    /// See also
+    /// <https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk>.
+    fn get_actl_num_frames(&self) -> u32 {
+        self.0.info().animation_control.as_ref().unwrap().num_frames
+    }
+
+    /// Returns `num_plays` from the `aCTL` chunk.  Panics if there is no `aCTL`
+    /// chunk.
+    ///
+    /// `0` indicates that the animation should play indefinitely. See
+    /// <https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk>.
+    fn get_actl_num_plays(&self) -> u32 {
+        self.0.info().animation_control.as_ref().unwrap().num_plays
+    }
+
     fn output_buffer_size(&self) -> usize {
         self.0.output_buffer_size()
     }
diff --git a/experimental/rust_png/impl/SkPngRustCodec.cpp b/experimental/rust_png/impl/SkPngRustCodec.cpp
index 9d89b29..0d40457 100644
--- a/experimental/rust_png/impl/SkPngRustCodec.cpp
+++ b/experimental/rust_png/impl/SkPngRustCodec.cpp
@@ -7,6 +7,7 @@
 
 #include "experimental/rust_png/impl/SkPngRustCodec.h"
 
+#include <limits>
 #include <memory>
 #include <utility>
 
@@ -390,6 +391,32 @@
     return false;
 }
 
+int SkPngRustCodec::onGetRepetitionCount() {
+    if (!fReader->has_actl_chunk()) {
+        return 0;
+    }
+
+    uint32_t num_frames = fReader->get_actl_num_frames();
+    if (num_frames <= 1) {
+        return 0;
+    }
+
+    // APNG spec says that "`num_plays` indicates the number of times that this
+    // animation should play; if it is 0, the animation should play
+    // indefinitely."
+    uint32_t num_plays = fReader->get_actl_num_plays();
+    constexpr unsigned int kMaxInt = static_cast<unsigned int>(std::numeric_limits<int>::max());
+    if ((num_plays == 0) || (num_plays > kMaxInt)) {
+        return kRepetitionCountInfinite;
+    }
+
+    // Subtracting 1, because `SkCodec::onGetRepetitionCount` doc comment says
+    // that "This number does not include the first play through of each frame.
+    // For example, a repetition count of 4 means that each frame is played 5
+    // times and then the animation stops."
+    return num_plays - 1;
+}
+
 std::optional<SkSpan<const SkPngCodecBase::PaletteColorEntry>> SkPngRustCodec::onTryGetPlteChunk() {
     if (fReader->output_color_type() != rust_png::ColorType::Indexed) {
         return std::nullopt;
diff --git a/experimental/rust_png/impl/SkPngRustCodec.h b/experimental/rust_png/impl/SkPngRustCodec.h
index 22ec3cd..eeff43e 100644
--- a/experimental/rust_png/impl/SkPngRustCodec.h
+++ b/experimental/rust_png/impl/SkPngRustCodec.h
@@ -24,7 +24,7 @@
 //   Rust)
 // * Skia's `SkSwizzler` and `skcms_Transform` (pixel format and color space
 //   transformations implemented in C++).
-class SkPngRustCodec : public SkPngCodecBase {
+class SkPngRustCodec final : public SkPngCodecBase {
 public:
     static std::unique_ptr<SkPngRustCodec> MakeFromStream(std::unique_ptr<SkStream>, Result*);
 
@@ -71,6 +71,7 @@
                                     const Options&) override;
     Result onIncrementalDecode(int* rowsDecoded) override;
     bool onGetFrameInfo(int, FrameInfo*) const override;
+    int onGetRepetitionCount() override;
 
     // SkPngCodecBase overrides:
     std::optional<SkSpan<const PaletteColorEntry>> onTryGetPlteChunk() override;