Expose more info in SkCodec::FrameInfo

Bug: b/160984428

Add more fields to SkCodec::FrameInfo, which describes the properties of
an individual frame in an animated image. This allows a client that
wishes to seek to determine frame dependencies so that they can decode
an arbitrary frame, which in turn will allow SkCodec to remove
SkCodec::FrameInfo::fRequiredFrame. Currently, SkCodec seeks through the
stream to determine frame dependencies, but this is unnecessary work
(and storage) for a client that does not want to seek.

These fields also support the proposed APIs in go/animated-ndk.

Move SkCodecAnimation::Blend from SkCodecAnimationPriv (and delete that
file) into SkCodecAnimation.h. Rename its values to be more clear.

Merge common code for populating SkCodec::FrameInfo.

Add a test for a GIF with offsets outside the range of the image. Note
that libwebp rejects such an image.

Update libgifcodec.

Change-Id: Ie27e0531e7d62eaae153eccb3105bf2121b5aac4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/339857
Commit-Queue: Leon Scroggins <scroggo@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Reviewed-by: Nigel Tao <nigeltao@google.com>
diff --git a/DEPS b/DEPS
index fd0e25c..1c7799a 100644
--- a/DEPS
+++ b/DEPS
@@ -24,7 +24,7 @@
   "third_party/externals/harfbuzz"        : "https://chromium.googlesource.com/external/github.com/harfbuzz/harfbuzz.git@3a74ee528255cc027d84b204a87b5c25e47bff79",
   "third_party/externals/icu"             : "https://chromium.googlesource.com/chromium/deps/icu.git@dbd3825b31041d782c5b504c59dcfb5ac7dda08c",
   "third_party/externals/imgui"           : "https://skia.googlesource.com/external/github.com/ocornut/imgui.git@9418dcb69355558f70de260483424412c5ca2fce",
-  "third_party/externals/libgifcodec"     : "https://skia.googlesource.com/libgifcodec@b89e0a4edd6c0158b24730845e0e498969e22a16",
+  "third_party/externals/libgifcodec"     : "https://skia.googlesource.com/libgifcodec@e13b82fac077383d9f93631a177573509be44f38",
   "third_party/externals/libjpeg-turbo"   : "https://chromium.googlesource.com/chromium/deps/libjpeg_turbo.git@64fc43d52351ed52143208ce6a656c03db56462b",
   "third_party/externals/libpng"          : "https://skia.googlesource.com/third_party/libpng.git@386707c6d19b974ca2e3db7f5c61873813c6fe44",
   "third_party/externals/libwebp"         : "https://chromium.googlesource.com/webm/libwebp.git@55a080e50af655d1fbe0a5c22954835cdd59ff92",
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 3dc29b4..c2b4bbd 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -6,6 +6,9 @@
 
 Milestone 89
 ------------
+  * Expose more info in SkCodec::FrameInfo
+    https://review.skia.org/339857
+
   * Added dither control to the SkImageFilters::Shader factory.
     https://review.skia.org/338156
 
diff --git a/include/codec/SkCodec.h b/include/codec/SkCodec.h
index 88fd07b..f756585 100644
--- a/include/codec/SkCodec.h
+++ b/include/codec/SkCodec.h
@@ -645,9 +645,32 @@
         SkAlphaType fAlphaType;
 
         /**
+         *  Whether the updated rectangle contains alpha.
+         *
+         *  This is conservative; it will still be set to true if e.g. a color
+         *  index-based frame has a color with alpha but does not use it. In
+         *  addition, it may be set to true, even if the final frame, after
+         *  blending, is opaque.
+         */
+        bool fHasAlphaWithinBounds;
+
+        /**
          *  How this frame should be modified before decoding the next one.
          */
         SkCodecAnimation::DisposalMethod fDisposalMethod;
