Bug 1246851 (Part 2) - Add SurfaceFilter implementations for basic surface output operations. r=njn
authorSeth Fowler <mark.seth.fowler@gmail.com>
Thu, 25 Feb 2016 16:21:29 -0800
changeset 334806 e24b148198373a064a9544099882accbf2f8ca46
parent 334805 de2f01e26094f9c4bb9a42421ee07219278dcb4d
child 334807 db31b23b3652f29a4c136c188379b6e4d3ae0684
push id11645
push userrjesup@wgate.com
push dateFri, 26 Feb 2016 05:38:04 +0000
reviewersnjn
bugs1246851
milestone47.0a1
Bug 1246851 (Part 2) - Add SurfaceFilter implementations for basic surface output operations. r=njn
image/DownscalingFilter.h
image/SurfaceFilters.h
new file mode 100644
--- /dev/null
+++ b/image/DownscalingFilter.h
@@ -0,0 +1,381 @@
+/* -*- 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/. */
+
+/**
+ * DownscalingSurfaceFilter is a SurfaceFilter implementation for use with
+ * SurfacePipe which performs Lanczos downscaling.
+ *
+ * It's in this header file, separated from the other SurfaceFilters, because
+ * some preprocessor magic is necessary to ensure that there aren't compilation
+ * issues on platforms where Skia is unavailable.
+ */
+
+#ifndef mozilla_image_DownscalingFilter_h
+#define mozilla_image_DownscalingFilter_h
+
+#include <algorithm>
+#include <ctime>
+#include <stdint.h>
+#include <string.h>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/SSE.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+#include "gfxPrefs.h"
+
+#ifdef MOZ_ENABLE_SKIA
+#include "convolver.h"
+#include "image_operations.h"
+#include "skia/include/core/SkTypes.h"
+#endif
+
+#include "SurfacePipe.h"
+
+namespace mozilla {
+namespace image {
+
+//////////////////////////////////////////////////////////////////////////////
+// DownscalingFilter
+//////////////////////////////////////////////////////////////////////////////
+
+template <typename Next> class DownscalingFilter;
+
+/**
+ * A configuration struct for DownscalingConfig.
+ */
+struct DownscalingConfig
+{
+  template <typename Next> using Filter = DownscalingFilter<Next>;
+  gfx::IntSize mInputSize;     /// The size of the input image. We'll downscale
+                               /// from this size to the input size of the next
+                               /// SurfaceFilter in the chain.
+  gfx::SurfaceFormat mFormat;  /// The pixel format - BGRA or BGRX. (BGRX has
+                               /// slightly better performance.)
+};
+
+#ifndef MOZ_ENABLE_SKIA
+
+/**
+ * DownscalingFilter requires Skia. This is a fallback implementation for
+ * non-Skia builds that fails when Configure() is called (which will prevent
+ * SurfacePipeFactory from returning an instance of it) and crashes if a caller
+ * manually constructs an instance and attempts to actually use it. Callers
+ * should avoid this by ensuring that they do not request downscaling in
+ * non-Skia builds.
+ */
+template <typename Next>
+class DownscalingFilter final : public SurfaceFilter
+{
+public:
+  uint8_t* AdvanceRow() override { MOZ_CRASH(); return nullptr; }
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override { return Nothing(); }
+
+  template <typename... Rest>
+  nsresult Configure(const DownscalingConfig& aConfig, Rest... aRest)
+  {
+    return NS_ERROR_FAILURE;
+  }
+
+protected:
+  uint8_t* DoResetToFirstRow() override { MOZ_CRASH(); return nullptr; }
+};
+
+#else
+
+/**
+ * DownscalingFilter performs Lanczos downscaling, taking image input data at one size
+ * and outputting it rescaled to a different size.
+ *
+ * The 'Next' template parameter specifies the next filter in the chain.
+ */
+template <typename Next>
+class DownscalingFilter final : public SurfaceFilter
+{
+public:
+  DownscalingFilter()
+    : mXFilter(MakeUnique<skia::ConvolutionFilter1D>())
+    , mYFilter(MakeUnique<skia::ConvolutionFilter1D>())
+    , mWindowCapacity(0)
+    , mRowsInWindow(0)
+    , mInputRow(0)
+    , mOutputRow(0)
+    , mHasAlpha(true)
+  {
+    MOZ_ASSERT(gfxPrefs::ImageDownscaleDuringDecodeEnabled(),
+               "Downscaling even though downscale-during-decode is disabled?");
+  }
+
+  ~DownscalingFilter()
+  {
+    ReleaseWindow();
+  }
+
+  template <typename... Rest>
+  nsresult Configure(const DownscalingConfig& aConfig, Rest... aRest)
+  {
+    nsresult rv = mNext.Configure(aRest...);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    if (mNext.IsValidPalettedPipe()) {
+      NS_WARNING("Created a downscaler for a paletted surface?");
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (mNext.InputSize() == aConfig.mInputSize) {
+      NS_WARNING("Created a downscaler, but not downscaling?");
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (mNext.InputSize().width > aConfig.mInputSize.width) {
+      NS_WARNING("Created a downscaler, but width is larger");
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (mNext.InputSize().height > aConfig.mInputSize.height) {
+      NS_WARNING("Created a downscaler, but height is larger");
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (aConfig.mInputSize.width <= 0 || aConfig.mInputSize.height <= 0) {
+      NS_WARNING("Invalid input size for DownscalingFilter");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    mInputSize = aConfig.mInputSize;
+    gfx::IntSize outputSize = mNext.InputSize();
+    mScale = gfxSize(double(mInputSize.width) / outputSize.width,
+                     double(mInputSize.height) / outputSize.height);
+    mHasAlpha = aConfig.mFormat == gfx::SurfaceFormat::B8G8R8A8;
+
+    ReleaseWindow();
+
+    auto resizeMethod = skia::ImageOperations::RESIZE_LANCZOS3;
+
+    skia::resize::ComputeFilters(resizeMethod,
+                                 mInputSize.width, outputSize.width,
+                                 0, outputSize.width,
+                                 mXFilter.get());
+
+    skia::resize::ComputeFilters(resizeMethod,
+                                 mInputSize.height, outputSize.height,
+                                 0, outputSize.height,
+                                 mYFilter.get());
+
+    // Allocate the buffer, which contains scanlines of the input image.
+    mRowBuffer.reset(new (fallible)
+                       uint8_t[PaddedWidthInBytes(mInputSize.width)]);
+    if (MOZ_UNLIKELY(!mRowBuffer)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    // Clear the buffer to avoid writing uninitialized memory to the output.
+    memset(mRowBuffer.get(), 0, PaddedWidthInBytes(mInputSize.width));
+
+    // Allocate the window, which contains horizontally downscaled scanlines. (We
+    // can store scanlines which are already downscaled because our downscaling
+    // filter is separable.)
+    mWindowCapacity = mYFilter->max_filter();
+    mWindow.reset(new (fallible) uint8_t*[mWindowCapacity]);
+    if (MOZ_UNLIKELY(!mWindow)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    // Allocate the "window" of recent rows that we keep in memory as input for
+    // the downscaling code. We intentionally iterate through the entire array
+    // even if an allocation fails, to ensure that all the pointers in it are
+    // either valid or nullptr. That in turn ensures that ReleaseWindow() can
+    // clean up correctly.
+    bool anyAllocationFailed = false;
+    const uint32_t windowRowSizeInBytes = PaddedWidthInBytes(outputSize.width);
+    for (int32_t i = 0; i < mWindowCapacity; ++i) {
+      mWindow[i] = new (fallible) uint8_t[windowRowSizeInBytes];
+      anyAllocationFailed = anyAllocationFailed || mWindow[i] == nullptr;
+    }
+
+    if (MOZ_UNLIKELY(anyAllocationFailed)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    ConfigureFilter(mInputSize, sizeof(uint32_t));
+    return NS_OK;
+  }
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override
+  {
+    Maybe<SurfaceInvalidRect> invalidRect = mNext.TakeInvalidRect();
+
+    if (invalidRect) {
+      // Compute the input space invalid rect by scaling.
+      invalidRect->mInputSpaceRect.ScaleRoundOut(mScale.width, mScale.height);
+    }
+
+    return invalidRect;
+  }
+
+  uint8_t* AdvanceRow() override
+  {
+    if (mInputRow >= mInputSize.height) {
+      NS_WARNING("Advancing DownscalingFilter past the end of the input");
+      return nullptr;
+    }
+
+    if (mOutputRow >= mNext.InputSize().height) {
+      NS_WARNING("Advancing DownscalingFilter past the end of the output");
+      return nullptr;
+    }
+
+    int32_t filterOffset = 0;
+    int32_t filterLength = 0;
+    GetFilterOffsetAndLength(mYFilter, mOutputRow,
+                             &filterOffset, &filterLength);
+
+    int32_t inputRowToRead = filterOffset + mRowsInWindow;
+    MOZ_ASSERT(mInputRow <= inputRowToRead, "Reading past end of input");
+    if (mInputRow == inputRowToRead) {
+      skia::ConvolveHorizontally(mRowBuffer.get(), *mXFilter,
+                                 mWindow[mRowsInWindow++], mHasAlpha,
+                                 supports_sse2());
+    }
+
+    MOZ_ASSERT(mOutputRow < mNext.InputSize().height,
+               "Writing past end of output");
+
+    while (mRowsInWindow == filterLength) {
+      DownscaleInputRow();
+
+      if (mOutputRow == mNext.InputSize().height) {
+        break;  // We're done.
+      }
+
+      GetFilterOffsetAndLength(mYFilter, mOutputRow,
+                               &filterOffset, &filterLength);
+    }
+
+    mInputRow++;
+
+    return mInputRow < mInputSize.height ? GetRowPointer()
+                                         : nullptr;
+  }
+
+protected:
+  uint8_t* DoResetToFirstRow() override
+  {
+    mNext.ResetToFirstRow();
+
+    mInputRow = 0;
+    mOutputRow = 0;
+    mRowsInWindow = 0;
+
+    return GetRowPointer();
+  }
+
+private:
+  uint8_t* GetRowPointer() const { return mRowBuffer.get(); }
+
+  static uint32_t PaddedWidthInBytes(uint32_t aLogicalWidth)
+  {
+    // Convert from width in BGRA/BGRX pixels to width in bytes, padding by 15
+    // to handle overreads by the SIMD code inside Skia.
+    return aLogicalWidth * sizeof(uint32_t) + 15;
+  }
+
+  static void
+  GetFilterOffsetAndLength(UniquePtr<skia::ConvolutionFilter1D>& aFilter,
+                           int32_t aOutputImagePosition,
+                           int32_t* aFilterOffsetOut,
+                           int32_t* aFilterLengthOut)
+  {
+    MOZ_ASSERT(aOutputImagePosition < aFilter->num_values());
+    aFilter->FilterForValue(aOutputImagePosition,
+                            aFilterOffsetOut,
+                            aFilterLengthOut);
+  }
+
+  void DownscaleInputRow()
+  {
+    typedef skia::ConvolutionFilter1D::Fixed FilterValue;
+
+    MOZ_ASSERT(mOutputRow < mNext.InputSize().height,
+               "Writing past end of output");
+
+    int32_t filterOffset = 0;
+    int32_t filterLength = 0;
+    MOZ_ASSERT(mOutputRow < mYFilter->num_values());
+    auto filterValues =
+      mYFilter->FilterForValue(mOutputRow, &filterOffset, &filterLength);
+
+    mNext.template WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength)
+            -> Maybe<WriteState> {
+      skia::ConvolveVertically(static_cast<const FilterValue*>(filterValues),
+                               filterLength, mWindow.get(), mXFilter->num_values(),
+                               reinterpret_cast<uint8_t*>(aRow), mHasAlpha,
+                               supports_sse2());
+      return Some(WriteState::NEED_MORE_DATA);
+    });
+
+    mOutputRow++;
+
+    if (mOutputRow == mNext.InputSize().height) {
+      return;  // We're done.
+    }
+
+    int32_t newFilterOffset = 0;
+    int32_t newFilterLength = 0;
+    GetFilterOffsetAndLength(mYFilter, mOutputRow,
+                             &newFilterOffset, &newFilterLength);
+
+    int diff = newFilterOffset - filterOffset;
+    MOZ_ASSERT(diff >= 0, "Moving backwards in the filter?");
+
+    // Shift the buffer. We're just moving pointers here, so this is cheap.
+    mRowsInWindow -= diff;
+    mRowsInWindow = std::max(mRowsInWindow, 0);
+    for (int32_t i = 0; i < mRowsInWindow; ++i) {
+      std::swap(mWindow[i], mWindow[filterLength - mRowsInWindow + i]);
+    }
+  }
+
+  void ReleaseWindow()
+  {
+    if (!mWindow) {
+      return;
+    }
+
+    for (int32_t i = 0; i < mWindowCapacity; ++i) {
+      delete[] mWindow[i];
+    }
+
+    mWindow = nullptr;
+    mWindowCapacity = 0;
+  }
+
+  Next mNext;                       /// The next SurfaceFilter in the chain.
+
+  gfx::IntSize mInputSize;          /// The size of the input image.
+  gfxSize mScale;                   /// The scale factors in each dimension.
+                                    /// Computed from @mInputSize and
+                                    /// the next filter's input size.
+
+  UniquePtr<uint8_t[]> mRowBuffer;  /// The buffer into which input is written.
+  UniquePtr<uint8_t*[]> mWindow;    /// The last few rows which were written.
+
+  UniquePtr<skia::ConvolutionFilter1D> mXFilter;  /// The Lanczos filter in X.
+  UniquePtr<skia::ConvolutionFilter1D> mYFilter;  /// The Lanczos filter in Y.
+
+  int32_t mWindowCapacity;  /// How many rows the window contains.
+
+  int32_t mRowsInWindow;    /// How many rows we've buffered in the window.
+  int32_t mInputRow;        /// The current row we're reading. (0-indexed)
+  int32_t mOutputRow;       /// The current row we're writing. (0-indexed)
+
+  bool mHasAlpha;           /// If true, the image has transparency.
+};
+
+#endif
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_DownscalingFilter_h
new file mode 100644
--- /dev/null
+++ b/image/SurfaceFilters.h
@@ -0,0 +1,560 @@
+/* -*- 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/. */
+
+/**
+ * This header contains various SurfaceFilter implementations that apply
+ * transformations to image data, for usage with SurfacePipe.
+ */
+
+#ifndef mozilla_image_SurfaceFilters_h
+#define mozilla_image_SurfaceFilters_h
+
+#include <stdint.h>
+#include <string.h>
+
+#include "mozilla/Likely.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+
+#include "DownscalingFilter.h"
+#include "SurfacePipe.h"
+
+namespace mozilla {
+namespace image {
+
+//////////////////////////////////////////////////////////////////////////////
+// DeinterlacingFilter
+//////////////////////////////////////////////////////////////////////////////
+
+template <typename PixelType, typename Next> class DeinterlacingFilter;
+
+/**
+ * A configuration struct for DeinterlacingFilter.
+ *
+ * The 'PixelType' template parameter should be either uint32_t (for output to a
+ * SurfaceSink) or uint8_t (for output to a PalettedSurfaceSink).
+ */
+template <typename PixelType>
+struct DeinterlacingConfig
+{
+  template <typename Next> using Filter = DeinterlacingFilter<PixelType, Next>;
+  bool mProgressiveDisplay; /// If true, duplicate rows during deinterlacing
+                            /// to make progressive display look better, at
+                            /// the cost of some performance.
+};
+
+/**
+ * DeinterlacingFilter performs deinterlacing by reordering the rows that are
+ * written to it.
+ *
+ * The 'PixelType' template parameter should be either uint32_t (for output to a
+ * SurfaceSink) or uint8_t (for output to a PalettedSurfaceSink).
+ *
+ * The 'Next' template parameter specifies the next filter in the chain.
+ */
+template <typename PixelType, typename Next>
+class DeinterlacingFilter final : public SurfaceFilter
+{
+public:
+  DeinterlacingFilter()
+    : mInputRow(0)
+    , mOutputRow(0)
+    , mPass(0)
+    , mProgressiveDisplay(true)
+  { }
+
+  template <typename... Rest>
+  nsresult Configure(const DeinterlacingConfig<PixelType>& aConfig, Rest... aRest)
+  {
+    nsresult rv = mNext.Configure(aRest...);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    if (sizeof(PixelType) == 1 && !mNext.IsValidPalettedPipe()) {
+      NS_WARNING("Paletted DeinterlacingFilter used with non-paletted pipe?");
+      return NS_ERROR_INVALID_ARG;
+    }
+    if (sizeof(PixelType) == 4 && mNext.IsValidPalettedPipe()) {
+      NS_WARNING("Non-paletted DeinterlacingFilter used with paletted pipe?");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    gfx::IntSize outputSize = mNext.InputSize();
+    mProgressiveDisplay = aConfig.mProgressiveDisplay;
+
+    const uint32_t bufferSize = outputSize.width *
+                                outputSize.height *
+                                sizeof(PixelType);
+
+    // Allocate the buffer, which contains deinterlaced scanlines of the image.
+    // The buffer is necessary so that we can output rows which have already
+    // been deinterlaced again on subsequent passes. Since a later stage in the
+    // pipeline may be transforming the rows it receives (for example, by
+    // downscaling them), the rows may no longer exist in their original form on
+    // the surface itself.
+    mBuffer.reset(new (fallible) uint8_t[bufferSize]);
+    if (MOZ_UNLIKELY(!mBuffer)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    // Clear the buffer to avoid writing uninitialized memory to the output.
+    memset(mBuffer.get(), 0, bufferSize);
+
+    ConfigureFilter(outputSize, sizeof(PixelType));
+    return NS_OK;
+  }
+
+  bool IsValidPalettedPipe() const override
+  {
+    return sizeof(PixelType) == 1 && mNext.IsValidPalettedPipe();
+  }
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override
+  {
+    return mNext.TakeInvalidRect();
+  }
+
+  uint8_t* AdvanceRow() override
+  {
+    if (mPass >= 4) {
+      return nullptr;  // We already finished all passes.
+    }
+    if (mInputRow >= InputSize().height) {
+      return nullptr;  // We already got all the input rows we expect.
+    }
+
+    // Duplicate from the first Haeberli row to the remaining Haeberli rows
+    // within the buffer.
+    DuplicateRows(HaeberliOutputStartRow(mPass, mProgressiveDisplay, mOutputRow),
+                  HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                         InputSize(), mOutputRow));
+
+    // Write the current set of Haeberli rows (which contains the current row)
+    // to the next stage in the pipeline.
+    OutputRows(HaeberliOutputStartRow(mPass, mProgressiveDisplay, mOutputRow),
+               HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                      InputSize(), mOutputRow));
+
+    // Determine which output row the next input row corresponds to.
+    bool advancedPass = false;
+    uint32_t stride = InterlaceStride(mPass);
+    int32_t nextOutputRow = mOutputRow + stride;
+    while (nextOutputRow >= InputSize().height) {
+      // Copy any remaining rows from the buffer.
+      if (!advancedPass) {
+        DuplicateRows(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                             InputSize(), mOutputRow),
+                      InputSize().height);
+        OutputRows(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                          InputSize(), mOutputRow),
+                   InputSize().height);
+      }
+
+      // We finished the current pass; advance to the next one.
+      mPass++;
+      if (mPass >= 4) {
+        return nullptr;  // Finished all passes.
+      }
+
+      // Tell the next pipeline stage that we're starting the next pass.
+      mNext.ResetToFirstRow();
+
+      // Update our state to reflect the pass change.
+      advancedPass = true;
+      stride = InterlaceStride(mPass);
+      nextOutputRow = InterlaceOffset(mPass);
+    }
+
+    MOZ_ASSERT(nextOutputRow >= 0);
+    MOZ_ASSERT(nextOutputRow < InputSize().height);
+
+    MOZ_ASSERT(HaeberliOutputStartRow(mPass, mProgressiveDisplay,
+                                      nextOutputRow) >= 0);
+    MOZ_ASSERT(HaeberliOutputStartRow(mPass, mProgressiveDisplay,
+                                      nextOutputRow) < InputSize().height);
+    MOZ_ASSERT(HaeberliOutputStartRow(mPass, mProgressiveDisplay,
+                                      nextOutputRow) <= nextOutputRow);
+
+    MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                      InputSize(), nextOutputRow) >= 0);
+    MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                      InputSize(), nextOutputRow)
+                 <= InputSize().height);
+    MOZ_ASSERT(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                      InputSize(), nextOutputRow)
+                 > nextOutputRow);
+
+    int32_t nextHaeberliOutputRow =
+      HaeberliOutputStartRow(mPass, mProgressiveDisplay, nextOutputRow);
+
+    // Copy rows from the buffer until we reach the desired output row.
+    if (advancedPass) {
+      OutputRows(0, nextHaeberliOutputRow);
+    } else {
+      OutputRows(HaeberliOutputUntilRow(mPass, mProgressiveDisplay,
+                                        InputSize(), mOutputRow),
+                 nextHaeberliOutputRow);
+    }
+
+    // Update our position within the buffer.
+    mInputRow++;
+    mOutputRow = nextOutputRow;
+
+    // We'll actually write to the first Haeberli output row, then copy it until
+    // we reach the last Haeberli output row. The assertions above make sure
+    // this always includes mOutputRow.
+    return GetRowPointer(nextHaeberliOutputRow);
+  }
+
+protected:
+  uint8_t* DoResetToFirstRow() override
+  {
+    mNext.ResetToFirstRow();
+    mPass = 0;
+    mInputRow = 0;
+    mOutputRow = InterlaceOffset(mPass);;
+    return GetRowPointer(mOutputRow);
+  }
+
+private:
+  static uint32_t InterlaceOffset(uint32_t aPass)
+  {
+    MOZ_ASSERT(aPass < 4, "Invalid pass");
+    static const uint8_t offset[] = { 0, 4, 2, 1 };
+    return offset[aPass];
+  }
+
+  static uint32_t InterlaceStride(uint32_t aPass)
+  {
+    MOZ_ASSERT(aPass < 4, "Invalid pass");
+    static const uint8_t stride[] = { 8, 8, 4, 2 };
+    return stride[aPass];
+  }
+
+  static int32_t HaeberliOutputStartRow(uint32_t aPass,
+                                        bool aProgressiveDisplay,
+                                        int32_t aOutputRow)
+  {
+    MOZ_ASSERT(aPass < 4, "Invalid pass");
+    static const uint8_t firstRowOffset[] = { 3, 1, 0, 0 };
+
+    if (aProgressiveDisplay) {
+      return std::max(aOutputRow - firstRowOffset[aPass], 0);
+    } else {
+      return aOutputRow;
+    }
+  }
+
+  static int32_t HaeberliOutputUntilRow(uint32_t aPass,
+                                        bool aProgressiveDisplay,
+                                        const gfx::IntSize& aInputSize,
+                                        int32_t aOutputRow)
+  {
+    MOZ_ASSERT(aPass < 4, "Invalid pass");
+    static const uint8_t lastRowOffset[] = { 4, 2, 1, 0 };
+
+    if (aProgressiveDisplay) {
+      return std::min(aOutputRow + lastRowOffset[aPass],
+                      aInputSize.height - 1)
+             + 1;  // Add one because this is an open interval on the right.
+    } else {
+      return aOutputRow + 1;
+    }
+  }
+
+  void DuplicateRows(int32_t aStart, int32_t aUntil)
+  {
+    MOZ_ASSERT(aStart >= 0);
+    MOZ_ASSERT(aUntil >= 0);
+
+    if (aUntil <= aStart || aStart >= InputSize().height) {
+      return;
+    }
+
+    // The source row is the first row in the range.
+    const uint8_t* sourceRowPointer = GetRowPointer(aStart);
+
+    // We duplicate the source row into each subsequent row in the range.
+    for (int32_t destRow = aStart + 1 ; destRow < aUntil ; ++destRow) {
+      uint8_t* destRowPointer = GetRowPointer(destRow);
+      memcpy(destRowPointer, sourceRowPointer, InputSize().width * sizeof(PixelType));
+    }
+  }
+
+  void OutputRows(int32_t aStart, int32_t aUntil)
+  {
+    MOZ_ASSERT(aStart >= 0);
+    MOZ_ASSERT(aUntil >= 0);
+
+    if (aUntil <= aStart || aStart >= InputSize().height) {
+      return;
+    }
+
+    int32_t rowToOutput = aStart;
+    mNext.template WriteRows<PixelType>([&](PixelType* aRow, uint32_t aLength) {
+      const uint8_t* rowToOutputPointer = GetRowPointer(rowToOutput);
+      memcpy(aRow, rowToOutputPointer, aLength * sizeof(PixelType));
+
+      rowToOutput++;
+      return rowToOutput >= aUntil ? Some(WriteState::NEED_MORE_DATA)
+                                   : Nothing();
+    });
+  }
+
+  uint8_t* GetRowPointer(uint32_t aRow) const
+  {
+    uint32_t offset = aRow * InputSize().width * sizeof(PixelType);
+    MOZ_ASSERT(offset < InputSize().width * InputSize().height * sizeof(PixelType),
+               "Start of row is outside of image");
+    MOZ_ASSERT(offset + InputSize().width * sizeof(PixelType)
+                 <= InputSize().width * InputSize().height * sizeof(PixelType),
+               "End of row is outside of image");
+    return mBuffer.get() + offset;
+  }
+
+  Next mNext;                    /// The next SurfaceFilter in the chain.
+
+  UniquePtr<uint8_t[]> mBuffer;  /// The buffer used to store reordered rows.
+  int32_t mInputRow;             /// The current row we're reading. (0-indexed)
+  int32_t mOutputRow;            /// The current row we're writing. (0-indexed)
+  uint8_t mPass;                 /// Which pass we're on. (0-indexed)
+  bool mProgressiveDisplay;      /// If true, duplicate rows to optimize for
+                                 /// progressive display.
+};
+
+
+//////////////////////////////////////////////////////////////////////////////
+// RemoveFrameRectFilter
+//////////////////////////////////////////////////////////////////////////////
+
+template <typename Next> class RemoveFrameRectFilter;
+
+/**
+ * A configuration struct for RemoveFrameRectFilter.
+ */
+struct RemoveFrameRectConfig
+{
+  template <typename Next> using Filter = RemoveFrameRectFilter<Next>;
+  gfx::IntRect mFrameRect;  /// The surface subrect which contains data.
+};
+
+/**
+ * RemoveFrameRectFilter turns an image with a frame rect that does not match
+ * its logical size into an image with no frame rect. It does this by writing
+ * transparent pixels into any padding regions and throwing away excess data.
+ *
+ * The 'Next' template parameter specifies the next filter in the chain.
+ */
+template <typename Next>
+class RemoveFrameRectFilter final : public SurfaceFilter
+{
+public:
+  RemoveFrameRectFilter()
+    : mRow(0)
+  { }
+
+  template <typename... Rest>
+  nsresult Configure(const RemoveFrameRectConfig& aConfig, Rest... aRest)
+  {
+    nsresult rv = mNext.Configure(aRest...);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    if (mNext.IsValidPalettedPipe()) {
+      NS_WARNING("RemoveFrameRectFilter used with paletted pipe?");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    mFrameRect = mUnclampedFrameRect = aConfig.mFrameRect;
+    gfx::IntSize outputSize = mNext.InputSize();
+
+    // Forbid frame rects with negative size.
+    if (aConfig.mFrameRect.width < 0 || aConfig.mFrameRect.height < 0) {
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    // Clamp mFrameRect to the output size.
+    gfx::IntRect outputRect(0, 0, outputSize.width, outputSize.height);
+    mFrameRect = mFrameRect.Intersect(outputRect);
+
+    // If there's no intersection, |mFrameRect| will be an empty rect positioned
+    // at the maximum of |inputRect|'s and |aFrameRect|'s coordinates, which is
+    // not what we want. Force it to (0, 0) in that case.
+    if (mFrameRect.IsEmpty()) {
+      mFrameRect.MoveTo(0, 0);
+    }
+
+    // We don't need an intermediate buffer unless the unclamped frame rect
+    // width is larger than the clamped frame rect width. In that case, the
+    // caller will end up writing data that won't end up in the final image at
+    // all, and we'll need a buffer to give that data a place to go.
+    if (mFrameRect.width < mUnclampedFrameRect.width) {
+      mBuffer.reset(new (fallible) uint8_t[mUnclampedFrameRect.width *
+                                           sizeof(uint32_t)]);
+      if (MOZ_UNLIKELY(!mBuffer)) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+
+      memset(mBuffer.get(), 0, mUnclampedFrameRect.width * sizeof(uint32_t));
+    }
+
+    ConfigureFilter(mUnclampedFrameRect.Size(), sizeof(uint32_t));
+    return NS_OK;
+  }
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override
+  {
+    return mNext.TakeInvalidRect();
+  }
+
+  uint8_t* AdvanceRow() override
+  {
+    uint8_t* rowPtr = nullptr;
+
+    const int32_t currentRow = mRow;
+    mRow++;
+
+    if (currentRow < mFrameRect.y) {
+      // This row is outside of the frame rect, so just drop it on the floor.
+      rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer();
+      return AdjustRowPointer(rowPtr);
+    } else if (currentRow >= mFrameRect.YMost()) {
+      NS_WARNING("RemoveFrameRectFilter: Advancing past end of frame rect");
+      return nullptr;
+    }
+
+    // If we had to buffer, copy the data. Otherwise, just advance the row.
+    if (mBuffer) {
+      mNext.template WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+        // Clear the part of the row before the clamped frame rect.
+        MOZ_ASSERT(mFrameRect.x >= 0);
+        MOZ_ASSERT(uint32_t(mFrameRect.x) < aLength);
+        memset(aRow, 0, mFrameRect.x * sizeof(uint32_t));
+
+        // Write the part of the row that's inside the clamped frame rect.
+        MOZ_ASSERT(mFrameRect.width >= 0);
+        aRow += mFrameRect.x;
+        aLength -= std::min(aLength, uint32_t(mFrameRect.x));
+        uint32_t toWrite = std::min(aLength, uint32_t(mFrameRect.width));
+        uint8_t* source = mBuffer.get() -
+                          std::min(mUnclampedFrameRect.x, 0) * sizeof(uint32_t);
+        MOZ_ASSERT(source >= mBuffer.get());
+        MOZ_ASSERT(source + toWrite * sizeof(uint32_t)
+                     <= mBuffer.get() + mUnclampedFrameRect.width * sizeof(uint32_t));
+        memcpy(aRow, source, toWrite * sizeof(uint32_t));
+
+        // Clear the part of the row after the clamped frame rect.
+        aRow += toWrite;
+        aLength -= std::min(aLength, toWrite);
+        memset(aRow, 0, aLength * sizeof(uint32_t));
+
+        return Some(WriteState::NEED_MORE_DATA);
+      });
+
+      rowPtr = mBuffer.get();
+    } else {
+      rowPtr = mNext.AdvanceRow();
+    }
+
+    // If there's still more data coming, just adjust the pointer and return.
+    if (mRow < mFrameRect.YMost() || rowPtr == nullptr) {
+      return AdjustRowPointer(rowPtr);
+    }
+
+    // We've finished the region specified by the frame rect. Advance to the end
+    // of the next pipeline stage's buffer, outputting blank rows.
+    mNext.template WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      memset(rowPtr, 0, aLength * sizeof(uint32_t));
+      return Nothing();
+    });
+
+    return nullptr;  // We're done.
+  }
+
+protected:
+  uint8_t* DoResetToFirstRow() override
+  {
+    uint8_t* rowPtr = mNext.ResetToFirstRow();
+    if (rowPtr == nullptr) {
+      mRow = InputSize().height;
+      return nullptr;
+    }
+
+    mRow = mUnclampedFrameRect.y;
+
+    // Advance the next pipeline stage to the beginning of the frame rect,
+    // outputting blank rows.
+    if (mFrameRect.y > 0) {
+      int32_t rowsToWrite = mFrameRect.y;
+      mNext.template WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength)
+                                           -> Maybe<WriteState> {
+        memset(aRow, 0, aLength * sizeof(uint32_t));
+        rowsToWrite--;
+        return rowsToWrite > 0 ? Nothing()
+                               : Some(WriteState::NEED_MORE_DATA);
+      });
+    }
+
+    // We're at the beginning of the frame rect now, so return if we're either
+    // ready for input or we're already done.
+    rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer();
+    if (!mFrameRect.IsEmpty() || rowPtr == nullptr) {
+      // Note that the pointer we're returning is for the next row we're
+      // actually going to write to, but we may discard writes before that point
+      // if mRow < mFrameRect.y.
+      return AdjustRowPointer(rowPtr);
+    }
+
+    // We've finished the region specified by the frame rect, but the frame rect
+    // is empty, so we need to output the rest of the image immediately. Advance
+    // to the end of the next pipeline stage's buffer, outputting blank rows.
+    int32_t rowsWritten = 0;
+    mNext.template WriteRows<uint32_t>([&](uint32_t* aRow, uint32_t aLength) {
+      rowsWritten++;
+      memset(aRow, 0, aLength * sizeof(uint32_t));
+      return Nothing();
+    });
+
+    mRow = InputSize().height;
+    return nullptr;  // We're done.
+  }
+
+private:
+  uint8_t* AdjustRowPointer(uint8_t* aNextRowPointer) const
+  {
+    if (mBuffer) {
+      MOZ_ASSERT(aNextRowPointer == mBuffer.get());
+      return aNextRowPointer;  // No adjustment needed for an intermediate buffer.
+    }
+
+    if (mFrameRect.IsEmpty() ||
+        mRow >= mFrameRect.YMost() ||
+        aNextRowPointer == nullptr) {
+      return nullptr;  // Nothing left to write.
+    }
+
+    return aNextRowPointer + mFrameRect.x * sizeof(uint32_t);
+  }
+
+  Next mNext;                        /// The next SurfaceFilter in the chain.
+
+  gfx::IntRect mFrameRect;           /// The surface subrect which contains data,
+                                     /// clamped to the image size.
+  gfx::IntRect mUnclampedFrameRect;  /// The frame rect before clamping.
+  UniquePtr<uint8_t[]> mBuffer;      /// The intermediate buffer, if one is
+                                     /// necessary because the frame rect width
+                                     /// is larger than the image's logical width.
+  int32_t  mRow;                     /// The row in unclamped frame rect space
+                                     /// that we're currently writing.
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_SurfaceFilters_h