Bug 1246851 (Part 4) - Add a test suite for SurfacePipes and SurfaceFilters. r=njn
authorSeth Fowler <mark.seth.fowler@gmail.com>
Thu, 25 Feb 2016 16:21:29 -0800
changeset 321991 f5e9eb7e2244ee2d0b41e0129e45acd642a7b4dd
parent 321990 db31b23b3652f29a4c136c188379b6e4d3ae0684
child 321992 307fa388360994c5515511320df3f2cd6085f3b0
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnjn
bugs1246851
milestone47.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 1246851 (Part 4) - Add a test suite for SurfacePipes and SurfaceFilters. r=njn
image/decoders/moz.build
image/test/gtest/Common.cpp
image/test/gtest/Common.h
image/test/gtest/TestDecodeToSurface.cpp
image/test/gtest/TestDecoders.cpp
image/test/gtest/TestDeinterlacingFilter.cpp
image/test/gtest/TestDownscalingFilter.cpp
image/test/gtest/TestDownscalingFilterNoSkia.cpp
image/test/gtest/TestRemoveFrameRectFilter.cpp
image/test/gtest/TestSurfacePipeIntegration.cpp
image/test/gtest/TestSurfaceSink.cpp
image/test/gtest/moz.build
--- a/image/decoders/moz.build
+++ b/image/decoders/moz.build
@@ -27,14 +27,20 @@ UNIFIED_SOURCES += [
     'nsBMPDecoder.cpp',
     'nsGIFDecoder2.cpp',
     'nsICODecoder.cpp',
     'nsIconDecoder.cpp',
     'nsJPEGDecoder.cpp',
     'nsPNGDecoder.cpp',
 ]
 
-# Decoders need RasterImage.h
+include('/ipc/chromium/chromium-config.mozbuild')
+
 LOCAL_INCLUDES += [
+    # Access to Skia headers for Downscaler.
+    '/gfx/2d',
+    # Decoders need ImageLib headers.
     '/image',
 ]
 
+LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES']
+
 FINAL_LIBRARY = 'xul'
--- a/image/test/gtest/Common.cpp
+++ b/image/test/gtest/Common.cpp
@@ -1,36 +1,36 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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 "Common.h"
 
 #include <cstdlib>
-#include "gtest/gtest.h"
 
 #include "nsDirectoryServiceDefs.h"
 #include "nsIDirectoryService.h"
 #include "nsIFile.h"
 #include "nsIInputStream.h"
 #include "nsIProperties.h"
 #include "nsNetUtil.h"
 #include "mozilla/RefPtr.h"
 #include "nsStreamUtils.h"
 #include "nsString.h"
 
 namespace mozilla {
+namespace image {
 
 using namespace gfx;
 
 using std::abs;
 
 ///////////////////////////////////////////////////////////////////////////////
-// Helpers
+// General Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 // These macros work like gtest's ASSERT_* macros, except that they can be used
 // in functions that return values.
 #define ASSERT_TRUE_OR_RETURN(e, rv) \
   EXPECT_TRUE(e);                    \
   if (!(e)) {                        \
     return rv;                       \
@@ -80,51 +80,337 @@ LoadFile(const char* aRelativePath)
     ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr);
     inputStream = bufStream;
   }
 
   return inputStream.forget();
 }
 
 bool
-IsSolidColor(SourceSurface* aSurface, BGRAColor aColor, bool aFuzzy)
+IsSolidColor(SourceSurface* aSurface,
+             BGRAColor aColor,
+             uint8_t aFuzz /* = 0 */)
+{
+  IntSize size = aSurface->GetSize();
+  return RectIsSolidColor(aSurface, IntRect(0, 0, size.width, size.height),
+                          aColor, aFuzz);
+}
+
+bool
+RowsAreSolidColor(SourceSurface* aSurface,
+                  int32_t aStartRow,
+                  int32_t aRowCount,
+                  BGRAColor aColor,
+                  uint8_t aFuzz /* = 0 */)
 {
+  IntSize size = aSurface->GetSize();
+  return RectIsSolidColor(aSurface, IntRect(0, aStartRow, size.width, aRowCount),
+                          aColor, aFuzz);
+}
+
+bool
+RectIsSolidColor(SourceSurface* aSurface,
+                 const IntRect& aRect,
+                 BGRAColor aColor,
+                 uint8_t aFuzz /* = 0 */)
+{
+  IntSize surfaceSize = aSurface->GetSize();
+  IntRect rect =
+    aRect.Intersect(IntRect(0, 0, surfaceSize.width, surfaceSize.height));
+
   RefPtr<DataSourceSurface> dataSurface = aSurface->GetDataSurface();
   ASSERT_TRUE_OR_RETURN(dataSurface != nullptr, false);
 
-  ASSERT_EQ_OR_RETURN(dataSurface->Stride(), aSurface->GetSize().width * 4,
-                      false);
+  ASSERT_EQ_OR_RETURN(dataSurface->Stride(), surfaceSize.width * 4, false);
 
   DataSourceSurface::ScopedMap mapping(dataSurface,
                                        DataSourceSurface::MapType::READ);
   ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false);
 
   uint8_t* data = dataSurface->GetData();
   ASSERT_TRUE_OR_RETURN(data != nullptr, false);
 
