Bug 1337111 - Part 4. Add gtests for BlendAnimationFilter. r=tnikkel
authorAndrew Osmond <aosmond@mozilla.com>
Mon, 17 Sep 2018 15:06:29 -0400
changeset 436848 8cd5c4f23ee8ca0ae719c53ee9b7e9229a2de38d
parent 436847 a826a94ae5dd12bfb01edde50dc9a37e4c1e6e3b
child 436849 f8687fe42df8b59eadd9aca296433161b37d9f43
push id34660
push userbtara@mozilla.com
push dateMon, 17 Sep 2018 21:58:52 +0000
treeherdermozilla-central@87a95e1b7ec6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1337111
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1337111 - Part 4. Add gtests for BlendAnimationFilter. r=tnikkel
image/test/gtest/TestBlendAnimationFilter.cpp
image/test/gtest/TestDecoders.cpp
image/test/gtest/moz.build
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestBlendAnimationFilter.cpp
@@ -0,0 +1,504 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/gfx/2D.h"
+#include "skia/include/core/SkColorPriv.h" // for SkPMSrcOver
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfaceFilters.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+static already_AddRefed<Decoder>
+CreateTrivialBlendingDecoder()
+{
+  gfxPrefs::GetSingleton();
+  DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+  DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION;
+  SurfaceFlags surfaceFlags = DefaultSurfaceFlags();
+  auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+  return DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer,
+                                                Nothing(), decoderFlags,
+                                                surfaceFlags);
+}
+
+template <typename Func> RawAccessFrameRef
+WithBlendAnimationFilter(Decoder* aDecoder,
+                         const AnimationParams& aAnimParams,
+                         const IntSize& aOutputSize,
+                         Func aFunc)
+{
+  DecoderTestHelper decoderHelper(aDecoder);
+
+  if (!aDecoder->HasAnimation()) {
+    decoderHelper.PostIsAnimated(aAnimParams.mTimeout);
+  }
+
+  BlendAnimationConfig blendAnim { aDecoder };
+  SurfaceConfig surfaceSink { aDecoder, aOutputSize, SurfaceFormat::B8G8R8A8,
+                              false, Some(aAnimParams) };
+
+  auto func = [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    aFunc(aDecoder, aFilter);
+  };
+
+  WithFilterPipeline(aDecoder, func, false, blendAnim, surfaceSink);
+
+  RawAccessFrameRef current = aDecoder->GetCurrentFrameRef();
+  if (current) {
+    decoderHelper.PostFrameStop(Opacity::SOME_TRANSPARENCY);
+  }
+
+  return current;
+}
+
+void
+AssertConfiguringBlendAnimationFilterFails(const IntRect& aFrameRect,
+                                           const IntSize& aOutputSize)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams animParams { aFrameRect, FrameTimeout::FromRawMilliseconds(0),
+                               0, BlendMethod::SOURCE, DisposalMethod::KEEP };
+  BlendAnimationConfig blendAnim { decoder };
+  SurfaceConfig surfaceSink { decoder, aOutputSize,
+                              SurfaceFormat::B8G8R8A8, false,
+                              Some(animParams) };
+  AssertConfiguringPipelineFails(decoder, blendAnim, surfaceSink);
+}
+
+TEST(ImageBlendAnimationFilter, BlendFailsForNegativeFrameRect)
+{
+  // A negative frame rect size is disallowed.
+  AssertConfiguringBlendAnimationFilterFails(IntRect(IntPoint(0, 0), IntSize(-1, -1)),
+                                             IntSize(100, 100));
+}
+
+TEST(ImageBlendAnimationFilter, WriteFullFirstFrame)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params { IntRect(0, 0, 100, 100),
+                           FrameTimeout::FromRawMilliseconds(0),
+                           /* aFrameNum */ 0, BlendMethod::SOURCE,
+                           DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)));
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+TEST(ImageBlendAnimationFilter, WritePartialFirstFrame)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params { IntRect(25, 50, 50, 25),
+                           FrameTimeout::FromRawMilliseconds(0),
+                           /* aFrameNum */ 0, BlendMethod::SOURCE,
+                           DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)),
+                                          Nothing(),
+                                          Some(IntRect(25, 50, 50, 25)),
+                                          Some(IntRect(25, 50, 50, 25)));
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+static void
+TestWithBlendAnimationFilterClear(BlendMethod aBlendMethod)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::CLEAR };
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+
+  AnimationParams params2 { IntRect(0, 50, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, aBlendMethod,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Blue().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 10, BGRAColor::Transparent()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 50, 20, BGRAColor::Blue()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 70, 30, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, ClearWithOver)
+{
+  TestWithBlendAnimationFilterClear(BlendMethod::OVER);
+}
+
+TEST(ImageBlendAnimationFilter, ClearWithSource)
+{
+  TestWithBlendAnimationFilterClear(BlendMethod::SOURCE);
+}
+
+TEST(ImageBlendAnimationFilter, KeepWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, KeepWithOver)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::OVER,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor1(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+  EXPECT_EQ(SkPMSrcOver(frameColor1.AsPixel(), frameColor0.AsPixel()),
+            blendedColor.AsPixel());
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithOver)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor1 = BGRAColor::Green();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::OVER,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+  EXPECT_EQ(SkPMSrcOver(frameColor2.AsPixel(), frameColor0.AsPixel()),
+            blendedColor.AsPixel());
+
+  RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor1 = BGRAColor::Green();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor2));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousClearWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0 = BGRAColor::Red();
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 0, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::CLEAR };
+  BGRAColor frameColor1 = BGRAColor::Blue();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 20), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor2 = BGRAColor::Green();
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 90), frame2->GetDirtyRect());
+
+  AnimationParams params3 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 3, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor3 = BGRAColor::Blue();
+  RawAccessFrameRef frame3 =
+    WithBlendAnimationFilter(decoder, params3, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor3.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 90), frame3->GetDirtyRect());
+
+  ASSERT_TRUE(frame3.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame3->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Transparent()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 20, 20, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor3));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, PartialOverlapFrameRect)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(-10, -20, 110, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0 = BGRAColor::Red();
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  RefPtr<SourceSurface> surface = frame0->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 80, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Transparent()));
+}
+
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gtest/gtest.h"
 
 #include "Common.h"