+
+        /**
+         *  How this frame should blend with the prior frame.
+         */
+        SkCodecAnimation::Blend fBlend;
+
+        /**
+         *  The rectangle updated by this frame.
+         *
+         *  It may be empty, if the frame does not change the image. It will
+         *  always be contained by SkCodec::dimensions().
+         */
+        SkIRect fFrameRect;
     };
 
     /**
diff --git a/include/codec/SkCodecAnimation.h b/include/codec/SkCodecAnimation.h
index 490c636..c5883e2 100644
--- a/include/codec/SkCodecAnimation.h
+++ b/include/codec/SkCodecAnimation.h
@@ -39,5 +39,23 @@
          */
         kRestorePrevious    = 3,
     };
+
+    /**
+     * How to blend the current frame.
+     */
+    enum class Blend {
+        /**
+         *  Blend with the prior frame as if using SkBlendMode::kSrcOver.
+         */
+        kSrcOver,
+
+        /**
+         *  Blend with the prior frame as if using SkBlendMode::kSrc.
+         *
+         *  This frame's pixels replace the destination pixels.
+         */
+        kSrc,
+    };
+
 } // namespace SkCodecAnimation
 #endif // SkCodecAnimation_DEFINED
diff --git a/resources/images/xOffsetTooBig.gif b/resources/images/xOffsetTooBig.gif
new file mode 100644
index 0000000..bb77163
--- /dev/null
+++ b/resources/images/xOffsetTooBig.gif
Binary files differ
diff --git a/src/codec/SkCodec.cpp b/src/codec/SkCodec.cpp
index 533cef5..98a6846 100644
--- a/src/codec/SkCodec.cpp
+++ b/src/codec/SkCodec.cpp
@@ -765,6 +765,20 @@
     }
 }
 
+void SkFrame::fillIn(SkCodec::FrameInfo* frameInfo, bool fullyReceived) const {
+    SkASSERT(frameInfo);
+
+    frameInfo->fRequiredFrame = fRequiredFrame;
+    frameInfo->fDuration = fDuration;
+    frameInfo->fFullyReceived = fullyReceived;
+    frameInfo->fAlphaType = fHasAlpha ? kUnpremul_SkAlphaType
+                                      : kOpaque_SkAlphaType;
+    frameInfo->fHasAlphaWithinBounds = this->reportedAlpha() != SkEncodedInfo::kOpaque_Alpha;
+    frameInfo->fDisposalMethod = fDisposalMethod;
+    frameInfo->fBlend = fBlend;
+    frameInfo->fFrameRect = fRect;
+}
+
 static bool independent(const SkFrame& frame) {
     return frame.getRequiredFrame() == SkCodec::kNoFrame;
 }
@@ -828,7 +842,7 @@
     }
 
 