-  int32_t length = dataSurface->Stride() * aSurface->GetSize().height;
-  for (int32_t i = 0 ; i < length ; i += 4) {
-    if (aFuzzy) {
-      ASSERT_LE_OR_RETURN(abs(aColor.mBlue - data[i + 0]), 1, false);
-      ASSERT_LE_OR_RETURN(abs(aColor.mGreen - data[i + 1]), 1, false);
-      ASSERT_LE_OR_RETURN(abs(aColor.mRed - data[i + 2]), 1, false);
-      ASSERT_LE_OR_RETURN(abs(aColor.mAlpha - data[i + 3]), 1, false);
-    } else {
-      ASSERT_EQ_OR_RETURN(aColor.mBlue,  data[i + 0], false);
-      ASSERT_EQ_OR_RETURN(aColor.mGreen, data[i + 1], false);
-      ASSERT_EQ_OR_RETURN(aColor.mRed,   data[i + 2], false);
-      ASSERT_EQ_OR_RETURN(aColor.mAlpha, data[i + 3], false);
+  int32_t rowLength = dataSurface->Stride();
+  for (int32_t row = rect.y; row < rect.YMost(); ++row) {
+    for (int32_t col = rect.x; col < rect.XMost(); ++col) {
+      int32_t i = row * rowLength + col * 4;
+      if (aFuzz != 0) {
+        ASSERT_LE_OR_RETURN(abs(aColor.mBlue - data[i + 0]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(aColor.mGreen - data[i + 1]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(aColor.mRed - data[i + 2]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(aColor.mAlpha - data[i + 3]), aFuzz, false);
+      } else {
+        ASSERT_EQ_OR_RETURN(aColor.mBlue,  data[i + 0], false);
+        ASSERT_EQ_OR_RETURN(aColor.mGreen, data[i + 1], false);
+        ASSERT_EQ_OR_RETURN(aColor.mRed,   data[i + 2], false);
+        ASSERT_EQ_OR_RETURN(aColor.mAlpha, data[i + 3], false);
+      }
     }
   }
 
   return true;
 }
 
 
 ///////////////////////////////////////////////////////////////////////////////
+// SurfacePipe Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+already_AddRefed<Decoder>
+CreateTrivialDecoder()
+{
+  gfxPrefs::GetSingleton();
+  DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+  RefPtr<SourceBuffer> sourceBuffer = new SourceBuffer();
+  RefPtr<Decoder> decoder =
+    DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer,
+                                           DefaultSurfaceFlags());
+  return decoder.forget();
+}
+
+void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter,
+                                     const gfx::IntRect& aInputSpaceRect,
+                                     const gfx::IntRect& aOutputSpaceRect)
+{
+  EXPECT_TRUE(aFilter->IsSurfaceFinished());
+  Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isSome());
+  EXPECT_EQ(aInputSpaceRect, invalidRect->mInputSpaceRect);
+  EXPECT_EQ(aOutputSpaceRect, invalidRect->mOutputSpaceRect);
+}
+
+void
+CheckGeneratedImage(Decoder* aDecoder,
+                    const IntRect& aRect,
+                    uint8_t aFuzz /* = 0 */)
+{
+  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+  RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+  const IntSize surfaceSize = surface->GetSize();
+
+  // This diagram shows how the surface is divided into regions that the code
+  // below tests for the correct content. The output rect is the bounds of the
+  // region labeled 'C'.
+  //
+  // +---------------------------+
+  // |             A             |
+  // +---------+--------+--------+
+  // |    B    |   C    |   D    |
+  // +---------+--------+--------+
+  // |             E             |
+  // +---------------------------+
+
+  // Check that the output rect itself is green. (Region 'C'.)
+  EXPECT_TRUE(RectIsSolidColor(surface, aRect, BGRAColor::Green(), aFuzz));
+
+  // Check that the area above the output rect is transparent. (Region 'A'.)
+  EXPECT_TRUE(RectIsSolidColor(surface,
+                               IntRect(0, 0, surfaceSize.width, aRect.y),
+                               BGRAColor::Transparent(), aFuzz));
+
+  // Check that the area to the left of the output rect is transparent. (Region 'B'.)
+  EXPECT_TRUE(RectIsSolidColor(surface,
+                               IntRect(0, aRect.y, aRect.x, aRect.YMost()),
+                               BGRAColor::Transparent(), aFuzz));
+
+  // Check that the area to the right of the output rect is transparent. (Region 'D'.)
+  const int32_t widthOnRight = surfaceSize.width - aRect.XMost();
+  EXPECT_TRUE(RectIsSolidColor(surface,
+                               IntRect(aRect.XMost(), aRect.y, widthOnRight, aRect.YMost()),
+                               BGRAColor::Transparent(), aFuzz));
+
+  // Check that the area below the output rect is transparent. (Region 'E'.)
+  const int32_t heightBelow = surfaceSize.height - aRect.YMost();
+  EXPECT_TRUE(RectIsSolidColor(surface,
+                               IntRect(0, aRect.YMost(), surfaceSize.width, heightBelow),
+                               BGRAColor::Transparent(), aFuzz));
+}
+
+template <typename Func> void
+CheckSurfacePipeWrite(Decoder* aDecoder,
+                      SurfaceFilter* aFilter,
+                      Maybe<IntRect> aOutputRect,
+                      Maybe<IntRect> aInputRect,
+                      Maybe<IntRect> aInputWriteRect,
+                      Maybe<IntRect> aOutputWriteRect,
+                      uint8_t aFuzz,
+                      Func aFunc)
+{
+  IntRect outputRect = aOutputRect.valueOr(IntRect(0, 0, 100, 100));
+  IntRect inputRect = aInputRect.valueOr(IntRect(0, 0, 100, 100));
+  IntRect inputWriteRect = aInputWriteRect.valueOr(inputRect);
+  IntRect outputWriteRect = aOutputWriteRect.valueOr(outputRect);
+
+  // Fill the image.
+  int32_t count = 0;
+  auto result = aFunc(count);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_EQ(inputWriteRect.width * inputWriteRect.height, count);
+
+  AssertCorrectPipelineFinalState(aFilter, inputRect, outputRect);
+
+  // Attempt to write more data and make sure nothing changes.
+  const int32_t oldCount = count;
+  result = aFunc(count);
+  EXPECT_EQ(oldCount, count);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_TRUE(aFilter->IsSurfaceFinished());
+  Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Attempt to advance to the next row and make sure nothing changes.
+  aFilter->AdvanceRow();
+  EXPECT_TRUE(aFilter->IsSurfaceFinished());
+  invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Check that the generated image is correct.
+  CheckGeneratedImage(aDecoder, outputWriteRect, aFuzz);
+}
+
+void
+CheckWritePixels(Decoder* aDecoder,
+                 SurfaceFilter* aFilter,
+                 Maybe<IntRect> aOutputRect /* = Nothing() */,
+                 Maybe<IntRect> aInputRect /* = Nothing() */,
+                 Maybe<IntRect> aInputWriteRect /* = Nothing() */,
+                 Maybe<IntRect> aOutputWriteRect /* = Nothing() */,
+                 uint8_t aFuzz /* = 0 */)
+{
+  CheckSurfacePipeWrite(aDecoder, aFilter,
+                        aOutputRect, aInputRect,
+                        aInputWriteRect, aOutputWriteRect,
+                        aFuzz,
+                        [&](int32_t& aCount) {
+    return aFilter->WritePixels<uint32_t>([&] {
+      ++aCount;
+      return AsVariant(BGRAColor::Green().AsPixel());
+    });
+  });
+}
+
+void
+CheckWriteRows(Decoder* aDecoder,
+               SurfaceFilter* aFilter,
+               Maybe<IntRect> aOutputRect /* = Nothing() */,
+               Maybe<IntRect> aInputRect /* = Nothing() */,
+               Maybe<IntRect> aInputWriteRect /* = Nothing() */,
+               Maybe<IntRect> aOutputWriteRect /* = Nothing() */,
+               uint8_t aFuzz /* = 0 */)
+{
+  CheckSurfacePipeWrite(aDecoder, aFilter,
+                        aOutputRect, aInputRect,
+                        aInputWriteRect, aOutputWriteRect,
+                        aFuzz,
+                        [&](int32_t& aCount) {
+    return aFilter->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      for (; aLength > 0; --aLength, ++aRow, ++aCount) {
+        *aRow = BGRAColor::Green().AsPixel();
+      }
+      return Nothing();
+    });
+  });
+}
+
+template <typename Func> void
+CheckPalettedSurfacePipeWrite(Decoder* aDecoder,
+                              SurfaceFilter* aFilter,
+                              Maybe<IntRect> aOutputRect,
+                              Maybe<IntRect> aInputRect,
+                              Maybe<IntRect> aInputWriteRect,
+                              Maybe<IntRect> aOutputWriteRect,
+                              uint8_t aFuzz,
+                              Func aFunc)
+{
+  IntRect outputRect = aOutputRect.valueOr(IntRect(0, 0, 100, 100));
+  IntRect inputRect = aInputRect.valueOr(IntRect(0, 0, 100, 100));
+  IntRect inputWriteRect = aInputWriteRect.valueOr(inputRect);
+  IntRect outputWriteRect = aOutputWriteRect.valueOr(outputRect);
+
+  // Fill the image.
+  int32_t count = 0;
+  auto result = aFunc(count);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_EQ(inputWriteRect.width * inputWriteRect.height, count);
+
+  AssertCorrectPipelineFinalState(aFilter, inputRect, outputRect);
+
+  // Attempt to write more data and make sure nothing changes.
+  const int32_t oldCount = count;
+  result = aFunc(count);
+  EXPECT_EQ(oldCount, count);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_TRUE(aFilter->IsSurfaceFinished());
+  Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Attempt to advance to the next row and make sure nothing changes.
+  aFilter->AdvanceRow();
+  EXPECT_TRUE(aFilter->IsSurfaceFinished());
+  invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Check that the generated image is correct.
+  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+  uint8_t* imageData;
+  uint32_t imageLength;
+  currentFrame->GetImageData(&imageData, &imageLength);
+  ASSERT_TRUE(imageData != nullptr);
+  ASSERT_EQ(outputWriteRect.width * outputWriteRect.height, int32_t(imageLength));
+  for (uint32_t i = 0; i < imageLength; ++i) {
+    ASSERT_EQ(uint8_t(255), imageData[i]);
+  }
+}
+
+void
+CheckPalettedWritePixels(Decoder* aDecoder,
+                         SurfaceFilter* aFilter,
+                         Maybe<IntRect> aOutputRect /* = Nothing() */,
+                         Maybe<IntRect> aInputRect /* = Nothing() */,
+                         Maybe<IntRect> aInputWriteRect /* = Nothing() */,
+                         Maybe<IntRect> aOutputWriteRect /* = Nothing() */,
+                         uint8_t aFuzz /* = 0 */)
+{
+  CheckPalettedSurfacePipeWrite(aDecoder, aFilter,
+                                aOutputRect, aInputRect,
+                                aInputWriteRect, aOutputWriteRect,
+                                aFuzz,
+                                [&](int32_t& aCount) {
+    return aFilter->WritePixels<uint8_t>([&] {
+      ++aCount;
+      return AsVariant(uint8_t(255));
+    });
+  });
+}
+
+void
+CheckPalettedWriteRows(Decoder* aDecoder,
+                       SurfaceFilter* aFilter,
+                       Maybe<IntRect> aOutputRect /* = Nothing() */,
+                       Maybe<IntRect> aInputRect /* = Nothing() */,
+                       Maybe<IntRect> aInputWriteRect /* = Nothing() */,
+                       Maybe<IntRect> aOutputWriteRect /* = Nothing() */,
+                       uint8_t aFuzz /* = 0*/)
+{
+  CheckPalettedSurfacePipeWrite(aDecoder, aFilter,
+                                aOutputRect, aInputRect,
+                                aInputWriteRect, aOutputWriteRect,
+                                aFuzz,
+                                [&](int32_t& aCount) {
+    return aFilter->WriteRows<uint8_t>([&](uint8_t* aRow, uint32_t aLength) {
+      for (; aLength > 0; --aLength, ++aRow, ++aCount) {
+        *aRow = uint8_t(255);
+      }
+      return Nothing();
+    });
+  });
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
 // Test Data
 ///////////////////////////////////////////////////////////////////////////////
 
 ImageTestCase GreenPNGTestCase()
 {
   return ImageTestCase("green.png", "image/png", IntSize(100, 100));
 }
 
@@ -219,9 +505,10 @@ ImageTestCase NoFrameDelayGIFTestCase()
 {
   // This is an invalid (or at least, questionably valid) GIF that's animated
   // even though it specifies a frame delay of zero. It's animated, but it's not
   // marked TEST_CASE_IS_ANIMATED because the metadata decoder can't detect that
   // it's animated.
   return ImageTestCase("no-frame-delay.gif", "image/gif", IntSize(100, 100));
 }
 
+} // namespace image
 } // namespace mozilla
--- a/image/test/gtest/Common.h
+++ b/image/test/gtest/Common.h
@@ -1,22 +1,31 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #ifndef mozilla_image_test_gtest_Common_h
 #define mozilla_image_test_gtest_Common_h
 
+#include "gtest/gtest.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
 #include "mozilla/gfx/2D.h"
+#include "Decoder.h"
+#include "gfxColor.h"
 #include "nsCOMPtr.h"
+#include "SurfacePipe.h"
+#include "SurfacePipeFactory.h"
 
 class nsIInputStream;
 
 namespace mozilla {
+namespace image {
 
 ///////////////////////////////////////////////////////////////////////////////
 // Types
 ///////////////////////////////////////////////////////////////////////////////
 
 enum TestCaseFlags
 {
   TEST_CASE_DEFAULT_FLAGS   = 0,
@@ -49,40 +58,238 @@ struct BGRAColor
   BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha)
     : mBlue(aBlue)
     , mGreen(aGreen)
     , mRed(aRed)
     , mAlpha(aAlpha)
   { }
 
   static BGRAColor Green() { return BGRAColor(0x00, 0xFF, 0x00, 0xFF); }
+  static BGRAColor Red()   { return BGRAColor(0x00, 0x00, 0xFF, 0xFF); }
+  static BGRAColor Transparent() { return BGRAColor(0x00, 0x00, 0x00, 0x00); }
+
+  uint32_t AsPixel() const { return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue); }
 
   uint8_t mBlue;
   uint8_t mGreen;
   uint8_t mRed;
   uint8_t mAlpha;
 };
 
 
 ///////////////////////////////////////////////////////////////////////////////