+#include "AnimationSurfaceProvider.h"
 #include "Decoder.h"
 #include "DecoderFactory.h"
 #include "decoders/nsBMPDecoder.h"
 #include "IDecodingTask.h"
 #include "ImageOps.h"
 #include "imgIContainer.h"
 #include "imgITools.h"
 #include "ImageFactory.h"
@@ -201,16 +202,138 @@ CheckDownscaleDuringDecode(const ImageTe
     // small amount of fuzz; this is just the nature of Lanczos downscaling.
     EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 47));
     EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 27));
     EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 47));
     EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 27));
   });
 }
 
+static void
+CheckAnimationDecoderResults(const ImageTestCase& aTestCase,
+                             AnimationSurfaceProvider* aProvider,
+                             Decoder* aDecoder)
+{
+  EXPECT_TRUE(aDecoder->GetDecodeDone());
+  EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR),
+            aDecoder->HasError());
+
+  if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) {
+    return;  // That's all we can check for bad images.
+  }
+
+  // The decoder should get the correct size.
+  IntSize size = aDecoder->Size();
+  EXPECT_EQ(aTestCase.mSize.width, size.width);
+  EXPECT_EQ(aTestCase.mSize.height, size.height);
+
+  if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) {
+    return;
+  }
+
+  // Check the output.
+  AutoTArray<BGRAColor, 2> framePixels;
+  framePixels.AppendElement(BGRAColor::Green());
+  framePixels.AppendElement(BGRAColor(0x7F, 0x7F, 0x7F, 0xFF));
+
+  DrawableSurface drawableSurface(WrapNotNull(aProvider));
+  for (size_t i = 0; i < framePixels.Length(); ++i) {
+    nsresult rv = drawableSurface.Seek(i);
+    EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+    // Check the first frame, all green.
+    RawAccessFrameRef rawFrame = drawableSurface->RawAccessRef();
+    RefPtr<SourceSurface> surface = rawFrame->GetSourceSurface();
+
+    // Verify that the resulting surfaces matches our expectations.
+    EXPECT_TRUE(surface->IsDataSourceSurface());
+    EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::B8G8R8X8 ||
+                surface->GetFormat() == SurfaceFormat::B8G8R8A8);
+    EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize());
+    EXPECT_TRUE(IsSolidColor(surface, framePixels[i],
+                             aTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
+  }
+
+  // Should be no more frames.
+  nsresult rv = drawableSurface.Seek(framePixels.Length());
+  EXPECT_TRUE(NS_FAILED(rv));
+}
+
+template <typename Func>
+static void
+WithSingleChunkAnimationDecode(const ImageTestCase& aTestCase,
+                               Func aResultChecker)
+{
+  // Create an image.
+  RefPtr<Image> image =
+    ImageFactory::CreateAnonymousImage(nsDependentCString(aTestCase.mMimeType));
+  ASSERT_TRUE(!image->HasError());
+
+  NotNull<RefPtr<RasterImage>> rasterImage =
+    WrapNotNull(static_cast<RasterImage*>(image.get()));
+
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+  ASSERT_TRUE(inputStream != nullptr);
+
+  // Figure out how much data we have.
+  uint64_t length;
+  nsresult rv = inputStream->Available(&length);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  // Write the data into a SourceBuffer.
+  NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+  sourceBuffer->ExpectLength(length);
+  rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+  sourceBuffer->Complete(NS_OK);
+
+  // Create a metadata decoder first, because otherwise RasterImage will get
+  // unhappy about finding out the image is animated during a full decode.
+  DecoderType decoderType =
+    DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+  RefPtr<IDecodingTask> task =
+    DecoderFactory::CreateMetadataDecoder(decoderType, rasterImage, sourceBuffer);
+  ASSERT_TRUE(task != nullptr);
+
+  // Run the metadata decoder synchronously.
+  task->Run();
+
+  // Create a decoder.
+  DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION;
+  SurfaceFlags surfaceFlags = DefaultSurfaceFlags();
+  RefPtr<Decoder> decoder =
+    DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+                                           decoderFlags, surfaceFlags);
+  ASSERT_TRUE(decoder != nullptr);
+
+  // Create an AnimationSurfaceProvider which will manage the decoding process
+  // and make this decoder's output available in the surface cache.
+  SurfaceKey surfaceKey =
+    RasterSurfaceKey(aTestCase.mOutputSize, surfaceFlags, PlaybackType::eAnimated);
+  RefPtr<AnimationSurfaceProvider> provider =
+    new AnimationSurfaceProvider(rasterImage,
+                                 surfaceKey,
+                                 WrapNotNull(decoder),
+                                 /* aCurrentFrame */ 0);
+
+  // Run the full decoder synchronously.
+  provider->Run();
+
+  // Call the lambda to verify the expected results.
+  aResultChecker(provider, decoder);
+}
+
+static void
+CheckAnimationDecoderSingleChunk(const ImageTestCase& aTestCase)
+{
+  WithSingleChunkAnimationDecode(aTestCase, [&](AnimationSurfaceProvider* aProvider, Decoder* aDecoder) {
+    CheckAnimationDecoderResults(aTestCase, aProvider, aDecoder);
+  });
+}
+
 class ImageDecoders : public ::testing::Test
 {
 protected:
   AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDecoders, PNGSingleChunk)
 {
@@ -312,26 +435,36 @@ TEST_F(ImageDecoders, AnimatedGIFSingleC
   CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
 }
 
 TEST_F(ImageDecoders, AnimatedGIFMultiChunk)
 {
   CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase());
 }
 