-    const bool blendWithPrevFrame = frame->getBlend() == SkCodecAnimation::Blend::kPriorFrame;
+    const bool blendWithPrevFrame = frame->getBlend() == SkCodecAnimation::Blend::kSrcOver;
     if ((!reportsAlpha || !blendWithPrevFrame) && frameRect == screenRect) {
         frame->setHasAlpha(reportsAlpha);
         frame->setRequiredFrame(SkCodec::kNoFrame);  // IND2
diff --git a/src/codec/SkCodecAnimationPriv.h b/src/codec/SkCodecAnimationPriv.h
deleted file mode 100644
index e326919..0000000
--- a/src/codec/SkCodecAnimationPriv.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2016 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#ifndef SkCodecAnimationPriv_DEFINED
-#define SkCodecAnimationPriv_DEFINED
-
-namespace SkCodecAnimation {
-    /**
-     * How to blend the current frame.
-     */
-    enum class Blend {
-        /**
-         *  Blend with the prior frame. This is the typical case, supported
-         *  by all animated image types.
-         */
-        kPriorFrame,
-
-        /**
-         *  Do not blend.
-         *
-         *  This frames pixels overwrite previous pixels "blending" with
-         *  the background color of transparent.
-         */
-        kBG,
-    };
-
-} // namespace SkCodecAnimation
-#endif // SkCodecAnimationPriv_DEFINED
diff --git a/src/codec/SkFrameHolder.h b/src/codec/SkFrameHolder.h
index c44d2e0..7b4031e 100644
--- a/src/codec/SkFrameHolder.h
+++ b/src/codec/SkFrameHolder.h
@@ -8,12 +8,12 @@
 #ifndef SkFrameHolder_DEFINED
 #define SkFrameHolder_DEFINED
 
+#include "include/codec/SkCodec.h"
 #include "include/codec/SkCodecAnimation.h"
 #include "include/core/SkRect.h"
 #include "include/core/SkTypes.h"
 #include "include/private/SkEncodedInfo.h"
 #include "include/private/SkNoncopyable.h"
-#include "src/codec/SkCodecAnimationPriv.h"
 
 /**
  *  Base class for a single frame of an animated image.
@@ -29,7 +29,7 @@
         , fRequiredFrame(kUninitialized)
         , fDisposalMethod(SkCodecAnimation::DisposalMethod::kKeep)
         , fDuration(0)
-        , fBlend(SkCodecAnimation::Blend::kPriorFrame)
+        , fBlend(SkCodecAnimation::Blend::kSrcOver)
     {
         fRect.setEmpty();
     }
@@ -142,6 +142,11 @@
         return fBlend;
     }
 
+    /**
+     * Fill in the FrameInfo with details from this object.
+     */
+    void fillIn(SkCodec::FrameInfo*, bool fullyReceived) const;
+
 protected:
     virtual SkEncodedInfo::Alpha onReportedAlpha() const = 0;
 
diff --git a/src/codec/SkHeifCodec.cpp b/src/codec/SkHeifCodec.cpp
index 1c83481..2a162e8 100644
--- a/src/codec/SkHeifCodec.cpp
+++ b/src/codec/SkHeifCodec.cpp
@@ -348,11 +348,7 @@
     }
 
     if (frameInfo) {
-        frameInfo->fRequiredFrame = SkCodec::kNoFrame;
-        frameInfo->fDuration = frame->getDuration();
-        frameInfo->fFullyReceived = true;
-        frameInfo->fAlphaType = kOpaque_SkAlphaType;
-        frameInfo->fDisposalMethod = SkCodecAnimation::DisposalMethod::kKeep;
+        frame->fillIn(frameInfo, true);
     }
 
     return true;
diff --git a/src/codec/SkWebpCodec.cpp b/src/codec/SkWebpCodec.cpp
index b66d576..e8b1bb4 100644
--- a/src/codec/SkWebpCodec.cpp
+++ b/src/codec/SkWebpCodec.cpp
@@ -12,7 +12,6 @@
 #include "include/core/SkCanvas.h"
 #include "include/private/SkTemplates.h"
 #include "include/private/SkTo.h"
-#include "src/codec/SkCodecAnimationPriv.h"
 #include "src/codec/SkCodecPriv.h"
 #include "src/codec/SkParseEncodedOrigin.h"
 #include "src/codec/SkSampler.h"
@@ -266,7 +265,7 @@
                 SkCodecAnimation::DisposalMethod::kKeep);
         frame->setDuration(iter.duration);
         if (WEBP_MUX_BLEND != iter.blend_method) {
-            frame->setBlend(SkCodecAnimation::Blend::kBG);
+            frame->setBlend(SkCodecAnimation::Blend::kSrc);
         }
         fFrameHolder.setAlphaAndRequiredFrame(frame);
     }
@@ -295,14 +294,9 @@
     }
 
     if (frameInfo) {
-        frameInfo->fRequiredFrame = frame->getRequiredFrame();
-        frameInfo->fDuration = frame->getDuration();
         // libwebp only reports fully received frames for an
         // animated image.
-        frameInfo->fFullyReceived = true;
-        frameInfo->fAlphaType = frame->hasAlpha() ? kUnpremul_SkAlphaType
-                                                  : kOpaque_SkAlphaType;
-        frameInfo->fDisposalMethod = frame->getDisposalMethod();
+        frame->fillIn(frameInfo, true);
     }
 
     return true;
diff --git a/src/codec/SkWuffsCodec.cpp b/src/codec/SkWuffsCodec.cpp
index 4fd2a04..8b828b2 100644
--- a/src/codec/SkWuffsCodec.cpp
+++ b/src/codec/SkWuffsCodec.cpp
@@ -156,7 +156,6 @@
 public:
     SkWuffsFrame(wuffs_base__frame_config* fc);
 