-// Helpers
+// General Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 /// Loads a file from the current directory. @return an nsIInputStream for it.
 already_AddRefed<nsIInputStream> LoadFile(const char* aRelativePath);
 
 /**
  * @returns true if every pixel of @aSurface is @aColor.
  * 
- * If @aFuzzy is true, a tolerance of 1 is allowed in each color component. This
- * may be necessary for tests that involve JPEG images.
+ * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
+ * component. This may be necessary for tests that involve JPEG images or
+ * downscaling.
  */
 bool IsSolidColor(gfx::SourceSurface* aSurface,
                   BGRAColor aColor,
-                  bool aFuzzy = false);
+                  uint8_t aFuzz = 0);
+
+/**
+ * @returns true if every pixel in the range of rows specified by @aStartRow and
+ * @aRowCount of @aSurface is @aColor.
+ *
+ * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
+ * component. This may be necessary for tests that involve JPEG images or
+ * downscaling.
+ */
+bool RowsAreSolidColor(gfx::SourceSurface* aSurface,
+                       int32_t aStartRow,
+                       int32_t aRowCount,
+                       BGRAColor aColor,
+                       uint8_t aFuzz = 0);
+
+/**
+ * @returns true if every pixel in the rect specified by @aRect is @aColor.
+ *
+ * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
+ * component. This may be necessary for tests that involve JPEG images or
+ * downscaling.
+ */
+bool RectIsSolidColor(gfx::SourceSurface* aSurface,
+                      const gfx::IntRect& aRect,
+                      BGRAColor aColor,
+                      uint8_t aFuzz = 0);
+
+
+///////////////////////////////////////////////////////////////////////////////
+// SurfacePipe Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Creates a decoder with no data associated with, suitable for testing code
+ * that requires a decoder to initialize or to allocate surfaces but doesn't
+ * actually need the decoder to do any decoding.
+ *
+ * XXX(seth): We only need this because SurfaceSink and PalettedSurfaceSink
+ * defer to the decoder for surface allocation. Once all decoders use
+ * SurfacePipe we won't need to do that anymore and we can remove this function.
+ */
+already_AddRefed<Decoder> CreateTrivialDecoder();
+
+/**
+ * Creates a pipeline of SurfaceFilters from a list of Config structs and passes
+ * it to the provided lambda @aFunc. Assertions that the pipeline is constructly
+ * correctly and cleanup of any allocated surfaces is handled automatically.
+ *
+ * @param aDecoder The decoder to use for allocating surfaces.
+ * @param aFunc The lambda function to pass the filter pipeline to.
+ * @param aConfigs The configuration for the pipeline.
+ */
+template <typename Func, typename... Configs>
+void WithFilterPipeline(Decoder* aDecoder, Func aFunc, Configs... aConfigs)
+{
+  auto pipe = MakeUnique<typename detail::FilterPipeline<Configs...>::Type>();
+  nsresult rv = pipe->Configure(aConfigs...);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  aFunc(aDecoder, pipe.get());
+
+  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+  if (currentFrame) {
+    currentFrame->Finish();
+  }
+}
+
+/**
+ * Creates a pipeline of SurfaceFilters from a list of Config structs and
+ * asserts that configuring it fails. Cleanup of any allocated surfaces is
+ * handled automatically.
+ *
+ * @param aDecoder The decoder to use for allocating surfaces.
+ * @param aConfigs The configuration for the pipeline.
+ */
+template <typename... Configs>
+void AssertConfiguringPipelineFails(Decoder* aDecoder, Configs... aConfigs)
+{
+  auto pipe = MakeUnique<typename detail::FilterPipeline<Configs...>::Type>();
+  nsresult rv = pipe->Configure(aConfigs...);
+
+  // Callers expect configuring the pipeline to fail.
+  ASSERT_TRUE(NS_FAILED(rv));
+
+  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+  if (currentFrame) {
+    currentFrame->Finish();
+  }
+}
+
+/**
+ * Asserts that the provided filter pipeline is in the correct final state,
+ * which is to say, the entire surface has been written to (IsSurfaceFinished()
+ * returns true) and the invalid rects are as expected.
+ *
+ * @param aFilter The filter pipeline to check.
+ * @param aInputSpaceRect The expect invalid rect, in input space.
+ * @param aoutputSpaceRect The expect invalid rect, in output space.
+ */
+void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter,
+                                     const gfx::IntRect& aInputSpaceRect,
+                                     const gfx::IntRect& aOutputSpaceRect);
+
+/**
+ * Checks a generated image for correctness. Reports any unexpected deviation
+ * from the expected image as GTest failures.
+ *
+ * @param aDecoder The decoder which contains the image. The decoder's current
+ *                 frame will be checked.
+ * @param aRect The region in the space of the output surface that the filter
+ *              pipeline will actually write to. It's expected that pixels in
+ *              this region are green, while pixels outside this region are
+ *              transparent. Defaults to the entire output rect.
+ * @param aFuzz The amount of fuzz to use in pixel comparisons.
+ */
+void CheckGeneratedImage(Decoder* aDecoder,
+                         const gfx::IntRect& aRect,
+                         uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WritePixels() using the provided SurfaceFilter
+ * pipeline. The pipeline must be a normal (i.e., non-paletted) pipeline.
+ *
+ * The arguments are specified in the an order intended to minimize the number
+ * of arguments that most test cases need to pass.
+ *
+ * @param aDecoder The decoder whose current frame will be written to.
+ * @param aFilter The SurfaceFilter pipeline to use.
+ * @param aOutputRect The region in the space of the output surface that will be
+ *                    invalidated by the filter pipeline. Defaults to
+ *                    (0, 0, 100, 100).
+ * @param aInputRect The region in the space of the input image that will be
+ *                   invalidated by the filter pipeline. Defaults to
+ *                   (0, 0, 100, 100).
+ * @param aInputWriteRect The region in the space of the input image that the
+ *                        filter pipeline will allow writes to. Note the
+ *                        difference from @aInputRect: @aInputRect is the actual
+ *                        region invalidated, while @aInputWriteRect is the
+ *                        region that is written to. These can differ in cases
+ *                        where the input is not clipped to the size of the image.
+ *                        Defaults to the entire input rect.
+ * @param aOutputWriteRect The region in the space of the output surface that
+ *                         the filter pipeline will actually write to. It's
+ *                         expected that pixels in this region are green, while
+ *                         pixels outside this region are transparent. Defaults
+ *                         to the entire output rect.
+ */
+void CheckWritePixels(Decoder* aDecoder,
+                      SurfaceFilter* aFilter,
+                      Maybe<gfx::IntRect> aOutputRect = Nothing(),
+                      Maybe<gfx::IntRect> aInputRect = Nothing(),
+                      Maybe<gfx::IntRect> aInputWriteRect = Nothing(),
+                      Maybe<gfx::IntRect> aOutputWriteRect = Nothing(),
+                      uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WriteRows() using the provided SurfaceFilter
+ * pipeline. The pipeline must be a normal (i.e., non-paletted) pipeline.
+ * @see CheckWritePixels() for documentation of the arguments.
+ */
+void CheckWriteRows(Decoder* aDecoder,
+                    SurfaceFilter* aFilter,
+                    Maybe<gfx::IntRect> aOutputRect = Nothing(),
+                    Maybe<gfx::IntRect> aInputRect = Nothing(),
+                    Maybe<gfx::IntRect> aInputWriteRect = Nothing(),
+                    Maybe<gfx::IntRect> aOutputWriteRect = Nothing(),
+                    uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WritePixels() using the provided SurfaceFilter
+ * pipeline. The pipeline must be a paletted pipeline.
+ * @see CheckWritePixels() for documentation of the arguments.
+ */
+void CheckPalettedWritePixels(Decoder* aDecoder,
+                              SurfaceFilter* aFilter,
+                              Maybe<gfx::IntRect> aOutputRect = Nothing(),
+                              Maybe<gfx::IntRect> aInputRect = Nothing(),
+                              Maybe<gfx::IntRect> aInputWriteRect = Nothing(),
+                              Maybe<gfx::IntRect> aOutputWriteRect = Nothing(),
+                              uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WriteRows() using the provided SurfaceFilter
+ * pipeline. The pipeline must be a paletted pipeline.
+ * @see CheckWritePixels() for documentation of the arguments.
+ */
+void CheckPalettedWriteRows(Decoder* aDecoder,
+                            SurfaceFilter* aFilter,
+                            Maybe<gfx::IntRect> aOutputRect = Nothing(),
+                            Maybe<gfx::IntRect> aInputRect = Nothing(),
+                            Maybe<gfx::IntRect> aInputWriteRect = Nothing(),
+                            Maybe<gfx::IntRect> aOutputWriteRect = Nothing(),
+                            uint8_t aFuzz = 0);
 
 
 ///////////////////////////////////////////////////////////////////////////////
 // Test Data
 ///////////////////////////////////////////////////////////////////////////////
 
 ImageTestCase GreenPNGTestCase();
 ImageTestCase GreenGIFTestCase();
@@ -100,11 +307,12 @@ ImageTestCase TransparentPNGTestCase();
 ImageTestCase TransparentGIFTestCase();
 ImageTestCase FirstFramePaddingGIFTestCase();
 ImageTestCase NoFrameDelayGIFTestCase();
 
 ImageTestCase TransparentBMPWhenBMPAlphaEnabledTestCase();
 ImageTestCase RLE4BMPTestCase();
 ImageTestCase RLE8BMPTestCase();
 
+} // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_test_gtest_Common_h
--- a/image/test/gtest/TestDecodeToSurface.cpp
+++ b/image/test/gtest/TestDecodeToSurface.cpp
@@ -58,17 +58,17 @@ public:
     ASSERT_TRUE(mSurface != nullptr);
 
     EXPECT_EQ(SurfaceType::DATA, mSurface->GetType());
     EXPECT_TRUE(mSurface->GetFormat() == SurfaceFormat::B8G8R8X8 ||
                 mSurface->GetFormat() == SurfaceFormat::B8G8R8A8);
     EXPECT_EQ(mTestCase.mSize, mSurface->GetSize());
 
     EXPECT_TRUE(IsSolidColor(mSurface, BGRAColor::Green(),
-                             mTestCase.mFlags & TEST_CASE_IS_FUZZY));
+                             mTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
   }
 
 private:
   RefPtr<SourceSurface>& mSurface;
   nsCOMPtr<nsIInputStream> mInputStream;
   ImageTestCase mTestCase;
 };
 
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -74,17 +74,17 @@ CheckDecoderResults(const ImageTestCase&
   RefPtr<SourceSurface> surface = currentFrame->GetSurface();
 
   // Verify that the resulting surfaces matches our expectations.
   EXPECT_EQ(SurfaceType::DATA, surface->GetType());
   EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::B8G8R8X8 ||
               surface->GetFormat() == SurfaceFormat::B8G8R8A8);
   EXPECT_EQ(aTestCase.mSize, surface->GetSize());
   EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green(),
-                           aTestCase.mFlags & TEST_CASE_IS_FUZZY));
+                           aTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
 }
 
 static void
 CheckDecoderSingleChunk(const ImageTestCase& aTestCase)
 {
   nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
   ASSERT_TRUE(inputStream != nullptr);
 
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestDeinterlacingFilter.cpp
@@ -0,0 +1,636 @@
+/* -*- 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 "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;
+
+template <typename Func> void
+WithDeinterlacingFilter(const IntSize& aSize,
+                        bool aProgressiveDisplay,
+                        Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(bool(decoder));
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     DeinterlacingConfig<uint32_t> { aProgressiveDisplay },
+                     SurfaceConfig { decoder, 0, aSize,
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+template <typename Func> void
+WithPalettedDeinterlacingFilter(const IntSize& aSize,
+                                Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     DeinterlacingConfig<uint8_t> { /* mProgressiveDisplay = */ true },
+                     PalettedSurfaceConfig { decoder, 0, aSize,
+                                             IntRect(0, 0, 100, 100),
+                                             SurfaceFormat::B8G8R8A8, 8,
+                                             false });
+}
+
+void
+AssertConfiguringDeinterlacingFilterFails(const IntSize& aSize)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AssertConfiguringPipelineFails(decoder,
+                                 DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true},
+                                 SurfaceConfig { decoder, 0, aSize,
+                                                 SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels100_100)
+{
+  WithDeinterlacingFilter(IntSize(100, 100), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows100_100)
+{
+  WithDeinterlacingFilter(IntSize(100, 100), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels99_99)
+{
+  WithDeinterlacingFilter(IntSize(99, 99), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 99, 99)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 99, 99)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows99_99)
+{
+  WithDeinterlacingFilter(IntSize(99, 99), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 99, 99)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 99, 99)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels8_8)
+{
+  WithDeinterlacingFilter(IntSize(8, 8), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 8, 8)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 8, 8)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows8_8)
+{
+  WithDeinterlacingFilter(IntSize(8, 8), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 8, 8)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 8, 8)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels7_7)
+{
+  WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 7, 7)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 7, 7)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows7_7)
+{
+  WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 7, 7)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 7, 7)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels3_3)
+{
+  WithDeinterlacingFilter(IntSize(3, 3), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 3, 3)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 3, 3)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows3_3)
+{
+  WithDeinterlacingFilter(IntSize(3, 3), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 3, 3)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 3, 3)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixels1_1)
+{
+  WithDeinterlacingFilter(IntSize(1, 1), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 1, 1)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRows1_1)
+{
+  WithDeinterlacingFilter(IntSize(1, 1), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 1, 1)));
+  });
+}
+
+TEST(ImageDeinterlacingFilter, PalettedWritePixels)
+{
+  WithPalettedDeinterlacingFilter(IntSize(100, 100),
+                                  [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckPalettedWritePixels(aDecoder, aFilter);
+  });
+}
+
+TEST(ImageDeinterlacingFilter, PalettedWriteRows)
+{
+  WithPalettedDeinterlacingFilter(IntSize(100, 100),
+                                  [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckPalettedWriteRows(aDecoder, aFilter);
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixelsOutput20_20)
+{
+  WithDeinterlacingFilter(IntSize(20, 20), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be green for even rows and red for odd
+    // rows but we need to write the rows in the order that the deinterlacer
+    // expects them.
+    uint32_t count = 0;
+    auto result = aFilter->WritePixels<uint32_t>([&]() {
+      uint32_t row = count / 20;  // Integer division.
+      ++count;
+
+      switch (row) {
+        // First pass. Output rows are positioned at 8n + 0.
+        case 0:  // Output row 0.
+        case 1:  // Output row 8.
+        case 2:  // Output row 16.
+          return AsVariant(BGRAColor::Green().AsPixel());
+
+        // Second pass. Rows are positioned at 8n + 4.
+        case 3:  // Output row 4.
+        case 4:  // Output row 12.
+          return AsVariant(BGRAColor::Green().AsPixel());
+
+        // Third pass. Rows are positioned at 4n + 2.
+        case 5: // Output row 2.
+        case 6: // Output row 6.
+        case 7: // Output row 10.
+        case 8: // Output row 14.
+        case 9: // Output row 18.
+          return AsVariant(BGRAColor::Green().AsPixel());
+
+        // Fourth pass. Rows are positioned at 2n + 1.
+        case 10:  // Output row 1.
+        case 11:  // Output row 3.
+        case 12:  // Output row 5.
+        case 13:  // Output row 7.
+        case 14:  // Output row 9.
+        case 15:  // Output row 11.
+        case 16:  // Output row 13.
+        case 17:  // Output row 15.
+        case 18:  // Output row 17.
+        case 19:  // Output row 19.
+          return AsVariant(BGRAColor::Red().AsPixel());
+
+        default:
+          MOZ_ASSERT_UNREACHABLE("Unexpected row");
+          return AsVariant(BGRAColor::Transparent().AsPixel());
+      }
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(20u * 20u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 20, 20),
+                                    IntRect(0, 0, 20, 20));
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // even rows to be green and odd rows to be red.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    for (uint32_t row = 0; row < 20; ++row) {
+      EXPECT_TRUE(RowsAreSolidColor(surface, row, 1,
+                                    row % 2 == 0 ? BGRAColor::Green()
+                                                 : BGRAColor::Red()));
+    }
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRowsOutput7_7)
+{
+  WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be a repeating pattern of two green
+    // rows followed by two red rows but we need to write the rows in the order
+    // that the deinterlacer expects them.
+    uint32_t count = 0;
+    uint32_t row = 0;
+    auto result = aFilter->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      uint32_t color = 0;
+      switch (row) {
+        // First pass. Output rows are positioned at 8n + 0.
+        case 0:  // Output row 0.
+          color = BGRAColor::Green().AsPixel();
+          break;
+
+        // Second pass. Rows are positioned at 8n + 4.
+        case 1:  // Output row 4.
+          color = BGRAColor::Green().AsPixel();
+          break;
+
+        // Third pass. Rows are positioned at 4n + 2.
+        case 2: // Output row 2.
+        case 3: // Output row 6.
+          color = BGRAColor::Red().AsPixel();
+          break;
+
+        // Fourth pass. Rows are positioned at 2n + 1.
+        case 4:  // Output row 1.
+          color = BGRAColor::Green().AsPixel();
+          break;
+
+        case 5:  // Output row 3.
+          color = BGRAColor::Red().AsPixel();
+          break;
+
+        case 6:  // Output row 5.
+          color = BGRAColor::Green().AsPixel();
+          break;
+
+        default:
+          MOZ_ASSERT_UNREACHABLE("Unexpected row");
+      }
+
+      ++row;
+
+      for (; aLength > 0; --aLength, ++aRow, ++count) {
+        *aRow = color;
+      }
+
+      return Nothing();
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(7u * 7u, count);
+    EXPECT_EQ(7u, row);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 7, 7),
+                                    IntRect(0, 0, 7, 7));
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // two green rows, followed by two red rows, then two green rows, etc.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    for (uint32_t row = 0; row < 7; ++row) {
+      BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5
+                      ? BGRAColor::Green()
+                      : BGRAColor::Red();
+      EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color));
+    }
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixelsOutput3_3)
+{
+  WithDeinterlacingFilter(IntSize(3, 3), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be green, red, green in that order, but
+    // we need to write the rows in the order that the deinterlacer expects
+    // them.
+    uint32_t count = 0;
+    auto result = aFilter->WritePixels<uint32_t>([&]() {
+      uint32_t row = count / 3;  // Integer division.
+      ++count;
+
+      switch (row) {
+        // First pass. Output rows are positioned at 8n + 0.
+        case 0:  // Output row 0.
+          return AsVariant(BGRAColor::Green().AsPixel());
+
+        // Second pass. Rows are positioned at 8n + 4.
+        // No rows for this pass.
+
+        // Third pass. Rows are positioned at 4n + 2.
+        case 1: // Output row 2.
+          return AsVariant(BGRAColor::Green().AsPixel());
+
+        // Fourth pass. Rows are positioned at 2n + 1.
+        case 2:  // Output row 1.
+          return AsVariant(BGRAColor::Red().AsPixel());
+
+        default:
+          MOZ_ASSERT_UNREACHABLE("Unexpected row");
+          return AsVariant(BGRAColor::Transparent().AsPixel());
+      }
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(3u * 3u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 3, 3),
+                                    IntRect(0, 0, 3, 3));
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // green, red, green in that order.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    for (uint32_t row = 0; row < 3; ++row) {
+      EXPECT_TRUE(RowsAreSolidColor(surface, row, 1,
+                                    row == 0 || row == 2 ? BGRAColor::Green()
+                                                         : BGRAColor::Red()));
+    }
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WritePixelsOutput1_1)
+{
+  WithDeinterlacingFilter(IntSize(1, 1), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be a single red row.
+    uint32_t count = 0;
+    auto result = aFilter->WritePixels<uint32_t>([&]() {
+      ++count;
+      return AsVariant(BGRAColor::Red().AsPixel());
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(1u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 1, 1),
+                                    IntRect(0, 0, 1, 1));
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // a single red row.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    EXPECT_TRUE(RowsAreSolidColor(surface, 0, 1, BGRAColor::Red()));
+  });
+}
+
+void
+WriteRowAndCheckInterlacerOutput(Decoder* aDecoder,
+                                 SurfaceFilter* aFilter,
+                                 BGRAColor aColor,
+                                 WriteState aNextState,
+                                 IntRect aInvalidRect,
+                                 uint32_t aFirstHaeberliRow,
+                                 uint32_t aLastHaeberliRow)
+{
+  uint32_t count = 0;
+
+  auto result = aFilter->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+    for (; aLength > 0; --aLength, ++aRow, ++count) {
+      *aRow = aColor.AsPixel();
+    }
+    return Some(WriteState::NEED_MORE_DATA);
+  });
+
+  EXPECT_EQ(aNextState, result);
+  EXPECT_EQ(7u, count);
+
+  // Assert that we got the expected invalidation region.
+  Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isSome());
+  EXPECT_EQ(aInvalidRect, invalidRect->mInputSpaceRect);
+  EXPECT_EQ(aInvalidRect, invalidRect->mOutputSpaceRect);
+
+  // Check that the portion of the image generated so far is correct. The rows
+  // from aFirstHaeberliRow to aLastHaeberliRow should be filled with aColor.
+  // Note that this is not the same as the set of rows in aInvalidRect, because
+  // after writing a row the deinterlacer seeks to the next row to write, which
+  // may involve copying previously-written rows in the buffer to the output
+  // even though they don't change in this pass.
+  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+  RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+  for (uint32_t row = aFirstHaeberliRow; row <= aLastHaeberliRow; ++row) {
+    EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, aColor));
+  }
+}
+
+TEST(ImageDeinterlacingFilter, WriteRowsIntermediateOutput7_7)
+{
+  WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ true,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be a repeating pattern of two green
+    // rows followed by two red rows but we need to write the rows in the order
+    // that the deinterlacer expects them.
+
+    // First pass. Output rows are positioned at 8n + 0.
+
+    // Output row 0. The invalid rect is the entire image because this is the
+    // end of the first pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 0, 4);
+
+    // Second pass. Rows are positioned at 8n + 4.
+
+    // Output row 4. The invalid rect is the entire image because this is the
+    // end of the second pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 1, 4);
+
+    // Third pass. Rows are positioned at 4n + 2.
+
+    // Output row 2. The invalid rect contains the Haeberli rows for this output
+    // row (rows 2 and 3) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (rows 4 and 5).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 2, 7, 4), 2, 3);
+
+    // Output row 6. The invalid rect is the entire image because this is the
+    // end of the third pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 6, 6);
+
+    // Fourth pass. Rows are positioned at 2n + 1.
+
+    // Output row 1. The invalid rect contains the Haeberli rows for this output
+    // row (just row 1) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 2).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 1, 7, 2), 1, 1);
+
+    // Output row 3. The invalid rect contains the Haeberli rows for this output
+    // row (just row 3) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 4).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 3, 7, 2), 3, 3);
+
+    // Output row 5. The invalid rect contains the Haeberli rows for this output
+    // row (just row 5) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 6).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::FINISHED,
+                                     IntRect(0, 5, 7, 2), 5, 5);
+
+    // Assert that we're in the expected final state.
+    EXPECT_TRUE(aFilter->IsSurfaceFinished());
+    Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+    EXPECT_TRUE(invalidRect.isNothing());
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // two green rows, followed by two red rows, then two green rows, etc.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    for (uint32_t row = 0; row < 7; ++row) {
+      BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5
+                      ? BGRAColor::Green()
+                      : BGRAColor::Red();
+      EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color));
+    }
+  });
+}
+
+TEST(ImageDeinterlacingFilter, WriteRowsNonProgressiveIntermediateOutput7_7)
+{
+  WithDeinterlacingFilter(IntSize(7, 7), /* aProgressiveDisplay = */ false,
+                          [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. The output should be a repeating pattern of two green
+    // rows followed by two red rows but we need to write the rows in the order
+    // that the deinterlacer expects them.
+
+    // First pass. Output rows are positioned at 8n + 0.
+
+    // Output row 0. The invalid rect is the entire image because this is the
+    // end of the first pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 0, 0);
+
+    // Second pass. Rows are positioned at 8n + 4.
+
+    // Output row 4. The invalid rect is the entire image because this is the
+    // end of the second pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 4, 4);
+
+    // Third pass. Rows are positioned at 4n + 2.
+
+    // Output row 2. The invalid rect contains the Haeberli rows for this output
+    // row (rows 2 and 3) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (rows 4 and 5).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 2, 7, 4), 2, 2);
+
+    // Output row 6. The invalid rect is the entire image because this is the
+    // end of the third pass.
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 0, 7, 7), 6, 6);
+
+    // Fourth pass. Rows are positioned at 2n + 1.
+
+    // Output row 1. The invalid rect contains the Haeberli rows for this output
+    // row (just row 1) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 2).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 1, 7, 2), 1, 1);
+
+    // Output row 3. The invalid rect contains the Haeberli rows for this output
+    // row (just row 3) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 4).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Red(),
+                                     WriteState::NEED_MORE_DATA,
+                                     IntRect(0, 3, 7, 2), 3, 3);
+
+    // Output row 5. The invalid rect contains the Haeberli rows for this output
+    // row (just row 5) as well as the rows that we copy from previous passes
+    // when seeking to the next output row (row 6).
+    WriteRowAndCheckInterlacerOutput(aDecoder, aFilter, BGRAColor::Green(),
+                                     WriteState::FINISHED,
+                                     IntRect(0, 5, 7, 2), 5, 5);
+
+    // Assert that we're in the expected final state.
+    EXPECT_TRUE(aFilter->IsSurfaceFinished());
+    Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+    EXPECT_TRUE(invalidRect.isNothing());
+
+    // Check that the generated image is correct. As mentioned above, we expect
+    // two green rows, followed by two red rows, then two green rows, etc.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+
+    for (uint32_t row = 0; row < 7; ++row) {
+      BGRAColor color = row == 0 || row == 1 || row == 4 || row == 5
+                      ? BGRAColor::Green()
+                      : BGRAColor::Red();
+      EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, color));
+    }
+  });
+}
+
+
+TEST(ImageDeinterlacingFilter, DeinterlacingFailsFor0_0)
+{
+  // A 0x0 input size is invalid, so configuration should fail.
+  AssertConfiguringDeinterlacingFilterFails(IntSize(0, 0));
+}
+
+TEST(ImageDeinterlacingFilter, DeinterlacingFailsForMinus1_Minus1)
+{
+  // A negative input size is invalid, so configuration should fail.
+  AssertConfiguringDeinterlacingFilterFails(IntSize(-1, -1));
+}
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestDownscalingFilter.cpp
@@ -0,0 +1,364 @@
+/* -*- 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 "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;
+
+template <typename Func> void
+WithDownscalingFilter(const IntSize& aInputSize,
+                      const IntSize& aOutputSize,
+                      Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     DownscalingConfig { aInputSize,
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, aOutputSize,
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+void
+AssertConfiguringDownscalingFilterFails(const IntSize& aInputSize,
+                                        const IntSize& aOutputSize)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AssertConfiguringPipelineFails(decoder,
+                                 DownscalingConfig { aInputSize,
+                                                     SurfaceFormat::B8G8R8A8 },
+                                 SurfaceConfig { decoder, 0, aOutputSize,
+                                                 SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to99_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 99, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to99_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 99, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to33_33)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(33, 33),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 33, 33)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to33_33)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(33, 33),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 33, 33)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to1_1)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(1, 1),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to1_1)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(1, 1),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to33_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(33, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 33, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to33_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(33, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 33, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to99_33)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 33),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 99, 33)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to99_33)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 33),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 99, 33)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to99_1)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 1),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 99, 1)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to99_1)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(99, 1),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 99, 1)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to1_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(1, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 1, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRows100_100to1_99)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(1, 99),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 1, 99)));
+  });
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to101_101)
+{
+  // Upscaling is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(101, 101));
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to100_100)
+{
+  // "Scaling" to the same size is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(100, 100));
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsFor0_0toMinus1_Minus1)
+{
+  // A 0x0 input size is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(0, 0), IntSize(-1, -1));
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsForMinus1_Minus1toMinus2_Minus2)
+{
+  // A negative input size is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(-1, -1), IntSize(-2, -2));
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsFor100_100to0_0)
+{
+  // A 0x0 output size is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(0, 0));
+}
+
+TEST(ImageDownscalingFilter, DownscalingFailsFor100_100toMinus1_Minus1)
+{
+  // A negative output size is disallowed.
+  AssertConfiguringDownscalingFilterFails(IntSize(100, 100), IntSize(-1, -1));
+}
+
+TEST(ImageDownscalingFilter, WritePixelsOutput100_100to20_20)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(20, 20),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. It consists of 25 lines of green, followed by 25 lines of
+    // red, followed by 25 lines of green, followed by 25 more lines of red.
+    uint32_t count = 0;
+    auto result = aFilter->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+      uint32_t color = (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100)
+                     ? BGRAColor::Green().AsPixel()
+                     : BGRAColor::Red().AsPixel();
+      ++count;
+      return AsVariant(color);
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(100u * 100u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 100, 100),
+                                    IntRect(0, 0, 20, 20));
+
+    // Check that the generated image is correct. Note that we skip rows near
+    // the transitions between colors, since the downscaler does not produce a
+    // sharp boundary at these points. Even some of the rows we test need a
+    // small amount of fuzz; this is just the nature of Lanczos downscaling.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 2));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 3));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRowsOutput100_100to20_20)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(20, 20),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. It consists of 25 lines of green, followed by 25 lines of
+    // red, followed by 25 lines of green, followed by 25 more lines of red.
+    uint32_t count = 0;
+    auto result = aFilter->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      uint32_t color = (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100)
+                     ? BGRAColor::Green().AsPixel()
+                     : BGRAColor::Red().AsPixel();
+      for (; aLength > 0; --aLength, ++aRow, ++count) {
+        *aRow = color;
+      }
+      return Nothing();
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(100u * 100u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 100, 100),
+                                    IntRect(0, 0, 20, 20));
+
+    // Check that the generated image is correct. (Note that we skip rows near
+    // the transitions between colors, since the downscaler does not produce a
+    // sharp boundary at these points.)
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 2));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 3));
+  });
+}
+
+TEST(ImageDownscalingFilter, WritePixelsOutput100_100to10_20)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(10, 20),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. It consists of 25 lines of green, followed by 25 lines of
+    // red, followed by 25 lines of green, followed by 25 more lines of red.
+    uint32_t count = 0;
+    auto result = aFilter->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+      uint32_t color = (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100)
+                     ? BGRAColor::Green().AsPixel()
+                     : BGRAColor::Red().AsPixel();
+      ++count;
+      return AsVariant(color);
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(100u * 100u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 100, 100),
+                                    IntRect(0, 0, 10, 20));
+
+    // Check that the generated image is correct. Note that we skip rows near
+    // the transitions between colors, since the downscaler does not produce a
+    // sharp boundary at these points. Even some of the rows we test need a
+    // small amount of fuzz; this is just the nature of Lanczos downscaling.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 2));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 3));
+  });
+}
+
+TEST(ImageDownscalingFilter, WriteRowsOutput100_100to10_20)
+{
+  WithDownscalingFilter(IntSize(100, 100), IntSize(10, 20),
+                        [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Fill the image. It consists of 25 lines of green, followed by 25 lines of
+    // red, followed by 25 lines of green, followed by 25 more lines of red.
+    uint32_t count = 0;
+    auto result = aFilter->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      uint32_t color = (count <= 25 * 100) || (count > 50 * 100 && count <= 75 * 100)
+                     ? BGRAColor::Green().AsPixel()
+                     : BGRAColor::Red().AsPixel();
+      for (; aLength > 0; --aLength, ++aRow, ++count) {
+        *aRow = color;
+      }
+      return Nothing();
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(100u * 100u, count);
+
+    AssertCorrectPipelineFinalState(aFilter,
+                                    IntRect(0, 0, 100, 100),
+                                    IntRect(0, 0, 10, 20));
+
+    // Check that the generated image is correct. (Note that we skip rows near
+    // the transitions between colors, since the downscaler does not produce a
+    // sharp boundary at these points.)
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 2));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 3));
+    EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 3));
+  });
+}
+
+TEST(ImageDownscalingFilter, ConfiguringPalettedDownscaleFails)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // DownscalingFilter does not support paletted images, so configuration should
+  // fail.
+  AssertConfiguringPipelineFails(decoder,
+                                 DownscalingConfig { IntSize(100, 100),
+                                                     SurfaceFormat::B8G8R8A8 },
+                                 PalettedSurfaceConfig { decoder, 0, IntSize(20, 20),
+                                                         IntRect(0, 0, 20, 20),
+                                                         SurfaceFormat::B8G8R8A8, 8,
+                                                         false });
+}
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestDownscalingFilterNoSkia.cpp
@@ -0,0 +1,57 @@
+/* -*- 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 "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfacePipe.h"
+
+// We want to ensure that we're testing the non-Skia fallback version of
+// DownscalingFilter, but there are two issues:
+//  (1) We don't know whether Skia is currently enabled.
+//  (2) If we force disable it, the disabled version will get linked into the
+//      binary and will cause the tests in TestDownscalingFilter to fail.
+// To avoid these problems, we ensure that MOZ_ENABLE_SKIA is defined when
+// including DownscalingFilter.h, and we use the preprocessor to redefine the
+// DownscalingFilter class to DownscalingFilterNoSkia.
+
+#define DownscalingFilter DownscalingFilterNoSkia
+
+#ifdef MOZ_ENABLE_SKIA
+
+#undef MOZ_ENABLE_SKIA
+#include "Common.h"
+#include "DownscalingFilter.h"
+#define MOZ_ENABLE_SKIA
+
+#else
+
+#include "Common.h"
+#include "DownscalingFilter.h"
+
+#endif
+
+#undef DownscalingFilter
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+TEST(ImageDownscalingFilter, NoSkia)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(bool(decoder));
+
+  // Configuring a DownscalingFilter should fail without Skia.
+  AssertConfiguringPipelineFails(decoder,
+                                 DownscalingConfig { IntSize(100, 100),
+                                                     SurfaceFormat::B8G8R8A8 },
+                                 SurfaceConfig { decoder, 0, IntSize(50, 50),
+                                                 SurfaceFormat::B8G8R8A8, false });
+}
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestRemoveFrameRectFilter.cpp
@@ -0,0 +1,565 @@
+/* -*- 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 "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;
+
+template <typename Func> void
+WithRemoveFrameRectFilter(const IntSize& aSize,
+                          const IntRect& aFrameRect,
+                          Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     RemoveFrameRectConfig { aFrameRect },
+                     SurfaceConfig { decoder, 0, aSize,
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+void
+AssertConfiguringRemoveFrameRectFilterFails(const IntSize& aSize,
+                                            const IntRect& aFrameRect)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AssertConfiguringPipelineFails(decoder,
+                                 RemoveFrameRectConfig { aFrameRect },
+                                 SurfaceConfig { decoder, 0, aSize,
+                                                 SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_0_0_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(0, 0, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_0_0_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(0, 0, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_0_0_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(0, 0, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_0_0_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(0, 0, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, 50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_Minus50_50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, 50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_Minus50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, -50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_50_Minus50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, -50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_150_50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(150, 50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_150_50_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(150, 50, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_150_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, 150, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_50_150_0_0)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, 150, 0, 0),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_200_200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(200, 200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_200_200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(200, 200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus200_25_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-200, 25, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_Minus200_25_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-200, 25, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_Minus200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, -200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_25_Minus200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, -200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_200_25_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(200, 25, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_200_25_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(200, 25, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, 200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_25_200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, 200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is zero-size because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus200_Minus200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-200, -200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_Minus200_Minus200_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-200, -200, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 0, 0)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 0, 0)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_Minus50_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, -50, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 0, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_Minus50_Minus50_100_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, -50, 100, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 0, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_Minus50_25_100_50)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, 25, 100, 50),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)),
+                     /* aOutputWriteRect = */ Some(IntRect(0, 25, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_Minus50_25_100_50)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(-50, 25, 100, 50),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)),
+                   /* aOutputWriteRect = */ Some(IntRect(0, 25, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_Minus50_50_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, -50, 50, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 100)),
+                     /* aOutputWriteRect = */ Some(IntRect(25, 0, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_25_Minus50_50_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, -50, 50, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 100)),
+                   /* aOutputWriteRect = */ Some(IntRect(25, 0, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_50_25_100_50)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, 25, 100, 50),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)),
+                     /* aOutputWriteRect = */ Some(IntRect(50, 25, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_50_25_100_50)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(50, 25, 100, 50),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 100, 50)),
+                   /* aOutputWriteRect = */ Some(IntRect(50, 25, 50, 50)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_25_50_50_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, 50, 50, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is 50x50 because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 50)),
+                     /* aOutputWriteRect = */ Some(IntRect(25, 50, 50, 100)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, WriteRows100_100_to_25_50_50_100)
+{
+  WithRemoveFrameRectFilter(IntSize(100, 100),
+                            IntRect(25, 50, 50, 100),
+                            [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    // Note that aInputRect is 50x50 because RemoveFrameRectFilter ignores
+    // trailing rows that don't show up in the output. (Leading rows
+    // unfortunately can't be ignored.)
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(0, 0, 50, 50)),
+                   /* aOutputWriteRect = */ Some(IntRect(25, 50, 50, 100)));
+  });
+}
+
+TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsFor0_0_to_0_0_100_100)
+{
+  // A zero-size image is disallowed.
+  AssertConfiguringRemoveFrameRectFilterFails(IntSize(0, 0),
+                                              IntRect(0, 0, 100, 100));
+}
+
+TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsForMinus1_Minus1_to_0_0_100_100)
+{
+  // A negative-size image is disallowed.
+  AssertConfiguringRemoveFrameRectFilterFails(IntSize(-1, -1),
+                                              IntRect(0, 0, 100, 100));
+}
+
+TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsFor100_100_to_0_0_0_0)
+{
+  // A zero size frame rect is disallowed.
+  AssertConfiguringRemoveFrameRectFilterFails(IntSize(100, 100),
+                                              IntRect(0, 0, -1, -1));
+}
+
+TEST(ImageRemoveFrameRectFilter, RemoveFrameRectFailsFor100_100_to_0_0_Minus1_Minus1)
+{
+  // A negative size frame rect is disallowed.
+  AssertConfiguringRemoveFrameRectFilterFails(IntSize(100, 100),
+                                              IntRect(0, 0, -1, -1));
+}
+
+TEST(ImageRemoveFrameRectFilter, ConfiguringPalettedRemoveFrameRectFails)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // RemoveFrameRectFilter does not support paletted images, so configuration
+  // should fail.
+  AssertConfiguringPipelineFails(decoder,
+                                 RemoveFrameRectConfig { IntRect(0, 0, 50, 50) },
+                                 PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+                                                         IntRect(0, 0, 50, 50),
+                                                         SurfaceFormat::B8G8R8A8, 8,
+                                                         false });
+}
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestSurfacePipeIntegration.cpp
@@ -0,0 +1,322 @@
+/* -*- 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 "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+namespace mozilla {
+namespace image {
+
+class TestSurfacePipeFactory
+{
+public:
+  static SurfacePipe SimpleSurfacePipe()
+  {
+    SurfacePipe pipe;
+    return Move(pipe);
+  }
+
+  template <typename T>
+  static SurfacePipe SurfacePipeFromPipeline(T&& aPipeline)
+  {
+    return SurfacePipe { Move(aPipeline) };
+  }
+
+private:
+  TestSurfacePipeFactory() { }
+};
+
+} // namespace image
+} // namespace mozilla
+
+TEST(ImageSurfacePipeIntegration, SurfacePipe)
+{
+  // Test that SurfacePipe objects can be initialized and move constructed.
+  SurfacePipe pipe = TestSurfacePipeFactory::SimpleSurfacePipe();
+
+  // Test that SurfacePipe objects can be move assigned.
+  pipe = TestSurfacePipeFactory::SimpleSurfacePipe();
+
+  // Test that SurfacePipe objects can be initialized with a pipeline.
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  auto sink = MakeUnique<SurfaceSink>();
+  nsresult rv =
+    sink->Configure(SurfaceConfig { decoder.get(), 0, IntSize(100, 100),
+                                    SurfaceFormat::B8G8R8A8, false });
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  pipe = TestSurfacePipeFactory::SurfacePipeFromPipeline(sink);
+
+  // Test that SurfacePipe passes through method calls to the underlying pipeline.
+  int32_t count = 0;
+  auto result = pipe.WritePixels<uint32_t>([&]() {
+    ++count;
+    return AsVariant(BGRAColor::Green().AsPixel());
+  });
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_EQ(100 * 100, count);
+
+  // Note that we're explicitly testing the SurfacePipe versions of these
+  // methods, so we don't want to use AssertCorrectPipelineFinalState() here.
+  EXPECT_TRUE(pipe.IsSurfaceFinished());
+  Maybe<SurfaceInvalidRect> invalidRect = pipe.TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isSome());
+  EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect);
+  EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect);
+
+  CheckGeneratedImage(decoder, IntRect(0, 0, 100, 100));
+
+  pipe.ResetToFirstRow();
+  EXPECT_FALSE(pipe.IsSurfaceFinished());
+
+  RawAccessFrameRef currentFrame = decoder->GetCurrentFrameRef();
+  currentFrame->Finish();
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceDownscaleWritePixels)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 25, 25)));
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(25, 25),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceDownscaleWriteRows)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 25, 25)));
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(25, 25),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, RemoveFrameRectDownscaleWritePixels)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // Note that aInputWriteRect is 100x50 because RemoveFrameRectFilter ignores
+  // trailing rows that don't show up in the output. (Leading rows unfortunately
+  // can't be ignored.) So the action of the pipeline is as follows:
+  //
+  // (1) RemoveFrameRectFilter reads a 100x50 region of the input.
+  //     (aInputWriteRect captures this fact.) The remaining 50 rows are ignored
+  //     because they extend off the bottom of the image due to the frame rect's
+  //     (50, 50) offset. The 50 columns on the right also don't end up in the
+  //     output, so ultimately only a 50x50 region in the output contains data
+  //     from the input. The filter's output is not 50x50, though, but 100x100,
+  //     because what RemoveFrameRectFilter does is introduce blank rows or
+  //     columns as necessary to transform an image that needs a frame rect into
+  //     an image that doesn't.
+  //
+  // (2) DownscalingFilter reads the output of RemoveFrameRectFilter (100x100)
+  //     and downscales it to 20x20.
+  //
+  // (3) The surface owned by SurfaceSink logically has only a 10x10 region
+  //     region in it that's non-blank; this is the downscaled version of the
+  //     50x50 region discussed in (1). (aOutputWriteRect captures this fact.)
+  //     Some fuzz, as usual, is necessary when dealing with Lanczos downscaling.
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 50)),
+                     /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)),
+                     /* aFuzz = */ 0x33);
+  };
+
+  WithFilterPipeline(decoder, test,
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(20, 20),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, RemoveFrameRectDownscaleWriteRows)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // See the WritePixels version of this test for a discussion of where the
+  // numbers below come from.
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 50)),
+                   /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)),
+                   /* aFuzz = */ 0x33);
+  };
+
+  WithFilterPipeline(decoder, test,
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(20, 20),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectWritePixels)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // Note that aInputRect is the full 100x100 size even though
+  // RemoveFrameRectFilter is part of this pipeline, because deinterlacing
+  // requires reading every row.
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)),
+                     /* aOutputWriteRect = */ Some(IntRect(50, 50, 50, 50)));
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     SurfaceConfig { decoder, 0, IntSize(100, 100),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectWriteRows)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // Note that aInputRect is the full 100x100 size even though
+  // RemoveFrameRectFilter is part of this pipeline, because deinterlacing
+  // requires reading every row.
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)),
+                   /* aOutputWriteRect = */ Some(IntRect(50, 50, 50, 50)));
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     SurfaceConfig { decoder, 0, IntSize(100, 100),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectDownscaleWritePixels)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWritePixels(aDecoder, aFilter,
+                     /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)),
+                     /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                     /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)),
+                     /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)),
+                     /* aFuzz = */ 33);
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(20, 20),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectDownscaleWriteRows)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  auto test = [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    CheckWriteRows(aDecoder, aFilter,
+                   /* aOutputRect = */ Some(IntRect(0, 0, 20, 20)),
+                   /* aInputRect = */ Some(IntRect(0, 0, 100, 100)),
+                   /* aInputWriteRect = */ Some(IntRect(50, 50, 100, 100)),
+                   /* aOutputWriteRect = */ Some(IntRect(10, 10, 10, 10)),
+                   /* aFuzz = */ 33);
+  };
+
+  WithFilterPipeline(decoder, test,
+                     DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true },
+                     RemoveFrameRectConfig { IntRect(50, 50, 100, 100) },
+                     DownscalingConfig { IntSize(100, 100),
+                                         SurfaceFormat::B8G8R8A8 },
+                     SurfaceConfig { decoder, 0, IntSize(20, 20),
+                                     SurfaceFormat::B8G8R8A8, false });
+}
+
+TEST(ImageSurfacePipeIntegration, ConfiguringPalettedRemoveFrameRectDownscaleFails)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // This is an invalid pipeline for paletted images, so configuration should
+  // fail.
+  AssertConfiguringPipelineFails(decoder,
+                                 RemoveFrameRectConfig { IntRect(0, 0, 50, 50) },
+                                 DownscalingConfig { IntSize(100, 100),
+                                                     SurfaceFormat::B8G8R8A8 },
+                                 PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+                                                         IntRect(0, 0, 50, 50),
+                                                         SurfaceFormat::B8G8R8A8, 8,
+                                                         false });
+}
+
+TEST(ImageSurfacePipeIntegration, ConfiguringPalettedDeinterlaceDownscaleFails)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  // This is an invalid pipeline for paletted images, so configuration should
+  // fail.
+  AssertConfiguringPipelineFails(decoder,
+                                 DeinterlacingConfig<uint8_t> { /* mProgressiveDisplay = */ true},
+                                 DownscalingConfig { IntSize(100, 100),
+                                                     SurfaceFormat::B8G8R8A8 },
+                                 PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+                                                         IntRect(0, 0, 20, 20),
+                                                         SurfaceFormat::B8G8R8A8, 8,
+                                                         false });
+}
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestSurfaceSink.cpp
@@ -0,0 +1,578 @@
+/* -*- 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 "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+enum class Orient
+{
+  NORMAL,
+  FLIP_VERTICALLY
+};
+
+template <Orient Orientation, typename Func> void
+WithSurfaceSink(Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  const bool flipVertically = Orientation == Orient::FLIP_VERTICALLY;
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     SurfaceConfig { decoder, 0, IntSize(100, 100),
+                                     SurfaceFormat::B8G8R8A8, flipVertically });
+}
+
+template <typename Func> void
+WithPalettedSurfaceSink(const IntRect& aFrameRect, Func aFunc)
+{
+  RefPtr<Decoder> decoder = CreateTrivialDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  WithFilterPipeline(decoder, Forward<Func>(aFunc),
+                     PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+                                             aFrameRect, SurfaceFormat::B8G8R8A8,
+                                             8, false });
+}
+
+TEST(ImageSurfaceSink, NullSurfaceSink)
+{
+  // Create the NullSurfaceSink.
+  NullSurfaceSink sink;
+  nsresult rv = sink.Configure(NullSurfaceConfig { });
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+  EXPECT_TRUE(!sink.IsValidPalettedPipe());
+
+  // Ensure that we can't write anything.
+  bool gotCalled = false;
+  auto result = sink.WritePixels<uint32_t>([&]() {
+    gotCalled = true;
+    return AsVariant(BGRAColor::Green().AsPixel());
+  });
+  EXPECT_FALSE(gotCalled);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_TRUE(sink.IsSurfaceFinished());
+  Maybe<SurfaceInvalidRect> invalidRect = sink.TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  result = sink.WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+    gotCalled = true;
+    for (; aLength > 0; --aLength, ++aRow) {
+      *aRow = BGRAColor::Green().AsPixel();
+    }
+    return Nothing();
+  });
+  EXPECT_FALSE(gotCalled);
+  EXPECT_EQ(WriteState::FINISHED, result);
+  EXPECT_TRUE(sink.IsSurfaceFinished());
+  invalidRect = sink.TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Attempt to advance to the next row and make sure nothing changes.
+  sink.AdvanceRow();
+  EXPECT_TRUE(sink.IsSurfaceFinished());
+  invalidRect = sink.TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+
+  // Attempt to advance to the next pass and make sure nothing changes.
+  sink.ResetToFirstRow();
+  EXPECT_TRUE(sink.IsSurfaceFinished());
+  invalidRect = sink.TakeInvalidRect();
+  EXPECT_TRUE(invalidRect.isNothing());
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixels)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    CheckWritePixels(aDecoder, aSink);
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteRows)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    CheckWriteRows(aDecoder, aSink);
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsFinish)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    // Write nothing into the surface; just finish immediately.
+    uint32_t count = 0;
+    auto result = aSink->WritePixels<uint32_t>([&]() {
+      count++;
+      return AsVariant(WriteState::FINISHED);
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(1u, count);
+    EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+    // Attempt to write more and make sure that nothing gets written.
+    count = 0;
+    result = aSink->WritePixels<uint32_t>([&]() {
+      count++;
+      return AsVariant(BGRAColor::Red().AsPixel());
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(0u, count);
+    EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+    // Check that the generated image is correct.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Transparent()));
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteRowsFinish)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    // Write nothing into the surface; just finish immediately.
+    uint32_t count = 0;
+    auto result = aSink->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      count++;
+      return Some(WriteState::FINISHED);
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(1u, count);
+    EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+    // Attempt to write more and make sure that nothing gets written.
+    count = 0;
+    result = aSink->WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      count++;
+      for (; aLength > 0; --aLength, ++aRow) {
+        *aRow = BGRAColor::Green().AsPixel();
+      }
+      return Nothing();
+    });
+    EXPECT_EQ(WriteState::FINISHED, result);
+    EXPECT_EQ(0u, count);
+    EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+    // Check that the generated image is correct.
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+    EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Transparent()));
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkProgressivePasses)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    {
+      // Fill the image with a first pass of red.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        ++count;
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+      EXPECT_EQ(100u * 100u, count);
+
+      AssertCorrectPipelineFinalState(aSink,
+                                      IntRect(0, 0, 100, 100),
+                                      IntRect(0, 0, 100, 100));
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+    }
+
+    {
+      // Reset for the second pass.
+      aSink->ResetToFirstRow();
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isNothing());
+
+      // Check that the generated image is still the first pass image.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+    }
+
+    {
+      // Fill the image with a second pass of green.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        ++count;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+      EXPECT_EQ(100u * 100u, count);
+
+      AssertCorrectPipelineFinalState(aSink,
+                                      IntRect(0, 0, 100, 100),
+                                      IntRect(0, 0, 100, 100));
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green()));
+    }
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkInvalidRect)
+{
+  WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+    {
+      // Write one row.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+        if (count == 100) {
+          return AsVariant(WriteState::NEED_MORE_DATA);
+        }
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_EQ(100u, count);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we have the right invalid rect.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isSome());
+      EXPECT_EQ(IntRect(0, 0, 100, 1), invalidRect->mInputSpaceRect);
+      EXPECT_EQ(IntRect(0, 0, 100, 1), invalidRect->mOutputSpaceRect);
+    }
+
+    {
+      // Write eight rows.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+        if (count == 100 * 8) {
+          return AsVariant(WriteState::NEED_MORE_DATA);
+        }
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_EQ(100u * 8u, count);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we have the right invalid rect.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isSome());
+      EXPECT_EQ(IntRect(0, 1, 100, 8), invalidRect->mInputSpaceRect);
+      EXPECT_EQ(IntRect(0, 1, 100, 8), invalidRect->mOutputSpaceRect);
+    }
+
+    {
+      // Write the left half of one row.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+        if (count == 50) {
+          return AsVariant(WriteState::NEED_MORE_DATA);
+        }
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_EQ(50u, count);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we don't have an invalid rect, since the invalid rect only
+      // gets updated when a row gets completed.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isNothing());
+    }
+
+    {
+      // Write the right half of the same row.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+        if (count == 50) {
+          return AsVariant(WriteState::NEED_MORE_DATA);
+        }
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_EQ(50u, count);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we have the right invalid rect, which will include both the
+      // left and right halves of this row now that we've completed it.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isSome());
+      EXPECT_EQ(IntRect(0, 9, 100, 1), invalidRect->mInputSpaceRect);
+      EXPECT_EQ(IntRect(0, 9, 100, 1), invalidRect->mOutputSpaceRect);
+    }
+
+    {
+      // Write no rows.
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        return AsVariant(WriteState::NEED_MORE_DATA);
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we don't have an invalid rect.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isNothing());
+    }
+
+    {
+      // Fill the rest of the image.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+      EXPECT_EQ(100u * 90u, count);
+      EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+      // Assert that we have the right invalid rect.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isSome());
+      EXPECT_EQ(IntRect(0, 10, 100, 90), invalidRect->mInputSpaceRect);
+      EXPECT_EQ(IntRect(0, 10, 100, 90), invalidRect->mOutputSpaceRect);
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green()));
+    }
+  });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkFlipVertically)
+{
+  WithSurfaceSink<Orient::FLIP_VERTICALLY>([](Decoder* aDecoder,
+                                              SurfaceSink* aSink) {
+    {
+      // Fill the image with a first pass of red.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        ++count;
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+      EXPECT_EQ(100u * 100u, count);
+
+      AssertCorrectPipelineFinalState(aSink,
+                                      IntRect(0, 0, 100, 100),
+                                      IntRect(0, 0, 100, 100));
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+    }
+
+    {
+      // Reset for the second pass.
+      aSink->ResetToFirstRow();
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isNothing());
+
+      // Check that the generated image is still the first pass image.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+    }
+
+    {
+      // Fill 25 rows of the image with green and make sure everything is OK.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+        if (count == 25 * 100) {
+          return AsVariant(WriteState::NEED_MORE_DATA);
+        }
+        count++;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+      EXPECT_EQ(25u * 100u, count);
+      EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+      // Assert that we have the right invalid rect, which should include the
+      // *bottom* (since we're flipping vertically) 25 rows of the image.
+      Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+      EXPECT_TRUE(invalidRect.isSome());
+      EXPECT_EQ(IntRect(0, 75, 100, 25), invalidRect->mInputSpaceRect);
+      EXPECT_EQ(IntRect(0, 75, 100, 25), invalidRect->mOutputSpaceRect);
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(RowsAreSolidColor(surface, 0, 75, BGRAColor::Red()));
+      EXPECT_TRUE(RowsAreSolidColor(surface, 75, 25, BGRAColor::Green()));
+    }
+
+    {
+      // Fill the rest of the image with a second pass of green.
+      uint32_t count = 0;
+      auto result = aSink->WritePixels<uint32_t>([&]() {
+        ++count;
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+      EXPECT_EQ(75u * 100u, count);
+
+      AssertCorrectPipelineFinalState(aSink,
+                                      IntRect(0, 0, 100, 75),
+                                      IntRect(0, 0, 100, 75));
+
+      // Check that the generated image is correct.
+      RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+      RefPtr<SourceSurface> surface = currentFrame->GetSurface();
+      EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green()));
+    }
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsFor0_0_100_100)
+{
+  WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink);
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsFor0_0_100_100)
+{
+  WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink);
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsFor25_25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(25, 25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink,
+                             /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputWriteRect = */ Some(IntRect(25, 25, 50, 50)),
+                             /* aOutputWriteRect = */ Some(IntRect(25, 25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsFor25_25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(25, 25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink,
+                           /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputWriteRect = */ Some(IntRect(25, 25, 50, 50)),
+                           /* aOutputWriteRect = */ Some(IntRect(25, 25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsForMinus25_Minus25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(-25, -25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink,
+                             /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputWriteRect = */ Some(IntRect(-25, -25, 50, 50)),
+                             /* aOutputWriteRect = */ Some(IntRect(-25, -25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsForMinus25_Minus25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(-25, -25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink,
+                           /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputWriteRect = */ Some(IntRect(-25, -25, 50, 50)),
+                           /* aOutputWriteRect = */ Some(IntRect(-25, -25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsFor75_Minus25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(75, -25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink,
+                             /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputWriteRect = */ Some(IntRect(75, -25, 50, 50)),
+                             /* aOutputWriteRect = */ Some(IntRect(75, -25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsFor75_Minus25_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(75, -25, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink,
+                           /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputWriteRect = */ Some(IntRect(75, -25, 50, 50)),
+                           /* aOutputWriteRect = */ Some(IntRect(75, -25, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsForMinus25_75_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(-25, 75, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink,
+                             /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputWriteRect = */ Some(IntRect(-25, 75, 50, 50)),
+                             /* aOutputWriteRect = */ Some(IntRect(-25, 75, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsForMinus25_75_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(-25, 75, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink,
+                           /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputWriteRect = */ Some(IntRect(-25, 75, 50, 50)),
+                           /* aOutputWriteRect = */ Some(IntRect(-25, 75, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsFor75_75_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(75, 75, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWritePixels(aDecoder, aSink,
+                             /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                             /* aInputWriteRect = */ Some(IntRect(75, 75, 50, 50)),
+                             /* aOutputWriteRect = */ Some(IntRect(75, 75, 50, 50)));
+  });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteRowsFor75_75_50_50)
+{
+  WithPalettedSurfaceSink(IntRect(75, 75, 50, 50),
+                          [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+    CheckPalettedWriteRows(aDecoder, aSink,
+                           /* aOutputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputRect = */ Some(IntRect(0, 0, 50, 50)),
+                           /* aInputWriteRect = */ Some(IntRect(75, 75, 50, 50)),
+                           /* aOutputWriteRect = */ Some(IntRect(75, 75, 50, 50)));
+  });
+}
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -6,18 +6,32 @@
 
 Library('imagetest')
 
 UNIFIED_SOURCES = [
     'Common.cpp',
     'TestCopyOnWrite.cpp',
     'TestDecoders.cpp',
     'TestDecodeToSurface.cpp',
+    'TestDeinterlacingFilter.cpp',
     'TestMetadata.cpp',
+    'TestRemoveFrameRectFilter.cpp',
     'TestStreamingLexer.cpp',
+    'TestSurfaceSink.cpp',
+]
+
+if CONFIG['MOZ_ENABLE_SKIA']:
+    UNIFIED_SOURCES += [
+        'TestDownscalingFilter.cpp',
+        'TestSurfacePipeIntegration.cpp',
+    ]
+
+SOURCES += [
+    # Can't be unified because it manipulates the preprocessor environment.
+    'TestDownscalingFilterNoSkia.cpp',
 ]
 
 TEST_HARNESS_FILES.gtest += [
     'corrupt.jpg',
     'first-frame-green.gif',
     'first-frame-green.png',
     'first-frame-padding.gif',
     'green.bmp',
@@ -29,13 +43,19 @@ TEST_HARNESS_FILES.gtest += [
     'no-frame-delay.gif',
     'rle4.bmp',
     'rle8.bmp',
     'transparent-if-within-ico.bmp',
     'transparent.gif',
     'transparent.png',
 ]
 
+include('/ipc/chromium/chromium-config.mozbuild')
+
 LOCAL_INCLUDES += [
+    '/dom/base',
+    '/gfx/2d',
     '/image',
 ]
 
+LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES']
+
 FINAL_LIBRARY = 'xul-gtest'