+TEST_F(ImageDecoders, AnimatedGIFWithBlendedFrames)
+{
+  CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
 TEST_F(ImageDecoders, AnimatedPNGSingleChunk)
 {
   CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
 }
 
 TEST_F(ImageDecoders, AnimatedPNGMultiChunk)
 {
   CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase());
 }
 
+TEST_F(ImageDecoders, AnimatedPNGWithBlendedFrames)
+{
+  CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
 TEST_F(ImageDecoders, CorruptSingleChunk)
 {
   CheckDecoderSingleChunk(CorruptTestCase());
 }
 
 TEST_F(ImageDecoders, CorruptMultiChunk)
 {
   CheckDecoderMultiChunk(CorruptTestCase());
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Library('imagetest')
 
 UNIFIED_SOURCES = [
     'Common.cpp',
     'TestADAM7InterpolatingFilter.cpp',
     'TestAnimationFrameBuffer.cpp',
+    'TestBlendAnimationFilter.cpp',
     'TestContainers.cpp',
     'TestCopyOnWrite.cpp',
     'TestDecoders.cpp',
     'TestDecodeToSurface.cpp',
     'TestDeinterlacingFilter.cpp',
     'TestLoader.cpp',
     'TestMetadata.cpp',
     'TestRemoveFrameRectFilter.cpp',