-    SkCodec::FrameInfo frameInfo(bool fullyReceived) const;
     uint64_t           ioPosition() const;
 
     // SkFrame overrides.
@@ -332,18 +331,8 @@
     this->setXYWH(r.min_incl_x, r.min_incl_y, r.width(), r.height());
     this->setDisposalMethod(wuffs_disposal_to_skia_disposal(fc->disposal()));
     this->setDuration(fc->duration() / WUFFS_BASE__FLICKS_PER_MILLISECOND);
-    this->setBlend(fc->overwrite_instead_of_blend() ? SkCodecAnimation::Blend::kBG
-                                                    : SkCodecAnimation::Blend::kPriorFrame);
-}
-
-SkCodec::FrameInfo SkWuffsFrame::frameInfo(bool fullyReceived) const {
-    SkCodec::FrameInfo ret;
-    ret.fRequiredFrame = getRequiredFrame();
-    ret.fDuration = getDuration();
-    ret.fFullyReceived = fullyReceived;
-    ret.fAlphaType = hasAlpha() ? kUnpremul_SkAlphaType : kOpaque_SkAlphaType;
-    ret.fDisposalMethod = getDisposalMethod();
-    return ret;
+    this->setBlend(fc->overwrite_instead_of_blend() ? SkCodecAnimation::Blend::kSrc
+                                                    : SkCodecAnimation::Blend::kSrcOver);
 }
 
 uint64_t SkWuffsFrame::ioPosition() const {
@@ -854,7 +843,7 @@
         return false;
     }
     if (frameInfo) {
-        *frameInfo = f->frameInfo(static_cast<uint64_t>(i) < this->fNumFullyReceivedFrames);
+        f->fillIn(frameInfo, static_cast<uint64_t>(i) < this->fNumFullyReceivedFrames);
     }
     return true;
 }
diff --git a/src/utils/SkAnimCodecPlayer.cpp b/src/utils/SkAnimCodecPlayer.cpp
index e26ce38..0a33616 100644
--- a/src/utils/SkAnimCodecPlayer.cpp
+++ b/src/utils/SkAnimCodecPlayer.cpp
@@ -86,7 +86,7 @@
             // of transparent black, and then draw that through the origin matrix
             // onto the required frame. To do that, SkCodec needs to expose the
             // rectangle of the delta and the blend mode, so we can handle
-            // kRestoreBGColor frames and Blend::kBG.
+            // kRestoreBGColor frames and Blend::kSrc.
             SkMatrix inverse;
             SkAssertResult(originMatrix.invert(&inverse));
             canvas->concat(inverse);
diff --git a/tests/CodecAnimTest.cpp b/tests/CodecAnimTest.cpp
index b0ccb99..8498d88 100644
--- a/tests/CodecAnimTest.cpp
+++ b/tests/CodecAnimTest.cpp
@@ -67,6 +67,31 @@
     return info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kRestorePrevious;
 }
 
+namespace {
+SkString to_string(bool boolean) { return boolean ? SkString("true") : SkString("false"); }
+SkString to_string(SkCodecAnimation::Blend blend) {
+    switch (blend) {
+        case SkCodecAnimation::Blend::kSrcOver:
+            return SkString("kSrcOver");
+        case SkCodecAnimation::Blend::kSrc:
+            return SkString("kSrc");
+        default:
+            return SkString();
+    }
+}
+SkString to_string(SkIRect rect) {
+    return SkStringPrintf("{ %i, %i, %i, %i }", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom);
+}
+
+template <typename T>
+void reporter_assert_equals(skiatest::Reporter* r, const char* name, int i, const char* prop,
+                            T expected, T actual) {
+    REPORTER_ASSERT(r, expected == actual, "%s's frame %i has wrong %s! expected:"
+                    " %s\tactual: %s", name, i, prop, to_string(expected).c_str(),
+                    to_string(actual).c_str());
+}
+} // namespace
+
 DEF_TEST(Codec_frames, r) {
     constexpr int kNoFrame = SkCodec::kNoFrame;
     constexpr SkAlphaType kOpaque = kOpaque_SkAlphaType;
@@ -77,6 +102,8 @@
             SkCodecAnimation::DisposalMethod::kRestoreBGColor;
     constexpr SkCodecAnimation::DisposalMethod kRestorePrev =
             SkCodecAnimation::DisposalMethod::kRestorePrevious;
+    constexpr auto kSrcOver = SkCodecAnimation::Blend::kSrcOver;
+    constexpr auto kSrc     = SkCodecAnimation::Blend::kSrc;
 
     static const struct {
         const char*                                   fName;
@@ -91,13 +118,22 @@
         std::vector<int>                              fDurations;
         int                                           fRepetitionCount;
         std::vector<SkCodecAnimation::DisposalMethod> fDisposalMethods;
+        std::vector<bool>                             fAlphaWithinBounds;
+        std::vector<SkCodecAnimation::Blend>          fBlends;
+        std::vector<SkIRect>                          fFrameRects;
     } gRecs[] = {
         { "images/required.gif", 7,
             { 0, 1, 2, 3, 4, 5 },
             { kOpaque, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul },
             { 100, 100, 100, 100, 100, 100, 100 },
             0,
-            { kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep } },
+            { kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep },
+            { false, true, true, true, true, true, true },
+            { kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver,
+              kSrcOver },
+            { {0, 0, 100, 100}, {0, 0, 75, 75}, {0, 0, 50, 50}, {0, 0, 60, 60},
+              {0, 0, 100, 100}, {0, 0, 50, 50}, {0, 0, 75, 75}},
+          },
         { "images/alphabetAnim.gif", 13,
             { kNoFrame, 0, 0, 0, 0, 5, 6, kNoFrame, kNoFrame, 9, 10, 11 },
             { kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul,
@@ -106,7 +142,16 @@
             0,
             { kKeep, kRestorePrev, kRestorePrev, kRestorePrev, kRestorePrev,
               kRestoreBG, kKeep, kRestoreBG, kRestoreBG, kKeep, kKeep,
-              kRestoreBG, kKeep } },
+              kRestoreBG, kKeep },
+            { true, false, true, false, true, true, true, true, true, true, true, true, true },
+            { kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver,
+              kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver,
+              kSrcOver },
+            { {25, 25, 75, 75}, {25, 25, 75, 75}, {25, 25, 75, 75}, {37, 37, 62, 62},
+              {37, 37, 62, 62}, {25, 25, 75, 75}, {0, 0, 50, 50}, {0, 0, 100, 100},
+              {25, 25, 75, 75}, {25, 25, 75, 75}, {0, 0, 100, 100}, {25, 25, 75, 75},
+              {37, 37, 62, 62}},
+          },
         { "images/randPixelsAnim2.gif", 4,
             // required frames
             { 0, 0, 1 },
@@ -116,7 +161,11 @@
             { 0, 1000, 170, 40 },
             // repetition count
             0,
-            { kKeep, kKeep, kRestorePrev, kKeep } },
+            { kKeep, kKeep, kRestorePrev, kKeep },
+            { false, true, false, false },
+            { kSrcOver, kSrcOver, kSrcOver, kSrcOver },
+            { {0, 0, 8, 8}, {6, 6, 8, 8}, {4, 4, 8, 8}, {7, 0, 8, 8} },
+          },
         { "images/randPixelsAnim.gif", 13,
             // required frames
             { 0, 1, 2, 3, 4, 3, 6, 7, 7, 7, 9, 9 },
@@ -128,41 +177,70 @@
             0,
             { kKeep, kKeep, kKeep, kKeep, kRestoreBG, kRestoreBG, kRestoreBG,
               kRestoreBG, kRestorePrev, kRestoreBG, kRestorePrev, kRestorePrev,
-              kRestorePrev,  } },
-        { "images/box.gif", 1, {}, {}, {}, 0, { kKeep } },
-        { "images/color_wheel.gif", 1, {}, {}, {}, 0, { kKeep } },
+              kRestorePrev,  },
+            { false, true, true, false, true, true, false, false, true, true, false, false,
+              true },
+            { kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver,
+              kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver, kSrcOver,
+              kSrcOver },
+            { {4, 4, 12, 12}, {4, 4, 12, 12}, {4, 4, 12, 12}, {0, 0, 8, 8}, {8, 8, 16, 16},
+              {8, 8, 16, 16}, {8, 8, 16, 16}, {2, 2, 10, 10}, {7, 7, 15, 15}, {7, 7, 15, 15},
+              {7, 7, 15, 15}, {0, 0, 8, 8}, {14, 14, 16, 16} },
+        },
+        { "images/box.gif", 1, {}, {}, {}, 0, { kKeep }, {}, {}, {} },
+        { "images/color_wheel.gif", 1, {}, {}, {}, 0, { kKeep }, {}, {}, {} },
         { "images/test640x479.gif", 4, { 0, 1, 2 },
                 { kOpaque, kOpaque, kOpaque },
                 { 200, 200, 200, 200 },
                 SkCodec::kRepetitionCountInfinite,
-                { kKeep, kKeep, kKeep, kKeep } },
+                { kKeep, kKeep, kKeep, kKeep },
+                { false, true, true, true },
+                { kSrcOver, kSrcOver, kSrcOver, kSrcOver },
+                { {0, 0, 640, 479}, {0, 0, 640, 479}, {0, 0, 640, 479}, {0, 0, 640, 479} },
+        },
         { "images/colorTables.gif", 2, { 0 }, { kOpaque }, { 1000, 1000 }, 5,
-                { kKeep, kKeep } },
+                { kKeep, kKeep }, {false, true}, { kSrcOver, kSrcOver },
+                { {0, 0, 640, 400}, {0, 0, 640, 200}},
+        },
 
-        { "images/arrow.png",  1, {}, {}, {}, 0, {} },
-        { "images/google_chrome.ico", 1, {}, {}, {}, 0, {} },
-        { "images/brickwork-texture.jpg", 1, {}, {}, {}, 0, {} },
+        { "images/arrow.png",  1, {}, {}, {}, 0, {}, {}, {}, {} },
+        { "images/google_chrome.ico", 1, {}, {}, {}, 0, {}, {}, {}, {} },
+        { "images/brickwork-texture.jpg", 1, {}, {}, {}, 0, {}, {}, {}, {} },
 #if defined(SK_CODEC_DECODES_RAW) && (!defined(_WIN32))
-        { "images/dng_with_preview.dng", 1, {}, {}, {}, 0, {} },
+        { "images/dng_with_preview.dng", 1, {}, {}, {}, 0, {}, {}, {}, {} },
 #endif
-        { "images/mandrill.wbmp", 1, {}, {}, {}, 0, {} },
-        { "images/randPixels.bmp", 1, {}, {}, {}, 0, {} },
-        { "images/yellow_rose.webp", 1, {}, {}, {}, 0, {} },
+        { "images/mandrill.wbmp", 1, {}, {}, {}, 0, {}, {}, {}, {} },
+        { "images/randPixels.bmp", 1, {}, {}, {}, 0, {}, {}, {}, {} },
+        { "images/yellow_rose.webp", 1, {}, {}, {}, 0, {}, {}, {}, {} },
         { "images/stoplight.webp", 3, { 0, 1 }, { kOpaque, kOpaque },
             { 1000, 500, 1000 }, SkCodec::kRepetitionCountInfinite,
-            { kKeep, kKeep, kKeep } },
+            { kKeep, kKeep, kKeep }, {false, false, false},
+            {kSrcOver, kSrcOver, kSrcOver},
+            { {0, 0, 11, 29}, {2, 10, 9, 27}, {2, 2, 9, 18}},
+        },
         { "images/blendBG.webp", 7,
             { 0, kNoFrame, kNoFrame, kNoFrame, 4, 4 },
             { kOpaque, kOpaque, kUnpremul, kOpaque, kUnpremul, kUnpremul },
             { 525, 500, 525, 437, 609, 729, 444 },
             6,
-            { kKeep, kKeep, kKeep, kKeep, kKeep, kKeep, kKeep } },
+            { kKeep, kKeep, kKeep, kKeep, kKeep, kKeep, kKeep },
+            { false, true, false, true, false, true, true },
+            { kSrc, kSrcOver, kSrc, kSrc, kSrc, kSrc, kSrc },
+            { {0, 0, 200, 200}, {0, 0, 200, 200}, {0, 0, 200, 200}, {0, 0, 200, 200},
+              {0, 0, 200, 200}, {100, 100, 200, 200}, {100, 100, 200, 200} },
+        },
         { "images/required.webp", 7,
             { 0, 1, 1, kNoFrame, 4, 4 },
             { kOpaque, kUnpremul, kUnpremul, kOpaque, kOpaque, kOpaque },
             { 100, 100, 100, 100, 100, 100, 100 },
             0,
-            { kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep } },
+            { kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep },
+            { false, false, false, false, false, false, false },
+            { kSrc, kSrcOver, kSrcOver, kSrcOver, kSrc, kSrcOver,
+              kSrcOver },
+            { {0, 0, 100, 100}, {0, 0, 75, 75}, {0, 0, 50, 50}, {0, 0, 60, 60},
+              {0, 0, 100, 100}, {0, 0, 50, 50}, {0, 0, 75, 75}},
+          },
     };
 
     for (const auto& rec : gRecs) {
@@ -296,6 +374,16 @@
                 }
 
                 REPORTER_ASSERT(r, frameInfo.fDisposalMethod == rec.fDisposalMethods[i]);
+
+                reporter_assert_equals<bool>(r, rec.fName, i, "alpha within bounds",
+                                             rec.fAlphaWithinBounds[i],
+                                             frameInfo.fHasAlphaWithinBounds);
+
+                reporter_assert_equals(r, rec.fName, i, "blend mode", rec.fBlends[i],
+                                       frameInfo.fBlend);
+
+                reporter_assert_equals(r, rec.fName, i, "frame rect", rec.fFrameRects[i],
+                                       frameInfo.fFrameRect);
             }
 
             if (TestMode::kIndividual == mode) {
diff --git a/tests/GifTest.cpp b/tests/GifTest.cpp
index 12d8108..7ae65a5 100644
--- a/tests/GifTest.cpp
+++ b/tests/GifTest.cpp
@@ -601,3 +601,46 @@
         }
     }
 }
+
+// This test verifies that a GIF frame outside the image dimensions is handled
+// as desired:
+// - The image reports a size of 0 x 0, but the first frame is 100 x 90. The
+// image (or "canvas") is expanded to fit the first frame. The first frame is red.
+// - The second frame is a green 75 x 75 rectangle, reporting its x-offset and
+// y-offset to be 105, placing it off screen. The decoder interprets this as no
+// change from the first frame.
+DEF_TEST(Codec_xOffsetTooBig, r) {
+    const char* path = "images/xOffsetTooBig.gif";
+    auto data = GetResourceAsData(path);
+    if (!data) {
+        ERRORF(r, "failed to find %s", path);
+        return;
+    }
+
+    auto codec = SkCodec::MakeFromData(std::move(data));
+    if (!codec) {
+        ERRORF(r, "Could not create codec from %s", path);
+        return;
+    }
+
+    REPORTER_ASSERT(r, codec->getFrameCount() == 2);
+
+    auto info = codec->getInfo();
+    REPORTER_ASSERT(r, info.width() == 100 && info.height() == 90);
+
+    SkBitmap bm;
+    bm.allocPixels(info);
+    for (int i = 0; i < 2; i++) {
+        SkCodec::FrameInfo frameInfo;
+        REPORTER_ASSERT(r, codec->getFrameInfo(i, &frameInfo));
+
+        SkIRect expectedRect = i == 0 ? SkIRect{0, 0, 100, 90} : SkIRect{100, 90, 100, 90};
+        REPORTER_ASSERT(r, expectedRect == frameInfo.fFrameRect);
+
+        SkCodec::Options options;
+        options.fFrameIndex = i;
+        REPORTER_ASSERT(r, SkCodec::kSuccess == codec->getPixels(bm.pixmap(), &options));
+
+        REPORTER_ASSERT(r, bm.getColor(0, 0) == SK_ColorRED);
+    }
+}