summaryrefslogtreecommitdiffstats
path: root/image/test/gtest
diff options
context:
space:
mode:
Diffstat (limited to 'image/test/gtest')
-rw-r--r--image/test/gtest/Common.cpp673
-rw-r--r--image/test/gtest/Common.h419
-rw-r--r--image/test/gtest/TestADAM7InterpolatingFilter.cpp671
-rw-r--r--image/test/gtest/TestCopyOnWrite.cpp235
-rw-r--r--image/test/gtest/TestDecodeToSurface.cpp123
-rw-r--r--image/test/gtest/TestDecoders.cpp669
-rw-r--r--image/test/gtest/TestDeinterlacingFilter.cpp672
-rw-r--r--image/test/gtest/TestDownscalingFilter.cpp231
-rw-r--r--image/test/gtest/TestDownscalingFilterNoSkia.cpp57
-rw-r--r--image/test/gtest/TestMetadata.cpp255
-rw-r--r--image/test/gtest/TestRemoveFrameRectFilter.cpp327
-rw-r--r--image/test/gtest/TestSourceBuffer.cpp810
-rw-r--r--image/test/gtest/TestStreamingLexer.cpp973
-rw-r--r--image/test/gtest/TestSurfacePipeIntegration.cpp508
-rw-r--r--image/test/gtest/TestSurfaceSink.cpp1491
-rw-r--r--image/test/gtest/animated-with-extra-image-sub-blocks.gifbin0 -> 434 bytes
-rw-r--r--image/test/gtest/corrupt-with-bad-bmp-height.icobin0 -> 41663 bytes
-rw-r--r--image/test/gtest/corrupt-with-bad-bmp-width.icobin0 -> 41663 bytes
-rw-r--r--image/test/gtest/corrupt.jpgbin0 -> 2477 bytes
-rw-r--r--image/test/gtest/downscaled.bmpbin0 -> 30138 bytes
-rw-r--r--image/test/gtest/downscaled.gifbin0 -> 223 bytes
-rw-r--r--image/test/gtest/downscaled.icobin0 -> 41662 bytes
-rw-r--r--image/test/gtest/downscaled.iconbin0 -> 40003 bytes
-rw-r--r--image/test/gtest/downscaled.jpgbin0 -> 6035 bytes
-rw-r--r--image/test/gtest/downscaled.pngbin0 -> 1015 bytes
-rw-r--r--image/test/gtest/first-frame-green.gifbin0 -> 317 bytes
-rw-r--r--image/test/gtest/first-frame-green.pngbin0 -> 364 bytes
-rw-r--r--image/test/gtest/first-frame-padding.gifbin0 -> 49 bytes
-rw-r--r--image/test/gtest/green-1x1-truncated.gifbin0 -> 53 bytes
-rw-r--r--image/test/gtest/green.bmpbin0 -> 30138 bytes
-rw-r--r--image/test/gtest/green.gifbin0 -> 156 bytes
-rw-r--r--image/test/gtest/green.icobin0 -> 41662 bytes
-rw-r--r--image/test/gtest/green.iconbin0 -> 40002 bytes
-rw-r--r--image/test/gtest/green.jpgbin0 -> 361 bytes
-rw-r--r--image/test/gtest/green.pngbin0 -> 255 bytes
-rw-r--r--image/test/gtest/invalid-truncated-metadata.bmpbin0 -> 54 bytes
-rw-r--r--image/test/gtest/moz.build78
-rw-r--r--image/test/gtest/no-frame-delay.gifbin0 -> 317 bytes
-rw-r--r--image/test/gtest/rle4.bmpbin0 -> 3686 bytes
-rw-r--r--image/test/gtest/rle8.bmpbin0 -> 1288 bytes
-rw-r--r--image/test/gtest/transparent-ico-with-and-mask.icobin0 -> 3262 bytes
-rw-r--r--image/test/gtest/transparent-if-within-ico.bmpbin0 -> 4234 bytes
-rw-r--r--image/test/gtest/transparent.gifbin0 -> 355 bytes
-rw-r--r--image/test/gtest/transparent.pngbin0 -> 419 bytes
44 files changed, 8192 insertions, 0 deletions
diff --git a/image/test/gtest/Common.cpp b/image/test/gtest/Common.cpp
new file mode 100644
index 000000000..5a24bbb14
--- /dev/null
+++ b/image/test/gtest/Common.cpp
@@ -0,0 +1,673 @@
+/* -*- 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 "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;
+using std::vector;
+
+///////////////////////////////////////////////////////////////////////////////
+// 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; \
+ }
+
+#define ASSERT_EQ_OR_RETURN(a, b, rv) \
+ EXPECT_EQ(a, b); \
+ if ((a) != (b)) { \
+ return rv; \
+ }
+
+#define ASSERT_GE_OR_RETURN(a, b, rv) \
+ EXPECT_GE(a, b); \
+ if (!((a) >= (b))) { \
+ return rv; \
+ }
+
+#define ASSERT_LE_OR_RETURN(a, b, rv) \
+ EXPECT_LE(a, b); \
+ if (!((a) <= (b))) { \
+ return rv; \
+ }
+
+#define ASSERT_LT_OR_RETURN(a, b, rv) \
+ EXPECT_LT(a, b); \
+ if (!((a) < (b))) { \
+ return rv; \
+ }
+
+already_AddRefed<nsIInputStream>
+LoadFile(const char* aRelativePath)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIProperties> dirService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ ASSERT_TRUE_OR_RETURN(dirService != nullptr, nullptr);
+
+ // Retrieve the current working directory.
+ nsCOMPtr<nsIFile> file;
+ rv = dirService->Get(NS_OS_CURRENT_WORKING_DIR,
+ NS_GET_IID(nsIFile), getter_AddRefs(file));
+ ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr);
+
+ // Construct the final path by appending the working path to the current
+ // working directory.
+ file->AppendNative(nsDependentCString(aRelativePath));
+
+ // Construct an input stream for the requested file.
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), file);
+ ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr);
+
+ // Ensure the resulting input stream is buffered.
+ if (!NS_InputStreamIsBuffered(inputStream)) {
+ nsCOMPtr<nsIInputStream> bufStream;
+ rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream),
+ inputStream, 1024);
+ ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr);
+ inputStream = bufStream;
+ }
+
+ return inputStream.forget();
+}
+
+bool
+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
+IsSolidPalettedColor(Decoder* aDecoder, uint8_t aColor)
+{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ return PalettedRectIsSolidColor(aDecoder, currentFrame->GetRect(), aColor);
+}
+
+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
+PalettedRowsAreSolidColor(Decoder* aDecoder,
+ int32_t aStartRow,
+ int32_t aRowCount,
+ uint8_t aColor)
+{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ IntRect frameRect = currentFrame->GetRect();
+ IntRect solidColorRect(frameRect.x, aStartRow, frameRect.width, aRowCount);
+ return PalettedRectIsSolidColor(aDecoder, solidColorRect, aColor);
+}
+
+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(), 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 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;
+}
+
+bool
+PalettedRectIsSolidColor(Decoder* aDecoder, const IntRect& aRect, uint8_t aColor)
+{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ uint8_t* imageData;
+ uint32_t imageLength;
+ currentFrame->GetImageData(&imageData, &imageLength);
+ ASSERT_TRUE_OR_RETURN(imageData, false);
+
+ // Clamp to the frame rect. If any pixels outside the frame rect are included,
+ // we immediately fail, because such pixels don't have any "color" in the
+ // sense this function measures - they're transparent, and that doesn't
+ // necessarily correspond to any color palette index at all.
+ IntRect frameRect = currentFrame->GetRect();
+ ASSERT_EQ_OR_RETURN(imageLength, uint32_t(frameRect.Area()), false);
+ IntRect rect = aRect.Intersect(frameRect);
+ ASSERT_EQ_OR_RETURN(rect.Area(), aRect.Area(), false);
+
+ // Translate |rect| by |frameRect.TopLeft()| to reflect the fact that the
+ // frame rect's offset doesn't actually mean anything in terms of the
+ // in-memory representation of the surface. The image data starts at the upper
+ // left corner of the frame rect, in other words.
+ rect -= frameRect.TopLeft();
+
+ // Walk through the image data and make sure that the entire rect has the
+ // palette index |aColor|.
+ int32_t rowLength = frameRect.width;
+ 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;
+ ASSERT_EQ_OR_RETURN(aColor, imageData[i], false);
+ }
+ }
+
+ return true;
+}
+
+bool
+RowHasPixels(SourceSurface* aSurface,
+ int32_t aRow,
+ const vector<BGRAColor>& aPixels)
+{
+ ASSERT_GE_OR_RETURN(aRow, 0, false);
+
+ IntSize surfaceSize = aSurface->GetSize();
+ ASSERT_EQ_OR_RETURN(aPixels.size(), size_t(surfaceSize.width), false);
+ ASSERT_LT_OR_RETURN(aRow, surfaceSize.height, false);
+
+ RefPtr<DataSourceSurface> dataSurface = aSurface->GetDataSurface();
+ ASSERT_TRUE_OR_RETURN(dataSurface, 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 rowLength = dataSurface->Stride();
+ for (int32_t col = 0; col < surfaceSize.width; ++col) {
+ int32_t i = aRow * rowLength + col * 4;
+ ASSERT_EQ_OR_RETURN(aPixels[col].mBlue, data[i + 0], false);
+ ASSERT_EQ_OR_RETURN(aPixels[col].mGreen, data[i + 1], false);
+ ASSERT_EQ_OR_RETURN(aPixels[col].mRed, data[i + 2], false);
+ ASSERT_EQ_OR_RETURN(aPixels[col].mAlpha, data[i + 3], false);
+ }
+
+ return true;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// SurfacePipe Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+already_AddRefed<Decoder>
+CreateTrivialDecoder()
+{
+ gfxPrefs::GetSingleton();
+ DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ RefPtr<Decoder> decoder =
+ DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+ 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->GetSourceSurface();
+ 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));
+}
+
+void
+CheckGeneratedPalettedImage(Decoder* aDecoder, const IntRect& aRect)
+{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ IntSize imageSize = currentFrame->GetImageSize();
+
+ // 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 all 255's. (Region 'C'.)
+ EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, aRect, 255));
+
+ // Check that the area above the output rect is all 0's. (Region 'A'.)
+ EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder,
+ IntRect(0, 0, imageSize.width, aRect.y),
+ 0));
+
+ // Check that the area to the left of the output rect is all 0's. (Region 'B'.)
+ EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder,
+ IntRect(0, aRect.y, aRect.x, aRect.YMost()),
+ 0));
+
+ // Check that the area to the right of the output rect is all 0's. (Region 'D'.)
+ const int32_t widthOnRight = imageSize.width - aRect.XMost();
+ EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder,
+ IntRect(aRect.XMost(), aRect.y, widthOnRight, aRect.YMost()),
+ 0));
+
+ // Check that the area below the output rect is transparent. (Region 'E'.)
+ const int32_t heightBelow = imageSize.height - aRect.YMost();
+ EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder,
+ IntRect(0, aRect.YMost(), imageSize.width, heightBelow),
+ 0));
+}
+
+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 */)
+{
+ 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 = aFilter->WritePixels<uint32_t>([&] {
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+ 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 = aFilter->WritePixels<uint32_t>([&] {
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+ 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
+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 */)
+{
+ 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 = aFilter->WritePixels<uint8_t>([&] {
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+ 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 = aFilter->WritePixels<uint8_t>([&] {
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+ 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]);
+ }
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Test Data
+///////////////////////////////////////////////////////////////////////////////
+
+ImageTestCase GreenPNGTestCase()
+{
+ return ImageTestCase("green.png", "image/png", IntSize(100, 100));
+}
+
+ImageTestCase GreenGIFTestCase()
+{
+ return ImageTestCase("green.gif", "image/gif", IntSize(100, 100));
+}
+
+ImageTestCase GreenJPGTestCase()
+{
+ return ImageTestCase("green.jpg", "image/jpeg", IntSize(100, 100),
+ TEST_CASE_IS_FUZZY);
+}
+
+ImageTestCase GreenBMPTestCase()
+{
+ return ImageTestCase("green.bmp", "image/bmp", IntSize(100, 100));
+}
+
+ImageTestCase GreenICOTestCase()
+{
+ // This ICO contains a 32-bit BMP, and we use a BMP's alpha data by default
+ // when the BMP is embedded in an ICO, so it's transparent.
+ return ImageTestCase("green.ico", "image/x-icon", IntSize(100, 100),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase GreenIconTestCase()
+{
+ return ImageTestCase("green.icon", "image/icon", IntSize(100, 100),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase GreenFirstFrameAnimatedGIFTestCase()
+{
+ return ImageTestCase("first-frame-green.gif", "image/gif", IntSize(100, 100),
+ TEST_CASE_IS_ANIMATED);
+}
+
+ImageTestCase GreenFirstFrameAnimatedPNGTestCase()
+{
+ return ImageTestCase("first-frame-green.png", "image/png", IntSize(100, 100),
+ TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED);
+}
+
+ImageTestCase CorruptTestCase()
+{
+ return ImageTestCase("corrupt.jpg", "image/jpeg", IntSize(100, 100),
+ TEST_CASE_HAS_ERROR);
+}
+
+ImageTestCase CorruptBMPWithTruncatedHeader()
+{
+ // This BMP has a header which is truncated right between the BIH and the
+ // bitfields, which is a particularly error-prone place w.r.t. the BMP decoder
+ // state machine.
+ return ImageTestCase("invalid-truncated-metadata.bmp", "image/bmp",
+ IntSize(100, 100), TEST_CASE_HAS_ERROR);
+}
+
+ImageTestCase CorruptICOWithBadBMPWidthTestCase()
+{
+ // This ICO contains a BMP icon which has a width that doesn't match the size
+ // listed in the corresponding ICO directory entry.
+ return ImageTestCase("corrupt-with-bad-bmp-width.ico", "image/x-icon",
+ IntSize(100, 100), TEST_CASE_HAS_ERROR);
+}
+
+ImageTestCase CorruptICOWithBadBMPHeightTestCase()
+{
+ // This ICO contains a BMP icon which has a height that doesn't match the size
+ // listed in the corresponding ICO directory entry.
+ return ImageTestCase("corrupt-with-bad-bmp-height.ico", "image/x-icon",
+ IntSize(100, 100), TEST_CASE_HAS_ERROR);
+}
+
+ImageTestCase TransparentPNGTestCase()
+{
+ return ImageTestCase("transparent.png", "image/png", IntSize(32, 32),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase TransparentGIFTestCase()
+{
+ return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase FirstFramePaddingGIFTestCase()
+{
+ return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase TransparentIfWithinICOBMPTestCase(TestCaseFlags aFlags)
+{
+ // This is a BMP that is only transparent when decoded as if it is within an
+ // ICO file. (Note: aFlags needs to be set to TEST_CASE_DEFAULT_FLAGS or
+ // TEST_CASE_IS_TRANSPARENT accordingly.)
+ return ImageTestCase("transparent-if-within-ico.bmp", "image/bmp",
+ IntSize(32, 32), aFlags);
+}
+
+ImageTestCase RLE4BMPTestCase()
+{
+ return ImageTestCase("rle4.bmp", "image/bmp", IntSize(320, 240),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase RLE8BMPTestCase()
+{
+ return ImageTestCase("rle8.bmp", "image/bmp", IntSize(32, 32),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+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));
+}
+
+ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase()
+{
+ // This is a corrupt GIF that has extra image sub blocks between the first and
+ // second frame.
+ return ImageTestCase("animated-with-extra-image-sub-blocks.gif", "image/gif",
+ IntSize(100, 100));
+}
+
+ImageTestCase DownscaledPNGTestCase()
+{
+ // This testcase (and all the other "downscaled") testcases) 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. It's intended that tests downscale it
+ // from 100x100 to 20x20, so we specify a 20x20 output size.
+ return ImageTestCase("downscaled.png", "image/png", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+ImageTestCase DownscaledGIFTestCase()
+{
+ return ImageTestCase("downscaled.gif", "image/gif", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+ImageTestCase DownscaledJPGTestCase()
+{
+ return ImageTestCase("downscaled.jpg", "image/jpeg", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+ImageTestCase DownscaledBMPTestCase()
+{
+ return ImageTestCase("downscaled.bmp", "image/bmp", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+ImageTestCase DownscaledICOTestCase()
+{
+ return ImageTestCase("downscaled.ico", "image/x-icon", IntSize(100, 100),
+ IntSize(20, 20), TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase DownscaledIconTestCase()
+{
+ return ImageTestCase("downscaled.icon", "image/icon", IntSize(100, 100),
+ IntSize(20, 20), TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase DownscaledTransparentICOWithANDMaskTestCase()
+{
+ // This test case is an ICO with AND mask transparency. We want to ensure that
+ // we can downscale it without crashing or triggering ASAN failures, but its
+ // content isn't simple to verify, so for now we don't check the output.
+ return ImageTestCase("transparent-ico-with-and-mask.ico", "image/x-icon",
+ IntSize(32, 32), IntSize(20, 20),
+ TEST_CASE_IS_TRANSPARENT | TEST_CASE_IGNORE_OUTPUT);
+}
+
+ImageTestCase TruncatedSmallGIFTestCase()
+{
+ return ImageTestCase("green-1x1-truncated.gif", "image/gif", IntSize(1, 1));
+}
+
+} // namespace image
+} // namespace mozilla
diff --git a/image/test/gtest/Common.h b/image/test/gtest/Common.h
new file mode 100644
index 000000000..79bed9fc1
--- /dev/null
+++ b/image/test/gtest/Common.h
@@ -0,0 +1,419 @@
+/* -*- 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 <vector>
+
+#include "gtest/gtest.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+#include "Decoder.h"
+#include "gfxColor.h"
+#include "imgITools.h"
+#include "nsCOMPtr.h"
+#include "SurfacePipe.h"
+#include "SurfacePipeFactory.h"
+
+class nsIInputStream;
+
+namespace mozilla {
+namespace image {
+
+///////////////////////////////////////////////////////////////////////////////
+// Types
+///////////////////////////////////////////////////////////////////////////////
+
+enum TestCaseFlags
+{
+ TEST_CASE_DEFAULT_FLAGS = 0,
+ TEST_CASE_IS_FUZZY = 1 << 0,
+ TEST_CASE_HAS_ERROR = 1 << 1,
+ TEST_CASE_IS_TRANSPARENT = 1 << 2,
+ TEST_CASE_IS_ANIMATED = 1 << 3,
+ TEST_CASE_IGNORE_OUTPUT = 1 << 4,
+};
+
+struct ImageTestCase
+{
+ ImageTestCase(const char* aPath,
+ const char* aMimeType,
+ gfx::IntSize aSize,
+ uint32_t aFlags = TEST_CASE_DEFAULT_FLAGS)
+ : mPath(aPath)
+ , mMimeType(aMimeType)
+ , mSize(aSize)
+ , mOutputSize(aSize)
+ , mFlags(aFlags)
+ { }
+
+ ImageTestCase(const char* aPath,
+ const char* aMimeType,
+ gfx::IntSize aSize,
+ gfx::IntSize aOutputSize,
+ uint32_t aFlags = TEST_CASE_DEFAULT_FLAGS)
+ : mPath(aPath)
+ , mMimeType(aMimeType)
+ , mSize(aSize)
+ , mOutputSize(aOutputSize)
+ , mFlags(aFlags)
+ { }
+
+ const char* mPath;
+ const char* mMimeType;
+ gfx::IntSize mSize;
+ gfx::IntSize mOutputSize;
+ uint32_t mFlags;
+};
+
+struct BGRAColor
+{
+ BGRAColor() : BGRAColor(0, 0, 0, 0) { }
+
+ 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 Blue() { return BGRAColor(0xFF, 0x00, 0x00, 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;
+};
+
+
+///////////////////////////////////////////////////////////////////////////////
+// General Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A RAII class that ensure that ImageLib services are available. Any tests that
+ * require ImageLib to be initialized (for example, any test that uses the
+ * SurfaceCache; see image::EnsureModuleInitialized() for the full list) can
+ * use this class to ensure that ImageLib services are available. Failure to do
+ * so can result in strange, non-deterministic failures.
+ */
+struct AutoInitializeImageLib
+{
+ AutoInitializeImageLib()
+ {
+ // Ensure that ImageLib services are initialized.
+ nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
+ EXPECT_TRUE(imgTools != nullptr);
+ }
+};
+
+/// 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 @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,
+ uint8_t aFuzz = 0);
+
+/**
+ * @returns true if every pixel of @aDecoder's surface has the palette index
+ * specified by @aColor.
+ */
+bool IsSolidPalettedColor(Decoder* aDecoder, uint8_t aColor);
+
+/**
+ * @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 range of rows specified by @aStartRow and
+ * @aRowCount of @aDecoder's surface has the palette index specified by @aColor.
+ */
+bool PalettedRowsAreSolidColor(Decoder* aDecoder,
+ int32_t aStartRow,
+ int32_t aRowCount,
+ uint8_t aColor);
+
+/**
+ * @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);
+
+/**
+ * @returns true if every pixel in the rect specified by @aRect has the palette
+ * index specified by @aColor.
+ */
+bool PalettedRectIsSolidColor(Decoder* aDecoder,
+ const gfx::IntRect& aRect,
+ uint8_t aColor);
+
+/**
+ * @returns true if the pixels in @aRow of @aSurface match the pixels given in
+ * @aPixels.
+ */
+bool RowHasPixels(gfx::SourceSurface* aSurface,
+ int32_t aRow,
+ const std::vector<BGRAColor>& aPixels);
+
+// ExpectNoResume is an IResumable implementation for use by tests that expect
+// Resume() to never get called.
+class ExpectNoResume final : public IResumable
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExpectNoResume, override)
+
+ void Resume() override { FAIL() << "Resume() should not get called"; }
+
+private:
+ ~ExpectNoResume() override { }
+};
+
+// CountResumes is an IResumable implementation for use by tests that expect
+// Resume() to get called a certain number of times.
+class CountResumes : public IResumable
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CountResumes, override)
+
+ CountResumes() : mCount(0) { }
+
+ void Resume() override { mCount++; }
+ uint32_t Count() const { return mCount; }
+
+private:
+ ~CountResumes() override { }
+
+ uint32_t mCount;
+};
+
+
+///////////////////////////////////////////////////////////////////////////////
+// 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.
+ * @param aFuzz The amount of fuzz to use in pixel comparisons.
+ */
+void CheckGeneratedImage(Decoder* aDecoder,
+ const gfx::IntRect& aRect,
+ uint8_t aFuzz = 0);
+
+/**
+ * Checks a generated paletted 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 have a palette index of 255, while pixels outside
+ * this region have a palette index of 0.
+ */
+void CheckGeneratedPalettedImage(Decoder* aDecoder, const gfx::IntRect& aRect);
+
+/**
+ * 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 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);
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Test Data
+///////////////////////////////////////////////////////////////////////////////
+
+ImageTestCase GreenPNGTestCase();
+ImageTestCase GreenGIFTestCase();
+ImageTestCase GreenJPGTestCase();
+ImageTestCase GreenBMPTestCase();
+ImageTestCase GreenICOTestCase();
+ImageTestCase GreenIconTestCase();
+
+ImageTestCase GreenFirstFrameAnimatedGIFTestCase();
+ImageTestCase GreenFirstFrameAnimatedPNGTestCase();
+
+ImageTestCase CorruptTestCase();
+ImageTestCase CorruptBMPWithTruncatedHeader();
+ImageTestCase CorruptICOWithBadBMPWidthTestCase();
+ImageTestCase CorruptICOWithBadBMPHeightTestCase();
+
+ImageTestCase TransparentPNGTestCase();
+ImageTestCase TransparentGIFTestCase();
+ImageTestCase FirstFramePaddingGIFTestCase();
+ImageTestCase NoFrameDelayGIFTestCase();
+ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase();
+
+ImageTestCase TransparentBMPWhenBMPAlphaEnabledTestCase();
+ImageTestCase RLE4BMPTestCase();
+ImageTestCase RLE8BMPTestCase();
+
+ImageTestCase DownscaledPNGTestCase();
+ImageTestCase DownscaledGIFTestCase();
+ImageTestCase DownscaledJPGTestCase();
+ImageTestCase DownscaledBMPTestCase();
+ImageTestCase DownscaledICOTestCase();
+ImageTestCase DownscaledIconTestCase();
+ImageTestCase DownscaledTransparentICOWithANDMaskTestCase();
+
+ImageTestCase TruncatedSmallGIFTestCase();
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_test_gtest_Common_h
diff --git a/image/test/gtest/TestADAM7InterpolatingFilter.cpp b/image/test/gtest/TestADAM7InterpolatingFilter.cpp
new file mode 100644
index 000000000..d9dab4346
--- /dev/null
+++ b/image/test/gtest/TestADAM7InterpolatingFilter.cpp
@@ -0,0 +1,671 @@
+/* -*- 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 <algorithm>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Maybe.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;
+
+using std::generate;
+using std::vector;
+
+template <typename Func> void
+WithADAM7InterpolatingFilter(const IntSize& aSize, Func aFunc)
+{
+ RefPtr<Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ WithFilterPipeline(decoder, Forward<Func>(aFunc),
+ ADAM7InterpolatingConfig { },
+ SurfaceConfig { decoder, 0, aSize,
+ SurfaceFormat::B8G8R8A8, false });
+}
+
+void
+AssertConfiguringADAM7InterpolatingFilterFails(const IntSize& aSize)
+{
+ RefPtr<Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ AssertConfiguringPipelineFails(decoder,
+ ADAM7InterpolatingConfig { },
+ SurfaceConfig { decoder, 0, aSize,
+ SurfaceFormat::B8G8R8A8, false });
+}
+
+uint8_t
+InterpolateByte(uint8_t aByteA, uint8_t aByteB, float aWeight)
+{
+ return uint8_t(aByteA * aWeight + aByteB * (1.0f - aWeight));
+}
+
+BGRAColor
+InterpolateColors(BGRAColor aColor1, BGRAColor aColor2, float aWeight)
+{
+ return BGRAColor(InterpolateByte(aColor1.mBlue, aColor2.mBlue, aWeight),
+ InterpolateByte(aColor1.mGreen, aColor2.mGreen, aWeight),
+ InterpolateByte(aColor1.mRed, aColor2.mRed, aWeight),
+ InterpolateByte(aColor1.mAlpha, aColor2.mAlpha, aWeight));
+}
+
+enum class ShouldInterpolate
+{
+ eYes,
+ eNo
+};
+
+BGRAColor
+HorizontallyInterpolatedPixel(uint32_t aCol,
+ uint32_t aWidth,
+ const vector<float>& aWeights,
+ ShouldInterpolate aShouldInterpolate,
+ const vector<BGRAColor>& aColors)
+{
+ // We cycle through the vector of weights forever.
+ float weight = aWeights[aCol % aWeights.size()];
+
+ // Find the columns of the two final pixels for this set of weights.
+ uint32_t finalPixel1 = aCol - aCol % aWeights.size();
+ uint32_t finalPixel2 = finalPixel1 + aWeights.size();
+
+ // If |finalPixel2| is past the end of the row, that means that there is no
+ // final pixel after the pixel at |finalPixel1|. In that case, we just want to
+ // duplicate |finalPixel1|'s color until the end of the row. We can do that by
+ // setting |finalPixel2| equal to |finalPixel1| so that the interpolation has
+ // no effect.
+ if (finalPixel2 >= aWidth) {
+ finalPixel2 = finalPixel1;
+ }
+
+ // We cycle through the vector of colors forever (subject to the above
+ // constraint about the end of the row).
+ BGRAColor color1 = aColors[finalPixel1 % aColors.size()];
+ BGRAColor color2 = aColors[finalPixel2 % aColors.size()];
+
+ // If we're not interpolating, we treat all pixels which aren't final as
+ // transparent. Since the number of weights we have is equal to the stride
+ // between final pixels, we can check if |aCol| is a final pixel by checking
+ // whether |aCol| is a multiple of |aWeights.size()|.
+ if (aShouldInterpolate == ShouldInterpolate::eNo) {
+ return aCol % aWeights.size() == 0 ? color1
+ : BGRAColor::Transparent();
+ }
+
+ // Interpolate.
+ return InterpolateColors(color1, color2, weight);
+}
+
+vector<float>&
+InterpolationWeights(int32_t aStride)
+{
+ // Precalculated interpolation weights. These are used to interpolate
+ // between final pixels or between important rows. Although no interpolation
+ // is actually applied to the previous final pixel or important row value,
+ // the arrays still start with 1.0f, which is always skipped, primarily
+ // because otherwise |stride1Weights| would have zero elements.
+ static vector<float> stride8Weights =
+ { 1.0f, 7 / 8.0f, 6 / 8.0f, 5 / 8.0f, 4 / 8.0f, 3 / 8.0f, 2 / 8.0f, 1 / 8.0f };
+ static vector<float> stride4Weights = { 1.0f, 3 / 4.0f, 2 / 4.0f, 1 / 4.0f };
+ static vector<float> stride2Weights = { 1.0f, 1 / 2.0f };
+ static vector<float> stride1Weights = { 1.0f };
+
+ switch (aStride) {
+ case 8: return stride8Weights;
+ case 4: return stride4Weights;
+ case 2: return stride2Weights;
+ case 1: return stride1Weights;
+ default:
+ MOZ_CRASH();
+ }
+}
+
+int32_t
+ImportantRowStride(uint8_t aPass)
+{
+ // The stride between important rows for each pass, with a dummy value for
+ // the nonexistent pass 0 and for pass 8, since the tests run an extra pass to
+ // make sure nothing breaks.
+ static int32_t strides[] = { 1, 8, 8, 4, 4, 2, 2, 1, 1 };
+
+ return strides[aPass];
+}
+
+size_t
+FinalPixelStride(uint8_t aPass)
+{
+ // The stride between the final pixels in important rows for each pass, with
+ // a dummy value for the nonexistent pass 0 and for pass 8, since the tests
+ // run an extra pass to make sure nothing breaks.
+ static size_t strides[] = { 1, 8, 4, 4, 2, 2, 1, 1, 1 };
+
+ return strides[aPass];
+}
+
+bool
+IsImportantRow(int32_t aRow, uint8_t aPass)
+{
+ return aRow % ImportantRowStride(aPass) == 0;
+}
+
+/**
+ * ADAM7 breaks up the image into 8x8 blocks. On each of the 7 passes, a new
+ * set of pixels in each block receives their final values, according to the
+ * following pattern:
+ *
+ * 1 6 4 6 2 6 4 6
+ * 7 7 7 7 7 7 7 7
+ * 5 6 5 6 5 6 5 6
+ * 7 7 7 7 7 7 7 7
+ * 3 6 4 6 3 6 4 6
+ * 7 7 7 7 7 7 7 7
+ * 5 6 5 6 5 6 5 6
+ * 7 7 7 7 7 7 7 7
+ *
+ * This function produces a row of pixels @aWidth wide, suitable for testing
+ * horizontal interpolation on pass @aPass. The pattern of pixels used is
+ * determined by @aPass and @aRow, which determine which pixels are final
+ * according to the table above, and @aColors, from which the pixel values
+ * are selected.
+ *
+ * There are two different behaviors: if |eNo| is passed for
+ * @aShouldInterpolate, non-final pixels are treated as transparent. If |eNo|
+ * is passed, non-final pixels get interpolated in from the surrounding final
+ * pixels. The intention is that |eNo| is passed to generate input which will
+ * be run through ADAM7InterpolatingFilter, and |eYes| is passed to generate
+ * reference data to check that the filter is performing horizontal
+ * interpolation correctly.
+ *
+ * This function does not perform vertical interpolation. Rows which aren't on
+ * the current pass are filled with transparent pixels.
+ *
+ * @return a vector<BGRAColor> representing a row of pixels.
+ */
+vector<BGRAColor>
+ADAM7HorizontallyInterpolatedRow(uint8_t aPass,
+ uint32_t aRow,
+ uint32_t aWidth,
+ ShouldInterpolate aShouldInterpolate,
+ const vector<BGRAColor>& aColors)
+{
+ EXPECT_GT(aPass, 0);
+ EXPECT_LE(aPass, 8);
+ EXPECT_GT(aColors.size(), 0u);
+
+ vector<BGRAColor> result(aWidth);
+
+ if (IsImportantRow(aRow, aPass)) {
+ vector<float>& weights = InterpolationWeights(FinalPixelStride(aPass));
+
+ // Compute the horizontally interpolated row.
+ uint32_t col = 0;
+ generate(result.begin(), result.end(), [&]{
+ return HorizontallyInterpolatedPixel(col++, aWidth, weights,
+ aShouldInterpolate, aColors);
+ });
+ } else {
+ // This is an unimportant row; just make the entire thing transparent.
+ generate(result.begin(), result.end(), []{
+ return BGRAColor::Transparent();
+ });
+ }
+
+ EXPECT_EQ(result.size(), size_t(aWidth));
+
+ return result;
+}
+
+WriteState
+WriteUninterpolatedPixels(SurfaceFilter* aFilter,
+ const IntSize& aSize,
+ uint8_t aPass,
+ const vector<BGRAColor>& aColors)
+{
+ WriteState result = WriteState::NEED_MORE_DATA;
+
+ for (int32_t row = 0; row < aSize.height; ++row) {
+ // Compute uninterpolated pixels for this row.
+ vector<BGRAColor> pixels =
+ Move(ADAM7HorizontallyInterpolatedRow(aPass, row, aSize.width,
+ ShouldInterpolate::eNo, aColors));
+
+ // Write them to the surface.
+ auto pixelIterator = pixels.cbegin();
+ result = aFilter->WritePixelsToRow<uint32_t>([&]{
+ return AsVariant((*pixelIterator++).AsPixel());
+ });
+
+ if (result != WriteState::NEED_MORE_DATA) {
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool
+CheckHorizontallyInterpolatedImage(Decoder* aDecoder,
+ const IntSize& aSize,
+ uint8_t aPass,
+ const vector<BGRAColor>& aColors)
+{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+
+ for (int32_t row = 0; row < aSize.height; ++row) {
+ if (!IsImportantRow(row, aPass)) {
+ continue; // Don't check rows which aren't important on this pass.
+ }
+
+ // Compute the expected pixels, *with* interpolation to match what the
+ // filter should have done.
+ vector<BGRAColor> expectedPixels =
+ Move(ADAM7HorizontallyInterpolatedRow(aPass, row, aSize.width,
+ ShouldInterpolate::eYes, aColors));
+
+ if (!RowHasPixels(surface, row, expectedPixels)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+CheckHorizontalInterpolation(const IntSize& aSize,
+ const vector<BGRAColor>& aColors)
+{
+ const IntRect surfaceRect(IntPoint(0, 0), aSize);
+
+ WithADAM7InterpolatingFilter(aSize,
+ [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+ // We check horizontal interpolation behavior for each pass individually. In
+ // addition to the normal 7 passes that ADAM7 includes, we also check an
+ // eighth pass to verify that nothing breaks if extra data is written.
+ for (uint8_t pass = 1; pass <= 8; ++pass) {
+ // Write our color pattern to the surface. We don't perform any
+ // interpolation when writing to the filter so that we can check that the
+ // filter itself *does*.
+ WriteState result =
+ WriteUninterpolatedPixels(aFilter, aSize, pass, aColors);
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ AssertCorrectPipelineFinalState(aFilter, surfaceRect, surfaceRect);
+
+ // Check that the generated image matches the expected pattern, with
+ // interpolation applied.
+ EXPECT_TRUE(CheckHorizontallyInterpolatedImage(aDecoder, aSize,
+ pass, aColors));
+
+ // Prepare for the next pass.
+ aFilter->ResetToFirstRow();
+ }
+ });
+}
+
+BGRAColor
+ADAM7RowColor(int32_t aRow,
+ uint8_t aPass,
+ const vector<BGRAColor>& aColors)
+{
+ EXPECT_LT(0, aPass);
+ EXPECT_GE(8, aPass);
+ EXPECT_LT(0u, aColors.size());
+
+ // If this is an important row, select the color from the provided vector of
+ // colors, which we cycle through infinitely. If not, just fill the row with
+ // transparent pixels.
+ return IsImportantRow(aRow, aPass) ? aColors[aRow % aColors.size()]
+ : BGRAColor::Transparent();
+}
+
+WriteState
+WriteRowColorPixels(SurfaceFilter* aFilter,
+ const IntSize& aSize,
+ uint8_t aPass,
+ const vector<BGRAColor>& aColors)
+{
+ WriteState result = WriteState::NEED_MORE_DATA;
+
+ for (int32_t row = 0; row < aSize.height; ++row) {
+ const uint32_t color = ADAM7RowColor(row, aPass, aColors).AsPixel();
+
+ // Fill the surface with |color| pixels.
+ result = aFilter->WritePixelsToRow<uint32_t>([&]{ return AsVariant(color); });
+
+ if (result != WriteState::NEED_MORE_DATA) {
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool
+CheckVerticallyInterpolatedImage(Decoder* aDecoder,
+ const IntSize& aSize,
+ uint8_t aPass,
+ const vector<BGRAColor>& aColors)
+{
+ vector<float>& weights = InterpolationWeights(ImportantRowStride(aPass));
+
+ for (int32_t row = 0; row < aSize.height; ++row) {
+ // Vertically interpolation takes place between two important rows. The
+ // separation between the important rows is determined by the stride of this
+ // pass. When there is no "next" important row because we'd run off the
+ // bottom of the image, we use the same row for both. This matches
+ // ADAM7InterpolatingFilter's behavior of duplicating the last important row
+ // since there isn't another important row to vertically interpolate it
+ // with.
+ const int32_t stride = ImportantRowStride(aPass);
+ const int32_t prevImportantRow = row - row % stride;
+ const int32_t maybeNextImportantRow = prevImportantRow + stride;
+ const int32_t nextImportantRow = maybeNextImportantRow < aSize.height
+ ? maybeNextImportantRow
+ : prevImportantRow;
+
+ // Retrieve the colors for the important rows we're going to interpolate.
+ const BGRAColor prevImportantRowColor =
+ ADAM7RowColor(prevImportantRow, aPass, aColors);
+ const BGRAColor nextImportantRowColor =
+ ADAM7RowColor(nextImportantRow, aPass, aColors);
+
+ // The weight we'll use for interpolation is also determined by the stride.
+ // A row halfway between two important rows should have pixels that have a
+ // 50% contribution from each of the important rows, for example.
+ const float weight = weights[row % stride];
+ const BGRAColor interpolatedColor =
+ InterpolateColors(prevImportantRowColor, nextImportantRowColor, weight);
+
+ // Generate a row of expected pixels. Every pixel in the row is always the
+ // same color since we're only testing vertical interpolation between
+ // solid-colored rows.
+ vector<BGRAColor> expectedPixels(aSize.width);
+ generate(expectedPixels.begin(), expectedPixels.end(), [&]{
+ return interpolatedColor;
+ });
+
+ // Check that the pixels match.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+ if (!RowHasPixels(surface, row, expectedPixels)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+CheckVerticalInterpolation(const IntSize& aSize,
+ const vector<BGRAColor>& aColors)
+{
+ const IntRect surfaceRect(IntPoint(0, 0), aSize);
+
+ WithADAM7InterpolatingFilter(aSize,
+ [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+ for (uint8_t pass = 1; pass <= 8; ++pass) {
+ // Write a pattern of rows to the surface. Important rows will receive a
+ // color selected from |aColors|; unimportant rows will be transparent.
+ WriteState result = WriteRowColorPixels(aFilter, aSize, pass, aColors);
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ AssertCorrectPipelineFinalState(aFilter, surfaceRect, surfaceRect);
+
+ // Check that the generated image matches the expected pattern, with
+ // interpolation applied.
+ EXPECT_TRUE(CheckVerticallyInterpolatedImage(aDecoder, aSize,
+ pass, aColors));
+
+ // Prepare for the next pass.
+ aFilter->ResetToFirstRow();
+ }
+ });
+}
+
+void
+CheckInterpolation(const IntSize& aSize, const vector<BGRAColor>& aColors)
+{
+ CheckHorizontalInterpolation(aSize, aColors);
+ CheckVerticalInterpolation(aSize, aColors);
+}
+
+void
+CheckADAM7InterpolatingWritePixels(const IntSize& aSize)
+{
+ // This test writes 8 passes of green pixels (the seven ADAM7 passes, plus one
+ // extra to make sure nothing goes wrong if we write too much input) and verifies
+ // that the output is a solid green surface each time. Because all the pixels
+ // are the same color, interpolation doesn't matter; we test the correctness
+ // of the interpolation algorithm itself separately.
+ WithADAM7InterpolatingFilter(aSize,
+ [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+ IntRect rect(IntPoint(0, 0), aSize);
+
+ for (int32_t pass = 1; pass <= 8; ++pass) {
+ // We only actually write up to the last important row for each pass,
+ // because that row unambiguously determines the remaining rows.
+ const int32_t lastRow = aSize.height - 1;
+ const int32_t lastImportantRow =
+ lastRow - (lastRow % ImportantRowStride(pass));
+ const IntRect inputWriteRect(0, 0, aSize.width, lastImportantRow + 1);
+
+ CheckWritePixels(aDecoder, aFilter,
+ /* aOutputRect = */ Some(rect),
+ /* aInputRect = */ Some(rect),
+ /* aInputWriteRect = */ Some(inputWriteRect));
+
+ aFilter->ResetToFirstRow();
+ EXPECT_FALSE(aFilter->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aFilter->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+ }
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels100_100)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(100, 100));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels99_99)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(99, 99));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels66_33)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(66, 33));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels33_66)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(33, 66));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels15_15)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(15, 15));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels9_9)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(9, 9));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels8_8)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(8, 8));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels7_7)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(7, 7));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels3_3)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(3, 3));
+}
+
+TEST(ImageADAM7InterpolatingFilter, WritePixels1_1)
+{
+ CheckADAM7InterpolatingWritePixels(IntSize(1, 1));
+}
+
+TEST(ImageADAM7InterpolatingFilter, TrivialInterpolation48_48)
+{
+ CheckInterpolation(IntSize(48, 48), { BGRAColor::Green() });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput33_17)
+{
+ // We check interpolation using irregular patterns to make sure that the
+ // interpolation will look different for different passes.
+ CheckInterpolation(IntSize(33, 17), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput32_16)
+{
+ CheckInterpolation(IntSize(32, 16), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput31_15)
+{
+ CheckInterpolation(IntSize(31, 15), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Blue(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput17_33)
+{
+ CheckInterpolation(IntSize(17, 33), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput16_32)
+{
+ CheckInterpolation(IntSize(16, 32), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput15_31)
+{
+ CheckInterpolation(IntSize(15, 31), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(),
+ BGRAColor::Red(), BGRAColor::Green(), BGRAColor::Blue(), BGRAColor::Red(),
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput9_9)
+{
+ CheckInterpolation(IntSize(9, 9), {
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput8_8)
+{
+ CheckInterpolation(IntSize(8, 8), {
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput7_7)
+{
+ CheckInterpolation(IntSize(7, 7), {
+ BGRAColor::Blue(), BGRAColor::Blue(), BGRAColor::Red(), BGRAColor::Green(),
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Red(), BGRAColor::Blue()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput3_3)
+{
+ CheckInterpolation(IntSize(3, 3), {
+ BGRAColor::Green(), BGRAColor::Red(), BGRAColor::Blue(), BGRAColor::Red()
+ });
+}
+
+TEST(ImageADAM7InterpolatingFilter, InterpolationOutput1_1)
+{
+ CheckInterpolation(IntSize(1, 1), { BGRAColor::Blue() });
+}
+
+TEST(ImageADAM7InterpolatingFilter, ADAM7InterpolationFailsFor0_0)
+{
+ // A 0x0 input size is invalid, so configuration should fail.
+ AssertConfiguringADAM7InterpolatingFilterFails(IntSize(0, 0));
+}
+
+TEST(ImageADAM7InterpolatingFilter, ADAM7InterpolationFailsForMinus1_Minus1)
+{
+ // A negative input size is invalid, so configuration should fail.
+ AssertConfiguringADAM7InterpolatingFilterFails(IntSize(-1, -1));
+}
+
+TEST(ImageADAM7InterpolatingFilter, ConfiguringPalettedADAM7InterpolatingFilterFails)
+{
+ RefPtr<Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ // ADAM7InterpolatingFilter does not support paletted images, so configuration
+ // should fail.
+ AssertConfiguringPipelineFails(decoder,
+ ADAM7InterpolatingConfig { },
+ PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+ IntRect(0, 0, 50, 50),
+ SurfaceFormat::B8G8R8A8, 8,
+ false });
+}
diff --git a/image/test/gtest/TestCopyOnWrite.cpp b/image/test/gtest/TestCopyOnWrite.cpp
new file mode 100644
index 000000000..0d420b672
--- /dev/null
+++ b/image/test/gtest/TestCopyOnWrite.cpp
@@ -0,0 +1,235 @@
+/* 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 "CopyOnWrite.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+struct ValueStats
+{
+ int32_t mCopies = 0;
+ int32_t mFrees = 0;
+ int32_t mCalls = 0;
+ int32_t mConstCalls = 0;
+ int32_t mSerial = 0;
+};
+
+struct Value
+{
+ NS_INLINE_DECL_REFCOUNTING(Value)
+
+ explicit Value(ValueStats& aStats)
+ : mStats(aStats)
+ , mSerial(mStats.mSerial++)
+ { }
+
+ Value(const Value& aOther)
+ : mStats(aOther.mStats)
+ , mSerial(mStats.mSerial++)
+ {
+ mStats.mCopies++;
+ }
+
+ void Go() { mStats.mCalls++; }
+ void Go() const { mStats.mConstCalls++; }
+
+ int32_t Serial() const { return mSerial; }
+
+protected:
+ ~Value() { mStats.mFrees++; }
+
+private:
+ ValueStats& mStats;
+ int32_t mSerial;
+};
+
+TEST(ImageCopyOnWrite, Read)
+{
+ ValueStats stats;
+
+ {
+ CopyOnWrite<Value> cow(new Value(stats));
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_TRUE(cow.CanRead());
+
+ cow.Read([&](const Value* aValue) {
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, aValue->Serial());
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ aValue->Go();
+
+ EXPECT_EQ(0, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ });
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ }
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(1, stats.mFrees);
+}
+
+TEST(ImageCopyOnWrite, RecursiveRead)
+{
+ ValueStats stats;
+
+ {
+ CopyOnWrite<Value> cow(new Value(stats));
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_TRUE(cow.CanRead());
+
+ cow.Read([&](const Value* aValue) {
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, aValue->Serial());
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ // Make sure that Read() inside a Read() succeeds.
+ cow.Read([&](const Value* aValue) {
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, aValue->Serial());
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ aValue->Go();
+
+ EXPECT_EQ(0, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ }, []() {
+ // This gets called if we can't read. We shouldn't get here.
+ EXPECT_TRUE(false);
+ });
+ });
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ }
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(1, stats.mFrees);
+}
+
+TEST(ImageCopyOnWrite, Write)
+{
+ ValueStats stats;
+
+ {
+ CopyOnWrite<Value> cow(new Value(stats));
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ cow.Write([&](Value* aValue) {
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, aValue->Serial());
+ EXPECT_TRUE(!cow.CanRead());
+ EXPECT_TRUE(!cow.CanWrite());
+
+ aValue->Go();
+
+ EXPECT_EQ(1, stats.mCalls);
+ EXPECT_EQ(0, stats.mConstCalls);
+ });
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(1, stats.mCalls);
+ EXPECT_EQ(0, stats.mConstCalls);
+ }
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(1, stats.mFrees);
+}
+
+TEST(ImageCopyOnWrite, WriteRecursive)
+{
+ ValueStats stats;
+
+ {
+ CopyOnWrite<Value> cow(new Value(stats));
+
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ cow.Read([&](const Value* aValue) {
+ EXPECT_EQ(0, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(0, aValue->Serial());
+ EXPECT_TRUE(cow.CanRead());
+ EXPECT_TRUE(cow.CanWrite());
+
+ // Make sure Write() inside a Read() succeeds.
+ cow.Write([&](Value* aValue) {
+ EXPECT_EQ(1, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(1, aValue->Serial());
+ EXPECT_TRUE(!cow.CanRead());
+ EXPECT_TRUE(!cow.CanWrite());
+
+ aValue->Go();
+
+ EXPECT_EQ(1, stats.mCalls);
+ EXPECT_EQ(0, stats.mConstCalls);
+
+ // Make sure Read() inside a Write() fails.
+ cow.Read([](const Value* aValue) {
+ // This gets called if we can read. We shouldn't get here.
+ EXPECT_TRUE(false);
+ }, []() {
+ // This gets called if we can't read. We *should* get here.
+ EXPECT_TRUE(true);
+ });
+
+ // Make sure Write() inside a Write() fails.
+ cow.Write([](Value* aValue) {
+ // This gets called if we can write. We shouldn't get here.
+ EXPECT_TRUE(false);
+ }, []() {
+ // This gets called if we can't write. We *should* get here.
+ EXPECT_TRUE(true);
+ });
+ }, []() {
+ // This gets called if we can't write. We shouldn't get here.
+ EXPECT_TRUE(false);
+ });
+
+ aValue->Go();
+
+ EXPECT_EQ(1, stats.mCopies);
+ EXPECT_EQ(0, stats.mFrees);
+ EXPECT_EQ(1, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ });
+
+ EXPECT_EQ(1, stats.mCopies);
+ EXPECT_EQ(1, stats.mFrees);
+ EXPECT_EQ(1, stats.mCalls);
+ EXPECT_EQ(1, stats.mConstCalls);
+ }
+
+ EXPECT_EQ(1, stats.mCopies);
+ EXPECT_EQ(2, stats.mFrees);
+}
diff --git a/image/test/gtest/TestDecodeToSurface.cpp b/image/test/gtest/TestDecodeToSurface.cpp
new file mode 100644
index 000000000..bd52e7590
--- /dev/null
+++ b/image/test/gtest/TestDecodeToSurface.cpp
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "Common.h"
+#include "imgIContainer.h"
+#include "imgITools.h"
+#include "ImageOps.h"
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+#include "nsIRunnable.h"
+#include "nsIThread.h"
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+class DecodeToSurfaceRunnable : public Runnable
+{
+public:
+ DecodeToSurfaceRunnable(RefPtr<SourceSurface>& aSurface,
+ nsIInputStream* aInputStream,
+ const ImageTestCase& aTestCase)
+ : mSurface(aSurface)
+ , mInputStream(aInputStream)
+ , mTestCase(aTestCase)
+ { }
+
+ NS_IMETHOD Run() override
+ {
+ Go();
+ return NS_OK;
+ }
+
+ void Go()
+ {
+ mSurface =
+ ImageOps::DecodeToSurface(mInputStream,
+ nsDependentCString(mTestCase.mMimeType),
+ imgIContainer::DECODE_FLAGS_DEFAULT);
+ 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 ? 1 : 0));
+ }
+
+private:
+ RefPtr<SourceSurface>& mSurface;
+ nsCOMPtr<nsIInputStream> mInputStream;
+ ImageTestCase mTestCase;
+};
+
+static void
+RunDecodeToSurface(const ImageTestCase& aTestCase)
+{
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewThread(getter_AddRefs(thread), nullptr);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // We run the DecodeToSurface tests off-main-thread to ensure that
+ // DecodeToSurface doesn't require any main-thread-only code.
+ RefPtr<SourceSurface> surface;
+ nsCOMPtr<nsIRunnable> runnable =
+ new DecodeToSurfaceRunnable(surface, inputStream, aTestCase);
+ thread->Dispatch(runnable, nsIThread::DISPATCH_SYNC);
+
+ thread->Shutdown();
+
+ // Explicitly release the SourceSurface on the main thread.
+ surface = nullptr;
+}
+
+class ImageDecodeToSurface : public ::testing::Test
+{
+protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageDecodeToSurface, PNG) { RunDecodeToSurface(GreenPNGTestCase()); }
+TEST_F(ImageDecodeToSurface, GIF) { RunDecodeToSurface(GreenGIFTestCase()); }
+TEST_F(ImageDecodeToSurface, JPG) { RunDecodeToSurface(GreenJPGTestCase()); }
+TEST_F(ImageDecodeToSurface, BMP) { RunDecodeToSurface(GreenBMPTestCase()); }
+TEST_F(ImageDecodeToSurface, ICO) { RunDecodeToSurface(GreenICOTestCase()); }
+TEST_F(ImageDecodeToSurface, Icon) { RunDecodeToSurface(GreenIconTestCase()); }
+
+TEST_F(ImageDecodeToSurface, AnimatedGIF)
+{
+ RunDecodeToSurface(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecodeToSurface, AnimatedPNG)
+{
+ RunDecodeToSurface(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecodeToSurface, Corrupt)
+{
+ ImageTestCase testCase = CorruptTestCase();
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ RefPtr<SourceSurface> surface =
+ ImageOps::DecodeToSurface(inputStream,
+ nsDependentCString(testCase.mMimeType),
+ imgIContainer::DECODE_FLAGS_DEFAULT);
+ EXPECT_TRUE(surface == nullptr);
+}
diff --git a/image/test/gtest/TestDecoders.cpp b/image/test/gtest/TestDecoders.cpp
new file mode 100644
index 000000000..58caa77a2
--- /dev/null
+++ b/image/test/gtest/TestDecoders.cpp
@@ -0,0 +1,669 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "decoders/nsBMPDecoder.h"
+#include "IDecodingTask.h"
+#include "imgIContainer.h"
+#include "imgITools.h"
+#include "ImageFactory.h"
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+#include "nsIRunnable.h"
+#include "nsIThread.h"
+#include "mozilla/RefPtr.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "ProgressTracker.h"
+#include "SourceBuffer.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+static already_AddRefed<SourceSurface>
+CheckDecoderState(const ImageTestCase& aTestCase, Decoder* aDecoder)
+{
+ EXPECT_TRUE(aDecoder->GetDecodeDone());
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR),
+ aDecoder->HasError());
+
+ // Verify that the decoder made the expected progress.
+ Progress progress = aDecoder->TakeProgress();
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR),
+ bool(progress & FLAG_HAS_ERROR));
+
+ if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) {
+ return nullptr; // That's all we can check for bad images.
+ }
+
+ EXPECT_TRUE(bool(progress & FLAG_SIZE_AVAILABLE));
+ EXPECT_TRUE(bool(progress & FLAG_DECODE_COMPLETE));
+ EXPECT_TRUE(bool(progress & FLAG_FRAME_COMPLETE));
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_TRANSPARENT),
+ bool(progress & FLAG_HAS_TRANSPARENCY));
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED),
+ bool(progress & FLAG_IS_ANIMATED));
+
+ // The decoder should get the correct size.
+ IntSize size = aDecoder->Size();
+ EXPECT_EQ(aTestCase.mSize.width, size.width);
+ EXPECT_EQ(aTestCase.mSize.height, size.height);
+
+ // Get the current frame, which is always the first frame of the image
+ // because CreateAnonymousDecoder() forces a first-frame-only decode.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+
+ // 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.mOutputSize, surface->GetSize());
+
+ return surface.forget();
+}
+
+static void
+CheckDecoderResults(const ImageTestCase& aTestCase, Decoder* aDecoder)
+{
+ RefPtr<SourceSurface> surface = CheckDecoderState(aTestCase, aDecoder);
+ if (!surface) {
+ return;
+ }
+
+ if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) {
+ return;
+ }
+
+ // Check the output.
+ EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green(),
+ aTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
+}
+
+template <typename Func>
+void WithSingleChunkDecode(const ImageTestCase& aTestCase,
+ const Maybe<IntSize>& aOutputSize,
+ Func aResultChecker)
+{
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into a SourceBuffer.
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ sourceBuffer->ExpectLength(length);
+ rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ sourceBuffer->Complete(NS_OK);
+
+ // Create a decoder.
+ DecoderType decoderType =
+ DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<Decoder> decoder =
+ DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, aOutputSize,
+ DefaultSurfaceFlags());
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
+
+ // Run the full decoder synchronously.
+ task->Run();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(decoder);
+}
+
+static void
+CheckDecoderSingleChunk(const ImageTestCase& aTestCase)
+{
+ WithSingleChunkDecode(aTestCase, Nothing(), [&](Decoder* aDecoder) {
+ CheckDecoderResults(aTestCase, aDecoder);
+ });
+}
+
+static void
+CheckDecoderMultiChunk(const ImageTestCase& aTestCase)
+{
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Create a SourceBuffer and a decoder.
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ sourceBuffer->ExpectLength(length);
+ DecoderType decoderType =
+ DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<Decoder> decoder =
+ DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+ DefaultSurfaceFlags());
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
+
+ for (uint64_t read = 0; read < length ; ++read) {
+ uint64_t available = 0;
+ rv = inputStream->Available(&available);
+ ASSERT_TRUE(available > 0);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ rv = sourceBuffer->AppendFromInputStream(inputStream, 1);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ task->Run();
+ }
+
+ sourceBuffer->Complete(NS_OK);
+ task->Run();
+
+ CheckDecoderResults(aTestCase, decoder);
+}
+
+static void
+CheckDownscaleDuringDecode(const ImageTestCase& aTestCase)
+{
+ // This function expects that |aTestCase| 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. We'll downscale it from 100x100 to 20x20.
+ IntSize outputSize(20, 20);
+
+ WithSingleChunkDecode(aTestCase, Some(outputSize), [&](Decoder* aDecoder) {
+ RefPtr<SourceSurface> surface = CheckDecoderState(aTestCase, aDecoder);
+
+ // There are no downscale-during-decode tests that have TEST_CASE_HAS_ERROR
+ // set, so we expect to always get a surface here.
+ EXPECT_TRUE(surface != nullptr);
+
+ if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) {
+ return;
+ }
+
+ // Check that the downscaled 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.
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 47));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 27));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 47));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 27));
+ });
+}
+
+class ImageDecoders : public ::testing::Test
+{
+protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageDecoders, PNGSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenPNGTestCase());
+}
+
+TEST_F(ImageDecoders, PNGMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenPNGTestCase());
+}
+
+TEST_F(ImageDecoders, PNGDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledPNGTestCase());
+}
+
+TEST_F(ImageDecoders, GIFSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenGIFTestCase());
+}
+
+TEST_F(ImageDecoders, GIFMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenGIFTestCase());
+}
+
+TEST_F(ImageDecoders, GIFDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledGIFTestCase());
+}
+
+TEST_F(ImageDecoders, JPGSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenJPGTestCase());
+}
+
+TEST_F(ImageDecoders, JPGMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenJPGTestCase());
+}
+
+TEST_F(ImageDecoders, JPGDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledJPGTestCase());
+}
+
+TEST_F(ImageDecoders, BMPSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenBMPTestCase());
+}
+
+TEST_F(ImageDecoders, BMPMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenBMPTestCase());
+}
+
+TEST_F(ImageDecoders, BMPDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledBMPTestCase());
+}
+
+TEST_F(ImageDecoders, ICOSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenICOTestCase());
+}
+
+TEST_F(ImageDecoders, ICOMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenICOTestCase());
+}
+
+TEST_F(ImageDecoders, ICODownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledICOTestCase());
+}
+
+TEST_F(ImageDecoders, ICOWithANDMaskDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledTransparentICOWithANDMaskTestCase());
+}
+
+TEST_F(ImageDecoders, IconSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenIconTestCase());
+}
+
+TEST_F(ImageDecoders, IconMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenIconTestCase());
+}
+
+TEST_F(ImageDecoders, IconDownscaleDuringDecode)
+{
+ CheckDownscaleDuringDecode(DownscaledIconTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedPNGSingleChunk)
+{
+ CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedPNGMultiChunk)
+{
+ CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptSingleChunk)
+{
+ CheckDecoderSingleChunk(CorruptTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptMultiChunk)
+{
+ CheckDecoderMultiChunk(CorruptTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderSingleChunk)
+{
+ CheckDecoderSingleChunk(CorruptBMPWithTruncatedHeader());
+}
+
+TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderMultiChunk)
+{
+ CheckDecoderMultiChunk(CorruptBMPWithTruncatedHeader());
+}
+
+TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthSingleChunk)
+{
+ CheckDecoderSingleChunk(CorruptICOWithBadBMPWidthTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthMultiChunk)
+{
+ CheckDecoderMultiChunk(CorruptICOWithBadBMPWidthTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptICOWithBadBMPHeightSingleChunk)
+{
+ CheckDecoderSingleChunk(CorruptICOWithBadBMPHeightTestCase());
+}
+
+TEST_F(ImageDecoders, CorruptICOWithBadBMPHeightMultiChunk)
+{
+ CheckDecoderMultiChunk(CorruptICOWithBadBMPHeightTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFWithFRAME_FIRST)
+{
+ ImageTestCase testCase = GreenFirstFrameAnimatedGIFTestCase();
+
+ // Verify that we can decode this test case and retrieve the first frame using
+ // imgIContainer::FRAME_FIRST. This ensures that we correctly trigger a
+ // single-frame decode rather than an animated decode when
+ // imgIContainer::FRAME_FIRST is requested.
+
+ // Create an image.
+ RefPtr<Image> image =
+ ImageFactory::CreateAnonymousImage(nsDependentCString(testCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into the image.
+ rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+ static_cast<uint32_t>(length));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Let the image know we've sent all the data.
+ rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
+ tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
+
+ // Lock the image so its surfaces don't disappear during the test.
+ image->LockImage();
+
+ // Use GetFrame() to force a sync decode of the image, specifying FRAME_FIRST
+ // to ensure that we don't get an animated decode.
+ RefPtr<SourceSurface> surface =
+ image->GetFrame(imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that the image's metadata meets our expectations.
+ IntSize imageSize(0, 0);
+ rv = image->GetWidth(&imageSize.width);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ rv = image->GetHeight(&imageSize.height);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ EXPECT_EQ(testCase.mSize.width, imageSize.width);
+ EXPECT_EQ(testCase.mSize.height, imageSize.height);
+
+ Progress imageProgress = tracker->GetProgress();
+
+ EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
+ EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
+
+ // Ensure that we decoded the static version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eStatic));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+
+ // Ensure that we didn't decode the animated version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::NOT_FOUND, result.Type());
+ }
+
+ // Use GetFrame() to force a sync decode of the image, this time specifying
+ // FRAME_CURRENT to ensure that we get an animated decode.
+ RefPtr<SourceSurface> animatedSurface =
+ image->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that we decoded both frames of the animated version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+
+ // Ensure that the static version is still around.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eStatic));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+}
+
+TEST_F(ImageDecoders, AnimatedGIFWithFRAME_CURRENT)
+{
+ ImageTestCase testCase = GreenFirstFrameAnimatedGIFTestCase();
+
+ // Verify that we can decode this test case and retrieve the entire sequence
+ // of frames using imgIContainer::FRAME_CURRENT. This ensures that we
+ // correctly trigger an animated decode rather than a single-frame decode when
+ // imgIContainer::FRAME_CURRENT is requested.
+
+ // Create an image.
+ RefPtr<Image> image =
+ ImageFactory::CreateAnonymousImage(nsDependentCString(testCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into the image.
+ rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+ static_cast<uint32_t>(length));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Let the image know we've sent all the data.
+ rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
+ tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
+
+ // Lock the image so its surfaces don't disappear during the test.
+ image->LockImage();
+
+ // Use GetFrame() to force a sync decode of the image, specifying
+ // FRAME_CURRENT to ensure we get an animated decode.
+ RefPtr<SourceSurface> surface =
+ image->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that the image's metadata meets our expectations.
+ IntSize imageSize(0, 0);
+ rv = image->GetWidth(&imageSize.width);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ rv = image->GetHeight(&imageSize.height);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ EXPECT_EQ(testCase.mSize.width, imageSize.width);
+ EXPECT_EQ(testCase.mSize.height, imageSize.height);
+
+ Progress imageProgress = tracker->GetProgress();
+
+ EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
+ EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
+
+ // Ensure that we decoded both frames of the animated version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+
+ // Ensure that we didn't decode the static version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eStatic));
+ ASSERT_EQ(MatchType::NOT_FOUND, result.Type());
+ }
+
+ // Use GetFrame() to force a sync decode of the image, this time specifying
+ // FRAME_FIRST to ensure that we get a single-frame decode.
+ RefPtr<SourceSurface> animatedSurface =
+ image->GetFrame(imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that we decoded the static version of the image.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eStatic));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+
+ // Ensure that both frames of the animated version are still around.
+ {
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+}
+
+TEST_F(ImageDecoders, AnimatedGIFWithExtraImageSubBlocks)
+{
+ ImageTestCase testCase = ExtraImageSubBlocksAnimatedGIFTestCase();
+
+ // Verify that we can decode this test case and get two frames, even though
+ // there are extra image sub blocks between the first and second frame. The
+ // extra data shouldn't confuse the decoder or cause the decode to fail.
+
+ // Create an image.
+ RefPtr<Image> image =
+ ImageFactory::CreateAnonymousImage(nsDependentCString(testCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into the image.
+ rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+ static_cast<uint32_t>(length));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Let the image know we've sent all the data.
+ rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
+ tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
+
+ // Use GetFrame() to force a sync decode of the image.
+ RefPtr<SourceSurface> surface =
+ image->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that the image's metadata meets our expectations.
+ IntSize imageSize(0, 0);
+ rv = image->GetWidth(&imageSize.width);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ rv = image->GetHeight(&imageSize.height);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ EXPECT_EQ(testCase.mSize.width, imageSize.width);
+ EXPECT_EQ(testCase.mSize.height, imageSize.height);
+
+ Progress imageProgress = tracker->GetProgress();
+
+ EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
+ EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
+
+ // Ensure that we decoded both frames of the image.
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+ EXPECT_TRUE(bool(result.Surface()));
+}
+
+TEST_F(ImageDecoders, TruncatedSmallGIFSingleChunk)
+{
+ CheckDecoderSingleChunk(TruncatedSmallGIFTestCase());
+}
diff --git a/image/test/gtest/TestDeinterlacingFilter.cpp b/image/test/gtest/TestDeinterlacingFilter.cpp
new file mode 100644
index 000000000..30cad7993
--- /dev/null
+++ b/image/test/gtest/TestDeinterlacingFilter.cpp
@@ -0,0 +1,672 @@
+/* -*- 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 });
+}
+
+class ImageDeinterlacingFilter : public ::testing::Test
+{
+protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(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_F(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_F(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_F(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_F(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_F(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_F(ImageDeinterlacingFilter, PalettedWritePixels)
+{
+ WithPalettedDeinterlacingFilter(IntSize(100, 100),
+ [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckPalettedWritePixels(aDecoder, aFilter);
+ });
+}
+
+TEST_F(ImageDeinterlacingFilter, WritePixelsNonProgressiveOutput51_52)
+{
+ WithDeinterlacingFilter(IntSize(51, 52), /* aProgressiveDisplay = */ false,
+ [](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 / 51; // Integer division.
+ ++count;
+
+ // Note that we use a switch statement here, even though it's quite
+ // verbose, because it's useful to have the mappings between input and
+ // output rows available when debugging these tests.
+
+ 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.
+ case 3: // Output row 24.
+ case 4: // Output row 32.
+ case 5: // Output row 40.
+ case 6: // Output row 48.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ // Second pass. Rows are positioned at 8n + 4.
+ case 7: // Output row 4.
+ case 8: // Output row 12.
+ case 9: // Output row 20.
+ case 10: // Output row 28.
+ case 11: // Output row 36.
+ case 12: // Output row 44.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ // Third pass. Rows are positioned at 4n + 2.
+ case 13: // Output row 2.
+ case 14: // Output row 6.
+ case 15: // Output row 10.
+ case 16: // Output row 14.
+ case 17: // Output row 18.
+ case 18: // Output row 22.
+ case 19: // Output row 26.
+ case 20: // Output row 30.
+ case 21: // Output row 34.
+ case 22: // Output row 38.
+ case 23: // Output row 42.
+ case 24: // Output row 46.
+ case 25: // Output row 50.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ // Fourth pass. Rows are positioned at 2n + 1.
+ case 26: // Output row 1.
+ case 27: // Output row 3.
+ case 28: // Output row 5.
+ case 29: // Output row 7.
+ case 30: // Output row 9.
+ case 31: // Output row 11.
+ case 32: // Output row 13.
+ case 33: // Output row 15.
+ case 34: // Output row 17.
+ case 35: // Output row 19.
+ case 36: // Output row 21.
+ case 37: // Output row 23.
+ case 38: // Output row 25.
+ case 39: // Output row 27.
+ case 40: // Output row 29.
+ case 41: // Output row 31.
+ case 42: // Output row 33.
+ case 43: // Output row 35.
+ case 44: // Output row 37.
+ case 45: // Output row 39.
+ case 46: // Output row 41.
+ case 47: // Output row 43.
+ case 48: // Output row 45.
+ case 49: // Output row 47.
+ case 50: // Output row 49.
+ case 51: // Output row 51.
+ return AsVariant(BGRAColor::Red().AsPixel());
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected row");
+ return AsVariant(BGRAColor::Transparent().AsPixel());
+ }
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(51u * 52u, count);
+
+ AssertCorrectPipelineFinalState(aFilter,
+ IntRect(0, 0, 51, 52),
+ IntRect(0, 0, 51, 52));
+
+ // 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->GetSourceSurface();
+
+ for (uint32_t row = 0; row < 52; ++row) {
+ EXPECT_TRUE(RowsAreSolidColor(surface, row, 1,
+ row % 2 == 0 ? BGRAColor::Green()
+ : BGRAColor::Red()));
+ }
+ });
+}
+
+TEST_F(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;
+
+ // Note that we use a switch statement here, even though it's quite
+ // verbose, because it's useful to have the mappings between input and
+ // output rows available when debugging these tests.
+
+ 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->GetSourceSurface();
+
+ for (uint32_t row = 0; row < 20; ++row) {
+ EXPECT_TRUE(RowsAreSolidColor(surface, row, 1,
+ row % 2 == 0 ? BGRAColor::Green()
+ : BGRAColor::Red()));
+ }
+ });
+}
+
+TEST_F(ImageDeinterlacingFilter, WritePixelsOutput7_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;
+ auto result = aFilter->WritePixels<uint32_t>([&]() {
+ uint32_t row = count / 7; // 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.
+ case 1: // Output row 4.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ // Third pass. Rows are positioned at 4n + 2.
+ case 2: // Output row 2.
+ case 3: // Output row 6.
+ return AsVariant(BGRAColor::Red().AsPixel());
+
+ // Fourth pass. Rows are positioned at 2n + 1.
+ case 4: // Output row 1.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ case 5: // Output row 3.
+ return AsVariant(BGRAColor::Red().AsPixel());
+
+ case 6: // Output row 5.
+ return AsVariant(BGRAColor::Green().AsPixel());
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected row");
+ return AsVariant(BGRAColor::Transparent().AsPixel());
+ }
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(7u * 7u, count);
+
+ 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->GetSourceSurface();
+
+ 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_F(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->GetSourceSurface();
+
+ for (uint32_t row = 0; row < 3; ++row) {
+ EXPECT_TRUE(RowsAreSolidColor(surface, row, 1,
+ row == 0 || row == 2 ? BGRAColor::Green()
+ : BGRAColor::Red()));
+ }
+ });
+}
+
+TEST_F(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->GetSourceSurface();
+
+ 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->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+ if (count < 7) {
+ ++count;
+ return AsVariant(aColor.AsPixel());
+ }
+ return AsVariant(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->GetSourceSurface();
+
+ for (uint32_t row = aFirstHaeberliRow; row <= aLastHaeberliRow; ++row) {
+ EXPECT_TRUE(RowsAreSolidColor(surface, row, 1, aColor));
+ }
+}
+
+TEST_F(ImageDeinterlacingFilter, WritePixelsIntermediateOutput7_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->GetSourceSurface();
+
+ 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_F(ImageDeinterlacingFilter, WritePixelsNonProgressiveIntermediateOutput7_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->GetSourceSurface();
+
+ 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_F(ImageDeinterlacingFilter, DeinterlacingFailsFor0_0)
+{
+ // A 0x0 input size is invalid, so configuration should fail.
+ AssertConfiguringDeinterlacingFilterFails(IntSize(0, 0));
+}
+
+TEST_F(ImageDeinterlacingFilter, DeinterlacingFailsForMinus1_Minus1)
+{
+ // A negative input size is invalid, so configuration should fail.
+ AssertConfiguringDeinterlacingFilterFails(IntSize(-1, -1));
+}
diff --git a/image/test/gtest/TestDownscalingFilter.cpp b/image/test/gtest/TestDownscalingFilter.cpp
new file mode 100644
index 000000000..596becab0
--- /dev/null
+++ b/image/test/gtest/TestDownscalingFilter.cpp
@@ -0,0 +1,231 @@
+/* -*- 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, 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, 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, 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, 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, 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, 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, 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->GetSourceSurface();
+ 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->GetSourceSurface();
+ 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 });
+}
diff --git a/image/test/gtest/TestDownscalingFilterNoSkia.cpp b/image/test/gtest/TestDownscalingFilterNoSkia.cpp
new file mode 100644
index 000000000..c62ca018d
--- /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 });
+}
diff --git a/image/test/gtest/TestMetadata.cpp b/image/test/gtest/TestMetadata.cpp
new file mode 100644
index 000000000..9f3a64898
--- /dev/null
+++ b/image/test/gtest/TestMetadata.cpp
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "decoders/nsBMPDecoder.h"
+#include "IDecodingTask.h"
+#include "imgIContainer.h"
+#include "imgITools.h"
+#include "ImageFactory.h"
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+#include "nsIRunnable.h"
+#include "nsIThread.h"
+#include "mozilla/RefPtr.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "ProgressTracker.h"
+#include "SourceBuffer.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+enum class BMPWithinICO
+{
+ NO,
+ YES
+};
+
+static void
+CheckMetadata(const ImageTestCase& aTestCase,
+ BMPWithinICO aBMPWithinICO = BMPWithinICO::NO)
+{
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into a SourceBuffer.
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ sourceBuffer->ExpectLength(length);
+ rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ sourceBuffer->Complete(NS_OK);
+
+ // Create a metadata decoder.
+ DecoderType decoderType =
+ DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<Decoder> decoder =
+ DecoderFactory::CreateAnonymousMetadataDecoder(decoderType, sourceBuffer);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
+
+ if (aBMPWithinICO == BMPWithinICO::YES) {
+ static_cast<nsBMPDecoder*>(decoder.get())->SetIsWithinICO();
+ }
+
+ // Run the metadata decoder synchronously.
+ task->Run();
+
+ // Ensure that the metadata decoder didn't make progress it shouldn't have
+ // (which would indicate that it decoded past the header of the image).
+ Progress metadataProgress = decoder->TakeProgress();
+ EXPECT_TRUE(0 == (metadataProgress & ~(FLAG_SIZE_AVAILABLE |
+ FLAG_HAS_TRANSPARENCY |
+ FLAG_IS_ANIMATED)));
+
+ // If the test case is corrupt, assert what we can and return early.
+ if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) {
+ EXPECT_TRUE(decoder->GetDecodeDone());
+ EXPECT_TRUE(decoder->HasError());
+ return;
+ }
+
+ EXPECT_TRUE(decoder->GetDecodeDone() && !decoder->HasError());
+
+ // Check that we got the expected metadata.
+ EXPECT_TRUE(metadataProgress & FLAG_SIZE_AVAILABLE);
+
+ IntSize metadataSize = decoder->Size();
+ EXPECT_EQ(aTestCase.mSize.width, metadataSize.width);
+ EXPECT_EQ(aTestCase.mSize.height, metadataSize.height);
+
+ bool expectTransparency = aBMPWithinICO == BMPWithinICO::YES
+ ? true
+ : bool(aTestCase.mFlags & TEST_CASE_IS_TRANSPARENT);
+ EXPECT_EQ(expectTransparency, bool(metadataProgress & FLAG_HAS_TRANSPARENCY));
+
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED),
+ bool(metadataProgress & FLAG_IS_ANIMATED));
+
+ // Create a full decoder, so we can compare the result.
+ decoder =
+ DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+ DefaultSurfaceFlags());
+ ASSERT_TRUE(decoder != nullptr);
+ task = new AnonymousDecodingTask(WrapNotNull(decoder));
+
+ if (aBMPWithinICO == BMPWithinICO::YES) {
+ static_cast<nsBMPDecoder*>(decoder.get())->SetIsWithinICO();
+ }
+
+ // Run the full decoder synchronously.
+ task->Run();
+
+ EXPECT_TRUE(decoder->GetDecodeDone() && !decoder->HasError());
+ Progress fullProgress = decoder->TakeProgress();
+
+ // If the metadata decoder set a progress bit, the full decoder should also
+ // have set the same bit.
+ EXPECT_EQ(fullProgress, metadataProgress | fullProgress);
+
+ // The full decoder and the metadata decoder should agree on the image's size.
+ IntSize fullSize = decoder->Size();
+ EXPECT_EQ(metadataSize.width, fullSize.width);
+ EXPECT_EQ(metadataSize.height, fullSize.height);
+
+ // We should not discover transparency during the full decode that we didn't
+ // discover during the metadata decode, unless the image is animated.
+ EXPECT_TRUE(!(fullProgress & FLAG_HAS_TRANSPARENCY) ||
+ (metadataProgress & FLAG_HAS_TRANSPARENCY) ||
+ (fullProgress & FLAG_IS_ANIMATED));
+}
+
+class ImageDecoderMetadata : public ::testing::Test
+{
+protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageDecoderMetadata, PNG) { CheckMetadata(GreenPNGTestCase()); }
+TEST_F(ImageDecoderMetadata, TransparentPNG) { CheckMetadata(TransparentPNGTestCase()); }
+TEST_F(ImageDecoderMetadata, GIF) { CheckMetadata(GreenGIFTestCase()); }
+TEST_F(ImageDecoderMetadata, TransparentGIF) { CheckMetadata(TransparentGIFTestCase()); }
+TEST_F(ImageDecoderMetadata, JPG) { CheckMetadata(GreenJPGTestCase()); }
+TEST_F(ImageDecoderMetadata, BMP) { CheckMetadata(GreenBMPTestCase()); }
+TEST_F(ImageDecoderMetadata, ICO) { CheckMetadata(GreenICOTestCase()); }
+TEST_F(ImageDecoderMetadata, Icon) { CheckMetadata(GreenIconTestCase()); }
+
+TEST_F(ImageDecoderMetadata, AnimatedGIF)
+{
+ CheckMetadata(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoderMetadata, AnimatedPNG)
+{
+ CheckMetadata(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoderMetadata, FirstFramePaddingGIF)
+{
+ CheckMetadata(FirstFramePaddingGIFTestCase());
+}
+
+TEST_F(ImageDecoderMetadata, TransparentIfWithinICOBMPNotWithinICO)
+{
+ CheckMetadata(TransparentIfWithinICOBMPTestCase(TEST_CASE_DEFAULT_FLAGS),
+ BMPWithinICO::NO);
+}
+
+TEST_F(ImageDecoderMetadata, TransparentIfWithinICOBMPWithinICO)
+{
+ CheckMetadata(TransparentIfWithinICOBMPTestCase(TEST_CASE_IS_TRANSPARENT),
+ BMPWithinICO::YES);
+}
+
+TEST_F(ImageDecoderMetadata, RLE4BMP) { CheckMetadata(RLE4BMPTestCase()); }
+TEST_F(ImageDecoderMetadata, RLE8BMP) { CheckMetadata(RLE8BMPTestCase()); }
+
+TEST_F(ImageDecoderMetadata, Corrupt) { CheckMetadata(CorruptTestCase()); }
+
+TEST_F(ImageDecoderMetadata, NoFrameDelayGIF)
+{
+ CheckMetadata(NoFrameDelayGIFTestCase());
+}
+
+TEST_F(ImageDecoderMetadata, NoFrameDelayGIFFullDecode)
+{
+ ImageTestCase testCase = NoFrameDelayGIFTestCase();
+
+ // The previous test (NoFrameDelayGIF) verifies that we *don't* detect that
+ // this test case is animated, because it has a zero frame delay for the first
+ // frame. This test verifies that when we do a full decode, we detect the
+ // animation at that point and successfully decode all the frames.
+
+ // Create an image.
+ RefPtr<Image> image =
+ ImageFactory::CreateAnonymousImage(nsDependentCString(testCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into the image.
+ rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+ static_cast<uint32_t>(length));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Let the image know we've sent all the data.
+ rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
+ tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
+
+ // Use GetFrame() to force a sync decode of the image.
+ RefPtr<SourceSurface> surface =
+ image->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE);
+
+ // Ensure that the image's metadata meets our expectations.
+ IntSize imageSize(0, 0);
+ rv = image->GetWidth(&imageSize.width);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ rv = image->GetHeight(&imageSize.height);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ EXPECT_EQ(testCase.mSize.width, imageSize.width);
+ EXPECT_EQ(testCase.mSize.height, imageSize.height);
+
+ Progress imageProgress = tracker->GetProgress();
+
+ EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
+ EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
+
+ // Ensure that we decoded both frames of the image.
+ LookupResult result =
+ SurfaceCache::Lookup(ImageKey(image.get()),
+ RasterSurfaceKey(imageSize,
+ DefaultSurfaceFlags(),
+ PlaybackType::eAnimated));
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+ EXPECT_TRUE(bool(result.Surface()));
+}
diff --git a/image/test/gtest/TestRemoveFrameRectFilter.cpp b/image/test/gtest/TestRemoveFrameRectFilter.cpp
new file mode 100644
index 000000000..e1def590e
--- /dev/null
+++ b/image/test/gtest/TestRemoveFrameRectFilter.cpp
@@ -0,0 +1,327 @@
+/* -*- 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 });
+}
diff --git a/image/test/gtest/TestSourceBuffer.cpp b/image/test/gtest/TestSourceBuffer.cpp
new file mode 100644
index 000000000..05a88093f
--- /dev/null
+++ b/image/test/gtest/TestSourceBuffer.cpp
@@ -0,0 +1,810 @@
+/* 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 <algorithm>
+#include <cstdint>
+
+#include "mozilla/Move.h"
+#include "SourceBuffer.h"
+#include "SurfaceCache.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+using std::min;
+
+void
+ExpectChunkAndByteCount(const SourceBufferIterator& aIterator,
+ uint32_t aChunks,
+ size_t aBytes)
+{
+ EXPECT_EQ(aChunks, aIterator.ChunkCount());
+ EXPECT_EQ(aBytes, aIterator.ByteCount());
+}
+
+void
+ExpectRemainingBytes(const SourceBufferIterator& aIterator, size_t aBytes)
+{
+ EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes));
+ EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes + 1));
+
+ if (aBytes > 0) {
+ EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(0));
+ EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(aBytes - 1));
+ }
+}
+
+char
+GenerateByte(size_t aIndex)
+{
+ uint8_t byte = aIndex % 256;
+ return *reinterpret_cast<char*>(&byte);
+}
+
+void
+GenerateData(char* aOutput, size_t aOffset, size_t aLength)
+{
+ for (size_t i = 0; i < aLength; ++i) {
+ aOutput[i] = GenerateByte(aOffset + i);
+ }
+}
+
+void
+GenerateData(char* aOutput, size_t aLength)
+{
+ GenerateData(aOutput, 0, aLength);
+}
+
+void
+CheckData(const char* aData, size_t aOffset, size_t aLength)
+{
+ for (size_t i = 0; i < aLength; ++i) {
+ ASSERT_EQ(GenerateByte(aOffset + i), aData[i]);
+ }
+}
+
+enum class AdvanceMode
+{
+ eAdvanceAsMuchAsPossible,
+ eAdvanceByLengthExactly
+};
+
+class ImageSourceBuffer : public ::testing::Test
+{
+public:
+ ImageSourceBuffer()
+ : mSourceBuffer(new SourceBuffer)
+ , mExpectNoResume(new ExpectNoResume)
+ , mCountResumes(new CountResumes)
+ {
+ GenerateData(mData, sizeof(mData));
+ EXPECT_FALSE(mSourceBuffer->IsComplete());
+ }
+
+protected:
+ void CheckedAppendToBuffer(const char* aData, size_t aLength)
+ {
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->Append(aData, aLength)));
+ }
+
+ void CheckedAppendToBufferLastByteForLength(size_t aLength)
+ {
+ const char lastByte = GenerateByte(aLength);
+ CheckedAppendToBuffer(&lastByte, 1);
+ }
+
+ void CheckedAppendToBufferInChunks(size_t aChunkLength, size_t aTotalLength)
+ {
+ char* data = new char[aChunkLength];
+
+ size_t bytesWritten = 0;
+ while (bytesWritten < aTotalLength) {
+ GenerateData(data, bytesWritten, aChunkLength);
+ size_t toWrite = min(aChunkLength, aTotalLength - bytesWritten);
+ CheckedAppendToBuffer(data, toWrite);
+ bytesWritten += toWrite;
+ }
+
+ delete[] data;
+ }
+
+ void CheckedCompleteBuffer(nsresult aCompletionStatus = NS_OK)
+ {
+ mSourceBuffer->Complete(aCompletionStatus);
+ EXPECT_TRUE(mSourceBuffer->IsComplete());
+ }
+
+ void CheckedCompleteBuffer(SourceBufferIterator& aIterator,
+ size_t aLength,
+ nsresult aCompletionStatus = NS_OK)
+ {
+ CheckedCompleteBuffer(aCompletionStatus);
+ ExpectRemainingBytes(aIterator, aLength);
+ }
+
+ void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator,
+ size_t aLength,
+ uint32_t aChunks,
+ size_t aTotalLength,
+ AdvanceMode aAdvanceMode
+ = AdvanceMode::eAdvanceAsMuchAsPossible)
+ {
+ const size_t advanceBy = aAdvanceMode == AdvanceMode::eAdvanceAsMuchAsPossible
+ ? SIZE_MAX
+ : aLength;
+
+ auto state = aIterator.AdvanceOrScheduleResume(advanceBy, mExpectNoResume);
+ ASSERT_EQ(SourceBufferIterator::READY, state);
+ EXPECT_TRUE(aIterator.Data());
+ EXPECT_EQ(aLength, aIterator.Length());
+
+ ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength);
+ }
+
+ void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator,
+ size_t aLength)
+ {
+ CheckedAdvanceIteratorStateOnly(aIterator, aLength, 1, aLength);
+ }
+
+ void CheckedAdvanceIterator(SourceBufferIterator& aIterator,
+ size_t aLength,
+ uint32_t aChunks,
+ size_t aTotalLength,
+ AdvanceMode aAdvanceMode
+ = AdvanceMode::eAdvanceAsMuchAsPossible)
+ {
+ // Check that the iterator is in the expected state.
+ CheckedAdvanceIteratorStateOnly(aIterator, aLength, aChunks,
+ aTotalLength, aAdvanceMode);
+
+ // Check that we read the expected data. To do this, we need to compute our
+ // offset in the SourceBuffer, but fortunately that's pretty easy: it's the
+ // total number of bytes the iterator has advanced through, minus the length
+ // of the current chunk.
+ const size_t offset = aIterator.ByteCount() - aIterator.Length();
+ CheckData(aIterator.Data(), offset, aIterator.Length());
+ }
+
+ void CheckedAdvanceIterator(SourceBufferIterator& aIterator, size_t aLength)
+ {
+ CheckedAdvanceIterator(aIterator, aLength, 1, aLength);
+ }
+
+ void CheckIteratorMustWait(SourceBufferIterator& aIterator,
+ IResumable* aOnResume)
+ {
+ auto state = aIterator.AdvanceOrScheduleResume(1, aOnResume);
+ EXPECT_EQ(SourceBufferIterator::WAITING, state);
+ }
+
+ void CheckIteratorIsComplete(SourceBufferIterator& aIterator,
+ uint32_t aChunks,
+ size_t aTotalLength,
+ nsresult aCompletionStatus = NS_OK)
+ {
+ ASSERT_TRUE(mSourceBuffer->IsComplete());
+ auto state = aIterator.AdvanceOrScheduleResume(1, mExpectNoResume);
+ ASSERT_EQ(SourceBufferIterator::COMPLETE, state);
+ EXPECT_EQ(aCompletionStatus, aIterator.CompletionStatus());
+ ExpectRemainingBytes(aIterator, 0);
+ ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength);
+ }
+
+ void CheckIteratorIsComplete(SourceBufferIterator& aIterator,
+ size_t aTotalLength)
+ {
+ CheckIteratorIsComplete(aIterator, 1, aTotalLength);
+ }
+
+ AutoInitializeImageLib mInit;
+ char mData[9];
+ RefPtr<SourceBuffer> mSourceBuffer;
+ RefPtr<ExpectNoResume> mExpectNoResume;
+ RefPtr<CountResumes> mCountResumes;
+};
+
+TEST_F(ImageSourceBuffer, InitialState)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // RemainingBytesIsNoMoreThan() should always return false in the initial
+ // state, since we can't know the answer until Complete() has been called.
+ EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(0));
+ EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(SIZE_MAX));
+
+ // We haven't advanced our iterator at all, so its counters should be zero.
+ ExpectChunkAndByteCount(iterator, 0, 0);
+
+ // Attempt to advance; we should fail, and end up in the WAITING state. We
+ // expect no resumes because we don't actually append anything to the
+ // SourceBuffer in this test.
+ CheckIteratorMustWait(iterator, mExpectNoResume);
+}
+
+TEST_F(ImageSourceBuffer, ZeroLengthBufferAlwaysFails)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Complete the buffer without writing to it, providing a successful
+ // completion status.
+ CheckedCompleteBuffer(iterator, 0);
+
+ // Completing a buffer without writing to it results in an automatic failure;
+ // make sure that the actual completion status we get from the iterator
+ // reflects this.
+ CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE);
+}
+
+TEST_F(ImageSourceBuffer, CompleteSuccess)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write a single byte to the buffer and complete the buffer. (We have to
+ // write at least one byte because completing a zero length buffer always
+ // fails; see the ZeroLengthBufferAlwaysFails test.)
+ CheckedAppendToBuffer(mData, 1);
+ CheckedCompleteBuffer(iterator, 1);
+
+ // We should be able to advance once (to read the single byte) and then should
+ // reach the COMPLETE state with a successful status.
+ CheckedAdvanceIterator(iterator, 1);
+ CheckIteratorIsComplete(iterator, 1);
+}
+
+TEST_F(ImageSourceBuffer, CompleteFailure)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write a single byte to the buffer and complete the buffer. (We have to
+ // write at least one byte because completing a zero length buffer always
+ // fails; see the ZeroLengthBufferAlwaysFails test.)
+ CheckedAppendToBuffer(mData, 1);
+ CheckedCompleteBuffer(iterator, 1, NS_ERROR_FAILURE);
+
+ // Advance the iterator. Because a failing status is propagated to the
+ // iterator as soon as it advances, we won't be able to read the single byte
+ // that we wrote above; we go directly into the COMPLETE state.
+ CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE);
+}
+
+TEST_F(ImageSourceBuffer, Append)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write test data to the buffer.
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))));
+ CheckedAppendToBuffer(mData, sizeof(mData));
+ CheckedCompleteBuffer(iterator, sizeof(mData));
+
+ // Verify that we can read it back via the iterator, and that the final state
+ // is what we expect.
+ CheckedAdvanceIterator(iterator, sizeof(mData));
+ CheckIteratorIsComplete(iterator, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, HugeAppendFails)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // We should fail to append anything bigger than what the SurfaceCache can
+ // hold, so use the SurfaceCache's maximum capacity to calculate what a
+ // "massive amount of data" (see below) consists of on this platform.
+ ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX);
+ const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1;
+
+ // Attempt to write a massive amount of data and verify that it fails. (We'd
+ // get a buffer overrun during the test if it succeeds, but if it succeeds
+ // that's the least of our problems.)
+ EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, hugeSize)));
+ EXPECT_TRUE(mSourceBuffer->IsComplete());
+ CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY);
+}
+
+TEST_F(ImageSourceBuffer, AppendFromInputStream)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Construct an input stream with some arbitrary data. (We use test data from
+ // one of the decoder tests.)
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(GreenPNGTestCase().mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ ASSERT_TRUE(NS_SUCCEEDED(inputStream->Available(&length)));
+
+ // Write test data to the buffer.
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->AppendFromInputStream(inputStream,
+ length)));
+ CheckedCompleteBuffer(iterator, length);
+
+ // Verify that the iterator sees the appropriate amount of data.
+ CheckedAdvanceIteratorStateOnly(iterator, length);
+ CheckIteratorIsComplete(iterator, length);
+}
+
+TEST_F(ImageSourceBuffer, AppendAfterComplete)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write test data to the buffer.
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))));
+ CheckedAppendToBuffer(mData, sizeof(mData));
+ CheckedCompleteBuffer(iterator, sizeof(mData));
+
+ // Verify that we can read it back via the iterator, and that the final state
+ // is what we expect.
+ CheckedAdvanceIterator(iterator, sizeof(mData));
+ CheckIteratorIsComplete(iterator, sizeof(mData));
+
+ // Write more data to the completed buffer.
+ EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, sizeof(mData))));
+
+ // Try to read with a new iterator and verify that the new data got ignored.
+ SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
+ CheckedAdvanceIterator(iterator2, sizeof(mData));
+ CheckIteratorIsComplete(iterator2, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, MinChunkCapacity)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write test data to the buffer using many small appends. Since
+ // ExpectLength() isn't being called, we should be able to write up to
+ // SourceBuffer::MIN_CHUNK_CAPACITY bytes without a second chunk being
+ // allocated.
+ CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+ // Verify that the iterator sees the appropriate amount of data.
+ CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+ // Write one more byte; we expect to see that it triggers an allocation.
+ CheckedAppendToBufferLastByteForLength(SourceBuffer::MIN_CHUNK_CAPACITY);
+ CheckedCompleteBuffer(iterator, 1);
+
+ // Verify that the iterator sees the new byte and a new chunk has been
+ // allocated.
+ CheckedAdvanceIterator(iterator, 1, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1);
+ CheckIteratorIsComplete(iterator, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1);
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthDoesNotShrinkBelowMinCapacity)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the buffer,
+ // but call ExpectLength() first to make SourceBuffer expect only a single
+ // byte. We expect this to still result in only one chunk, because
+ // regardless of ExpectLength() we won't allocate a chunk smaller than
+ // MIN_CHUNK_CAPACITY bytes.
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(1)));
+ CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY);
+ CheckedCompleteBuffer(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+ // Verify that the iterator sees a single chunk.
+ CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+ CheckIteratorIsComplete(iterator, 1, SourceBuffer::MIN_CHUNK_CAPACITY);
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthGrowsAboveMinCapacity)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer, calling ExpectLength() with the correct length first. We expect
+ // this to result in only one chunk, because ExpectLength() allows us to
+ // allocate a larger first chunk than MIN_CHUNK_CAPACITY bytes.
+ const size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY;
+ EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(length)));
+ CheckedAppendToBufferInChunks(10, length);
+
+ // Verify that the iterator sees a single chunk.
+ CheckedAdvanceIterator(iterator, length);
+
+ // Write one more byte; we expect to see that it triggers an allocation.
+ CheckedAppendToBufferLastByteForLength(length);
+ CheckedCompleteBuffer(iterator, 1);
+
+ // Verify that the iterator sees the new byte and a new chunk has been
+ // allocated.
+ CheckedAdvanceIterator(iterator, 1, 2, length + 1);
+ CheckIteratorIsComplete(iterator, 2, length + 1);
+}
+
+TEST_F(ImageSourceBuffer, HugeExpectLengthFails)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // ExpectLength() should fail if the length is bigger than what the
+ // SurfaceCache can hold, so use the SurfaceCache's maximum capacity to
+ // calculate what a "massive amount of data" (see below) consists of on this
+ // platform.
+ ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX);
+ const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1;
+
+ // Attempt to write a massive amount of data and verify that it fails. (We'd
+ // get a buffer overrun during the test if it succeeds, but if it succeeds
+ // that's the least of our problems.)
+ EXPECT_TRUE(NS_FAILED(mSourceBuffer->ExpectLength(hugeSize)));
+ EXPECT_TRUE(mSourceBuffer->IsComplete());
+ CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY);
+}
+
+TEST_F(ImageSourceBuffer, LargeAppendsAllocateOnlyOneChunk)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer in a single Append() call. We expect this to result in only one
+ // chunk even though ExpectLength() wasn't called, because we should always
+ // allocate a new chunk large enough to store the data we have at hand.
+ constexpr size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY;
+ char data[length];
+ GenerateData(data, sizeof(data));
+ CheckedAppendToBuffer(data, length);
+
+ // Verify that the iterator sees a single chunk.
+ CheckedAdvanceIterator(iterator, length);
+
+ // Write one more byte; we expect to see that it triggers an allocation.
+ CheckedAppendToBufferLastByteForLength(length);
+ CheckedCompleteBuffer(iterator, 1);
+
+ // Verify that the iterator sees the new byte and a new chunk has been
+ // allocated.
+ CheckedAdvanceIterator(iterator, 1, 2, length + 1);
+ CheckIteratorIsComplete(iterator, 2, length + 1);
+}
+
+TEST_F(ImageSourceBuffer, LargeAppendsAllocateAtMostOneChunk)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Allocate some data we'll use below.
+ constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2;
+ constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = firstWriteLength + secondWriteLength;
+ char data[totalLength];
+ GenerateData(data, sizeof(data));
+
+ // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer in a single Append() call. This should fill half of the first chunk.
+ CheckedAppendToBuffer(data, firstWriteLength);
+
+ // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer in a single Append() call. We expect this to result in the first of
+ // the first chunk being filled and a new chunk being allocated for the
+ // remainder.
+ CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength);
+
+ // Verify that the iterator sees a MIN_CHUNK_CAPACITY-length chunk.
+ CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+ // Verify that the iterator sees a second chunk of the length we expect.
+ const size_t expectedSecondChunkLength =
+ totalLength - SourceBuffer::MIN_CHUNK_CAPACITY;
+ CheckedAdvanceIterator(iterator, expectedSecondChunkLength, 2, totalLength);
+
+ // Write one more byte; we expect to see that it triggers an allocation.
+ CheckedAppendToBufferLastByteForLength(totalLength);
+ CheckedCompleteBuffer(iterator, 1);
+
+ // Verify that the iterator sees the new byte and a new chunk has been
+ // allocated.
+ CheckedAdvanceIterator(iterator, 1, 3, totalLength + 1);
+ CheckIteratorIsComplete(iterator, 3, totalLength + 1);
+}
+
+TEST_F(ImageSourceBuffer, CompactionHappensWhenBufferIsComplete)
+{
+ constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = 2 * chunkLength;
+
+ // Write enough data to create two chunks.
+ CheckedAppendToBufferInChunks(chunkLength, totalLength);
+
+ {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Verify that the iterator sees two chunks.
+ CheckedAdvanceIterator(iterator, chunkLength);
+ CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength);
+ }
+
+ // Complete the buffer, which should trigger compaction implicitly.
+ CheckedCompleteBuffer();
+
+ {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Verify that compaction happened and there's now only one chunk.
+ CheckedAdvanceIterator(iterator, totalLength);
+ CheckIteratorIsComplete(iterator, 1, totalLength);
+ }
+}
+
+TEST_F(ImageSourceBuffer, CompactionIsDelayedWhileIteratorsExist)
+{
+ constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = 2 * chunkLength;
+
+ {
+ SourceBufferIterator outerIterator = mSourceBuffer->Iterator();
+
+ {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Write enough data to create two chunks.
+ CheckedAppendToBufferInChunks(chunkLength, totalLength);
+ CheckedCompleteBuffer(iterator, totalLength);
+
+ // Verify that the iterator sees two chunks. Since there are live
+ // iterators, compaction shouldn't have happened when we completed the
+ // buffer.
+ CheckedAdvanceIterator(iterator, chunkLength);
+ CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength);
+ CheckIteratorIsComplete(iterator, 2, totalLength);
+ }
+
+ // Now |iterator| has been destroyed, but |outerIterator| still exists, so
+ // we expect no compaction to have occurred at this point.
+ CheckedAdvanceIterator(outerIterator, chunkLength);
+ CheckedAdvanceIterator(outerIterator, chunkLength, 2, totalLength);
+ CheckIteratorIsComplete(outerIterator, 2, totalLength);
+ }
+
+ // Now all iterators have been destroyed. Since the buffer was already
+ // complete, we expect compaction to happen implicitly here.
+
+ {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Verify that compaction happened and there's now only one chunk.
+ CheckedAdvanceIterator(iterator, totalLength);
+ CheckIteratorIsComplete(iterator, 1, totalLength);
+ }
+}
+
+TEST_F(ImageSourceBuffer, SourceBufferIteratorsCanBeMoved)
+{
+ constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = 2 * chunkLength;
+
+ // Write enough data to create two chunks. We create an iterator here to make
+ // sure that compaction doesn't happen during the test.
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+ CheckedAppendToBufferInChunks(chunkLength, totalLength);
+ CheckedCompleteBuffer(iterator, totalLength);
+
+ auto GetIterator = [&]{
+ SourceBufferIterator lambdaIterator = mSourceBuffer->Iterator();
+ CheckedAdvanceIterator(lambdaIterator, chunkLength);
+ return lambdaIterator;
+ };
+
+ // Move-construct |movedIterator| from the iterator returned from
+ // GetIterator() and check that its state is as we expect.
+ SourceBufferIterator movedIterator = Move(GetIterator());
+ EXPECT_TRUE(movedIterator.Data());
+ EXPECT_EQ(chunkLength, movedIterator.Length());
+ ExpectChunkAndByteCount(movedIterator, 1, chunkLength);
+
+ // Make sure that we can advance the iterator.
+ CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength);
+
+ // Make sure that the iterator handles completion properly.
+ CheckIteratorIsComplete(movedIterator, 2, totalLength);
+
+ // Move-assign |movedIterator| from the iterator returned from
+ // GetIterator() and check that its state is as we expect.
+ movedIterator = Move(GetIterator());
+ EXPECT_TRUE(movedIterator.Data());
+ EXPECT_EQ(chunkLength, movedIterator.Length());
+ ExpectChunkAndByteCount(movedIterator, 1, chunkLength);
+
+ // Make sure that we can advance the iterator.
+ CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength);
+
+ // Make sure that the iterator handles completion properly.
+ CheckIteratorIsComplete(movedIterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkAdvance)
+{
+ constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = 2 * chunkLength;
+
+ // Write enough data to create two chunks. We create our iterator here to make
+ // sure that compaction doesn't happen during the test.
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+ CheckedAppendToBufferInChunks(chunkLength, totalLength);
+ CheckedCompleteBuffer(iterator, totalLength);
+
+ // Advance through the first chunk. The chunk count should not increase.
+ // We check that by always passing 1 for the |aChunks| parameter of
+ // CheckedAdvanceIteratorStateOnly(). We have to call CheckData() manually
+ // because the offset calculation in CheckedAdvanceIterator() assumes that
+ // we're advancing a chunk at a time.
+ size_t offset = 0;
+ while (offset < chunkLength) {
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+ }
+
+ // Read the first byte of the second chunk. This is the point at which we
+ // can't advance within the same chunk, so the chunk count should increase. We
+ // check that by passing 2 for the |aChunks| parameter of
+ // CheckedAdvanceIteratorStateOnly().
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+
+ // Read the rest of the second chunk. The chunk count should not increase.
+ while (offset < totalLength) {
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+ }
+
+ // Make sure we reached the end.
+ CheckIteratorIsComplete(iterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvance)
+{
+ constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = 2 * chunkLength;
+
+ // Write enough data to create two chunks. We create our iterator here to make
+ // sure that compaction doesn't happen during the test.
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+ CheckedAppendToBufferInChunks(chunkLength, totalLength);
+ CheckedCompleteBuffer(iterator, totalLength);
+
+ // Make an initial zero-length advance. Although a zero-length advance
+ // normally won't cause us to read a chunk from the SourceBuffer, we'll do so
+ // if the iterator is in the initial state to keep the invariant that
+ // SourceBufferIterator in the READY state always returns a non-null pointer
+ // from Data().
+ CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+
+ // Advance through the first chunk. As in the |SubchunkAdvance| test, the
+ // chunk count should not increase. We do a zero-length advance after each
+ // normal advance to ensure that zero-length advances do not change the
+ // iterator's position or cause a new chunk to be read.
+ size_t offset = 0;
+ while (offset < chunkLength) {
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+ CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ }
+
+ // Read the first byte of the second chunk. This is the point at which we
+ // can't advance within the same chunk, so the chunk count should increase. As
+ // before, we do a zero-length advance afterward.
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+ CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+
+ // Read the rest of the second chunk. The chunk count should not increase. As
+ // before, we do a zero-length advance after each normal advance.
+ while (offset < totalLength) {
+ CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ CheckData(iterator.Data(), offset++, iterator.Length());
+ CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength,
+ AdvanceMode::eAdvanceByLengthExactly);
+ }
+
+ // Make sure we reached the end.
+ CheckIteratorIsComplete(iterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvanceWithNoData)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that advancing by zero bytes still makes us enter the WAITING state.
+ // This is because if we entered the READY state before reading any data at
+ // all, we'd break the invariant that SourceBufferIterator::Data() always
+ // returns a non-null pointer in the READY state.
+ auto state = iterator.AdvanceOrScheduleResume(0, mCountResumes);
+ EXPECT_EQ(SourceBufferIterator::WAITING, state);
+
+ // Call Complete(). This should trigger a resume.
+ CheckedCompleteBuffer();
+ EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, NullIResumable)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that we can't advance.
+ CheckIteratorMustWait(iterator, nullptr);
+
+ // Append to the buffer, which would cause a resume if we had passed a
+ // non-null IResumable.
+ CheckedAppendToBuffer(mData, sizeof(mData));
+ CheckedCompleteBuffer(iterator, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, AppendTriggersResume)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that we can't advance.
+ CheckIteratorMustWait(iterator, mCountResumes);
+
+ // Call Append(). This should trigger a resume.
+ mSourceBuffer->Append(mData, sizeof(mData));
+ EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, OnlyOneResumeTriggeredPerAppend)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that we can't advance.
+ CheckIteratorMustWait(iterator, mCountResumes);
+
+ // Allocate some data we'll use below.
+ constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2;
+ constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY;
+ constexpr size_t totalLength = firstWriteLength + secondWriteLength;
+ char data[totalLength];
+ GenerateData(data, sizeof(data));
+
+ // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer in a single Append() call. This should fill half of the first chunk.
+ // This should trigger a resume.
+ CheckedAppendToBuffer(data, firstWriteLength);
+ EXPECT_EQ(1u, mCountResumes->Count());
+
+ // Advance past the new data and wait again.
+ CheckedAdvanceIterator(iterator, firstWriteLength);
+ CheckIteratorMustWait(iterator, mCountResumes);
+
+ // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+ // buffer in a single Append() call. We expect this to result in the first of
+ // the first chunk being filled and a new chunk being allocated for the
+ // remainder. Even though two chunks are getting written to here, only *one*
+ // resume should get triggered, for a total of two in this test.
+ CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength);
+ EXPECT_EQ(2u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, CompleteTriggersResume)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that we can't advance.
+ CheckIteratorMustWait(iterator, mCountResumes);
+
+ // Call Complete(). This should trigger a resume.
+ CheckedCompleteBuffer();
+ EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthDoesNotTriggerResume)
+{
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Check that we can't advance.
+ CheckIteratorMustWait(iterator, mExpectNoResume);
+
+ // Call ExpectLength(). If this triggers a resume, |mExpectNoResume| will
+ // ensure that the test fails.
+ mSourceBuffer->ExpectLength(1000);
+}
diff --git a/image/test/gtest/TestStreamingLexer.cpp b/image/test/gtest/TestStreamingLexer.cpp
new file mode 100644
index 000000000..590b10e81
--- /dev/null
+++ b/image/test/gtest/TestStreamingLexer.cpp
@@ -0,0 +1,973 @@
+/* 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/Vector.h"
+#include "StreamingLexer.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+enum class TestState
+{
+ ONE,
+ TWO,
+ THREE,
+ UNBUFFERED,
+ TRUNCATED_SUCCESS,
+ TRUNCATED_FAILURE
+};
+
+void
+CheckLexedData(const char* aData,
+ size_t aLength,
+ size_t aOffset,
+ size_t aExpectedLength)
+{
+ EXPECT_TRUE(aLength == aExpectedLength);
+
+ for (size_t i = 0; i < aLength; ++i) {
+ EXPECT_EQ(aData[i], char(aOffset + i + 1));
+ }
+}
+
+LexerTransition<TestState>
+DoLex(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::To(TestState::TWO, 3);
+ case TestState::TWO:
+ CheckLexedData(aData, aLength, 3, 3);
+ return Transition::To(TestState::THREE, 3);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 6, 3);
+ return Transition::TerminateSuccess();
+ case TestState::TRUNCATED_SUCCESS:
+ return Transition::TerminateSuccess();
+ case TestState::TRUNCATED_FAILURE:
+ return Transition::TerminateFailure();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithUnbuffered(TestState aState, const char* aData, size_t aLength,
+ Vector<char>& aUnbufferedVector)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
+ case TestState::TWO:
+ CheckLexedData(aUnbufferedVector.begin(), aUnbufferedVector.length(), 3, 3);
+ return Transition::To(TestState::THREE, 3);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 6, 3);
+ return Transition::TerminateSuccess();
+ case TestState::UNBUFFERED:
+ EXPECT_TRUE(aLength <= 3);
+ EXPECT_TRUE(aUnbufferedVector.append(aData, aLength));
+ return Transition::ContinueUnbuffered(TestState::UNBUFFERED);
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithUnbufferedTerminate(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
+ case TestState::UNBUFFERED:
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithYield(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::ToAfterYield(TestState::TWO);
+ case TestState::TWO:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::To(TestState::THREE, 6);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 3, 6);
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithTerminateAfterYield(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::ToAfterYield(TestState::TWO);
+ case TestState::TWO:
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthStates(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::To(TestState::TWO, 0);
+ case TestState::TWO:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::To(TestState::THREE, 9);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 0, 9);
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthStatesAtEnd(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 9);
+ return Transition::To(TestState::TWO, 0);
+ case TestState::TWO:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::To(TestState::THREE, 0);
+ case TestState::THREE:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthYield(TestState aState, const char* aData, size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ EXPECT_EQ(0u, aLength);
+ return Transition::ToAfterYield(TestState::TWO);
+ case TestState::TWO:
+ EXPECT_EQ(0u, aLength);
+ return Transition::To(TestState::THREE, 9);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 0, 9);
+ return Transition::TerminateSuccess();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthStatesUnbuffered(TestState aState,
+ const char* aData,
+ size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 0);
+ case TestState::TWO:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::To(TestState::THREE, 9);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 0, 9);
+ return Transition::TerminateSuccess();
+ case TestState::UNBUFFERED:
+ ADD_FAILURE() << "Should not enter zero-length unbuffered state";
+ return Transition::TerminateFailure();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthStatesAfterUnbuffered(TestState aState,
+ const char* aData,
+ size_t aLength)
+{
+ switch (aState) {
+ case TestState::ONE:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 9);
+ case TestState::TWO:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::To(TestState::THREE, 0);
+ case TestState::THREE:
+ EXPECT_TRUE(aLength == 0);
+ return Transition::TerminateSuccess();
+ case TestState::UNBUFFERED:
+ CheckLexedData(aData, aLength, 0, 9);
+ return Transition::ContinueUnbuffered(TestState::UNBUFFERED);
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+}
+
+class ImageStreamingLexer : public ::testing::Test
+{
+public:
+ // Note that mLexer is configured to enter TerminalState::FAILURE immediately
+ // if the input data is truncated. We don't expect that to happen in most
+ // tests, so we want to detect that issue. If a test needs a different
+ // behavior, we create a special StreamingLexer just for that test.
+ ImageStreamingLexer()
+ : mLexer(Transition::To(TestState::ONE, 3), Transition::TerminateFailure())
+ , mSourceBuffer(new SourceBuffer)
+ , mIterator(mSourceBuffer->Iterator())
+ , mExpectNoResume(new ExpectNoResume)
+ , mCountResumes(new CountResumes)
+ { }
+
+protected:
+ void CheckTruncatedState(StreamingLexer<TestState>& aLexer,
+ TerminalState aExpectedTerminalState,
+ nsresult aCompletionStatus = NS_OK)
+ {
+ for (unsigned i = 0; i < 9; ++i) {
+ if (i < 2) {
+ mSourceBuffer->Append(mData + i, 1);
+ } else if (i == 2) {
+ mSourceBuffer->Complete(aCompletionStatus);
+ }
+
+ LexerResult result = aLexer.Lex(mIterator, mCountResumes, DoLex);
+
+ if (i >= 2) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(aExpectedTerminalState, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(2u, mCountResumes->Count());
+ }
+
+ AutoInitializeImageLib mInit;
+ const char mData[9] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+ StreamingLexer<TestState> mLexer;
+ RefPtr<SourceBuffer> mSourceBuffer;
+ SourceBufferIterator mIterator;
+ RefPtr<ExpectNoResume> mExpectNoResume;
+ RefPtr<CountResumes> mCountResumes;
+};
+
+TEST_F(ImageStreamingLexer, ZeroLengthData)
+{
+ // Test a zero-length input.
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::FAILURE, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthDataUnbuffered)
+{
+ // Test a zero-length input.
+ mSourceBuffer->Complete(NS_OK);
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be unbuffered.
+ StreamingLexer<TestState> lexer(Transition::ToUnbuffered(TestState::ONE,
+ TestState::UNBUFFERED,
+ sizeof(mData)),
+ Transition::TerminateFailure());
+
+ LexerResult result = lexer.Lex(mIterator, mExpectNoResume, DoLex);
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::FAILURE, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, StartWithTerminal)
+{
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be a terminal state. This doesn't really make sense, but we should
+ // handle it.
+ StreamingLexer<TestState> lexer(Transition::TerminateSuccess(),
+ Transition::TerminateFailure());
+ LexerResult result = lexer.Lex(mIterator, mExpectNoResume, DoLex);
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, SingleChunk)
+{
+ // Test delivering all the data at once.
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, SingleChunkWithUnbuffered)
+{
+ Vector<char> unbufferedVector;
+
+ // Test delivering all the data at once.
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result =
+ mLexer.Lex(mIterator, mExpectNoResume,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
+ });
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, SingleChunkWithYield)
+{
+ // Test delivering all the data at once.
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLexWithYield);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+
+ result = mLexer.Lex(mIterator, mExpectNoResume, DoLexWithYield);
+ ASSERT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ChunkPerState)
+{
+ // Test delivering in perfectly-sized chunks, one per state.
+ for (unsigned i = 0; i < 3; ++i) {
+ mSourceBuffer->Append(mData + 3 * i, 3);
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLex);
+
+ if (i == 2) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(2u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, ChunkPerStateWithUnbuffered)
+{
+ Vector<char> unbufferedVector;
+
+ // Test delivering in perfectly-sized chunks, one per state.
+ for (unsigned i = 0; i < 3; ++i) {
+ mSourceBuffer->Append(mData + 3 * i, 3);
+ LexerResult result =
+ mLexer.Lex(mIterator, mCountResumes,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
+ });
+
+ if (i == 2) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(2u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, ChunkPerStateWithYield)
+{
+ // Test delivering in perfectly-sized chunks, one per state.
+ mSourceBuffer->Append(mData, 3);
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield);
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+
+ result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield);
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+
+ mSourceBuffer->Append(mData + 3, 6);
+ result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield);
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+
+ EXPECT_EQ(1u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, ChunkPerStateWithUnbufferedYield)
+{
+ size_t unbufferedCallCount = 0;
+ Vector<char> unbufferedVector;
+ auto lexerFunc = [&](TestState aState, const char* aData, size_t aLength)
+ -> LexerTransition<TestState> {
+ switch (aState) {
+ case TestState::ONE:
+ CheckLexedData(aData, aLength, 0, 3);
+ return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
+ case TestState::TWO:
+ CheckLexedData(unbufferedVector.begin(), unbufferedVector.length(), 3, 3);
+ return Transition::To(TestState::THREE, 3);
+ case TestState::THREE:
+ CheckLexedData(aData, aLength, 6, 3);
+ return Transition::TerminateSuccess();
+ case TestState::UNBUFFERED:
+ switch (unbufferedCallCount) {
+ case 0:
+ CheckLexedData(aData, aLength, 3, 3);
+ EXPECT_TRUE(unbufferedVector.append(aData, 2));
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 2 bytes.
+ return Transition::ContinueUnbufferedAfterYield(TestState::UNBUFFERED, 2);
+
+ case 1:
+ CheckLexedData(aData, aLength, 5, 1);
+ EXPECT_TRUE(unbufferedVector.append(aData, 1));
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 1 byte.
+ // We should end up in the TWO state.
+ return Transition::ContinueUnbuffered(TestState::UNBUFFERED);
+ }
+ ADD_FAILURE() << "Too many invocations of TestState::UNBUFFERED";
+ return Transition::TerminateFailure();
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+ };
+
+ // Test delivering in perfectly-sized chunks, one per state.
+ for (unsigned i = 0; i < 3; ++i) {
+ mSourceBuffer->Append(mData + 3 * i, 3);
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, lexerFunc);
+
+ switch (i) {
+ case 0:
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ EXPECT_EQ(0u, unbufferedCallCount);
+ break;
+
+ case 1:
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ EXPECT_EQ(1u, unbufferedCallCount);
+
+ result = mLexer.Lex(mIterator, mCountResumes, lexerFunc);
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ EXPECT_EQ(2u, unbufferedCallCount);
+ break;
+
+ case 2:
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ break;
+ }
+ }
+
+ EXPECT_EQ(2u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, lexerFunc);
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, OneByteChunks)
+{
+ // Test delivering in one byte chunks.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLex);
+
+ if (i == 8) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(8u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, OneByteChunksWithUnbuffered)
+{
+ Vector<char> unbufferedVector;
+
+ // Test delivering in one byte chunks.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result =
+ mLexer.Lex(mIterator, mCountResumes,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
+ });
+
+ if (i == 8) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(8u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, OneByteChunksWithYield)
+{
+ // Test delivering in one byte chunks.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield);
+
+ switch (i) {
+ case 2:
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+
+ result = mLexer.Lex(mIterator, mCountResumes, DoLexWithYield);
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ break;
+
+ case 8:
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ break;
+
+ default:
+ EXPECT_TRUE(i < 9);
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ EXPECT_EQ(8u, mCountResumes->Count());
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthState)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be zero length.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 0),
+ Transition::TerminateFailure());
+
+ LexerResult result =
+ lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStates);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthStatesAtEnd)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to consume the full input.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 9),
+ Transition::TerminateFailure());
+
+ LexerResult result =
+ lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesAtEnd);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthStateWithYield)
+{
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be zero length.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 0),
+ Transition::TerminateFailure());
+
+ mSourceBuffer->Append(mData, 3);
+ LexerResult result =
+ lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthYield);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+
+ result = lexer.Lex(mIterator, mCountResumes, DoLexWithZeroLengthYield);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+
+ mSourceBuffer->Append(mData + 3, sizeof(mData) - 3);
+ mSourceBuffer->Complete(NS_OK);
+ result = lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthYield);
+ ASSERT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbuffered)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be both zero length and unbuffered.
+ StreamingLexer<TestState> lexer(Transition::ToUnbuffered(TestState::ONE,
+ TestState::UNBUFFERED,
+ 0),
+ Transition::TerminateFailure());
+
+ LexerResult result =
+ lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesUnbuffered);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthStateAfterUnbuffered)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be zero length.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 0),
+ Transition::TerminateFailure());
+
+ LexerResult result =
+ lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesAfterUnbuffered);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbufferedYield)
+{
+ size_t unbufferedCallCount = 0;
+ auto lexerFunc = [&](TestState aState, const char* aData, size_t aLength)
+ -> LexerTransition<TestState> {
+ switch (aState) {
+ case TestState::ONE:
+ EXPECT_EQ(0u, aLength);
+ return Transition::TerminateSuccess();
+
+ case TestState::UNBUFFERED:
+ switch (unbufferedCallCount) {
+ case 0:
+ CheckLexedData(aData, aLength, 0, 3);
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 0 bytes.
+ return Transition::ContinueUnbufferedAfterYield(TestState::UNBUFFERED, 0);
+
+ case 1:
+ CheckLexedData(aData, aLength, 0, 3);
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 2 bytes.
+ return Transition::ContinueUnbufferedAfterYield(TestState::UNBUFFERED, 2);
+
+ case 2:
+ EXPECT_EQ(1u, aLength);
+ CheckLexedData(aData, aLength, 2, 1);
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 1 bytes.
+ return Transition::ContinueUnbufferedAfterYield(TestState::UNBUFFERED, 1);
+
+ case 3:
+ CheckLexedData(aData, aLength, 3, 6);
+ unbufferedCallCount++;
+
+ // Continue after yield, telling StreamingLexer we consumed 6 bytes.
+ // We should transition to TestState::ONE when we return from the
+ // yield.
+ return Transition::ContinueUnbufferedAfterYield(TestState::UNBUFFERED, 6);
+ }
+
+ ADD_FAILURE() << "Too many invocations of TestState::UNBUFFERED";
+ return Transition::TerminateFailure();
+
+ default:
+ MOZ_CRASH("Unexpected or unhandled TestState");
+ }
+ };
+
+ // Create a special StreamingLexer for this test because we want the first
+ // state to be unbuffered.
+ StreamingLexer<TestState> lexer(Transition::ToUnbuffered(TestState::ONE,
+ TestState::UNBUFFERED,
+ sizeof(mData)),
+ Transition::TerminateFailure());
+
+ mSourceBuffer->Append(mData, 3);
+ LexerResult result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ EXPECT_EQ(1u, unbufferedCallCount);
+
+ result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ EXPECT_EQ(2u, unbufferedCallCount);
+
+ result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ EXPECT_EQ(3u, unbufferedCallCount);
+
+ result = lexer.Lex(mIterator, mCountResumes, lexerFunc);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ EXPECT_EQ(3u, unbufferedCallCount);
+
+ mSourceBuffer->Append(mData + 3, 6);
+ mSourceBuffer->Complete(NS_OK);
+ EXPECT_EQ(1u, mCountResumes->Count());
+ result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc);
+ ASSERT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ EXPECT_EQ(4u, unbufferedCallCount);
+
+ result = lexer.Lex(mIterator, mExpectNoResume, lexerFunc);
+ ASSERT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, TerminateSuccess)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Test that Terminate is "sticky".
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+ LexerResult result =
+ mLexer.Lex(iterator, mExpectNoResume,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ EXPECT_TRUE(aState == TestState::ONE);
+ return Transition::TerminateSuccess();
+ });
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+
+ SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
+ result =
+ mLexer.Lex(iterator2, mExpectNoResume,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ EXPECT_TRUE(false); // Shouldn't get here.
+ return Transition::TerminateFailure();
+ });
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, TerminateFailure)
+{
+ mSourceBuffer->Append(mData, sizeof(mData));
+ mSourceBuffer->Complete(NS_OK);
+
+ // Test that Terminate is "sticky".
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+ LexerResult result =
+ mLexer.Lex(iterator, mExpectNoResume,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ EXPECT_TRUE(aState == TestState::ONE);
+ return Transition::TerminateFailure();
+ });
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::FAILURE, result.as<TerminalState>());
+
+ SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
+ result =
+ mLexer.Lex(iterator2, mExpectNoResume,
+ [&](TestState aState, const char* aData, size_t aLength) {
+ EXPECT_TRUE(false); // Shouldn't get here.
+ return Transition::TerminateFailure();
+ });
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::FAILURE, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, TerminateUnbuffered)
+{
+ // Test that Terminate works during an unbuffered read.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result =
+ mLexer.Lex(mIterator, mCountResumes, DoLexWithUnbufferedTerminate);
+
+ if (i > 2) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ // We expect 3 resumes because TestState::ONE consumes 3 bytes and then
+ // transitions to TestState::UNBUFFERED, which calls TerminateSuccess() as
+ // soon as it receives a single byte. That's four bytes total, which are
+ // delivered one at a time, requiring 3 resumes.
+ EXPECT_EQ(3u, mCountResumes->Count());
+
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, TerminateAfterYield)
+{
+ // Test that Terminate works after yielding.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result =
+ mLexer.Lex(mIterator, mCountResumes, DoLexWithTerminateAfterYield);
+
+ if (i > 2) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else if (i == 2) {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::OUTPUT_AVAILABLE, result.as<Yield>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ // We expect 2 resumes because TestState::ONE consumes 3 bytes and then
+ // yields. When the lexer resumes at TestState::TWO, which receives the same 3
+ // bytes, TerminateSuccess() gets called immediately. That's three bytes
+ // total, which are delivered one at a time, requiring 2 resumes.
+ EXPECT_EQ(2u, mCountResumes->Count());
+
+ mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferImmediateComplete)
+{
+ // Test calling SourceBuffer::Complete() without appending any data. This
+ // causes the SourceBuffer to automatically have a failing completion status,
+ // no matter what you pass, so we expect TerminalState::FAILURE below.
+ mSourceBuffer->Complete(NS_OK);
+
+ LexerResult result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::FAILURE, result.as<TerminalState>());
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedTerminalStateSuccess)
+{
+ // Test that using a terminal state (in this case TerminalState::SUCCESS) as a
+ // truncated state works.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3),
+ Transition::TerminateSuccess());
+
+ CheckTruncatedState(lexer, TerminalState::SUCCESS);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedTerminalStateFailure)
+{
+ // Test that using a terminal state (in this case TerminalState::FAILURE) as a
+ // truncated state works.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3),
+ Transition::TerminateFailure());
+
+ CheckTruncatedState(lexer, TerminalState::FAILURE);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedStateReturningSuccess)
+{
+ // Test that a truncated state that returns TerminalState::SUCCESS works. When
+ // |lexer| discovers that the data is truncated, it invokes the
+ // TRUNCATED_SUCCESS state, which returns TerminalState::SUCCESS.
+ // CheckTruncatedState() verifies that this happens.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3),
+ Transition::To(TestState::TRUNCATED_SUCCESS, 0));
+
+ CheckTruncatedState(lexer, TerminalState::SUCCESS);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedStateReturningFailure)
+{
+ // Test that a truncated state that returns TerminalState::FAILURE works. When
+ // |lexer| discovers that the data is truncated, it invokes the
+ // TRUNCATED_FAILURE state, which returns TerminalState::FAILURE.
+ // CheckTruncatedState() verifies that this happens.
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3),
+ Transition::To(TestState::TRUNCATED_FAILURE, 0));
+
+ CheckTruncatedState(lexer, TerminalState::FAILURE);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedFailingCompleteStatus)
+{
+ // Test that calling SourceBuffer::Complete() with a failing status results in
+ // an immediate TerminalState::FAILURE result. (Note that |lexer|'s truncated
+ // state is TerminalState::SUCCESS, so if we ignore the failing status, the
+ // test will fail.)
+ StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3),
+ Transition::TerminateSuccess());
+
+ CheckTruncatedState(lexer, TerminalState::FAILURE, NS_ERROR_FAILURE);
+}
+
+TEST_F(ImageStreamingLexer, NoSourceBufferResumable)
+{
+ // Test delivering in one byte chunks with no IResumable.
+ for (unsigned i = 0; i < 9; ++i) {
+ mSourceBuffer->Append(mData + i, 1);
+ LexerResult result = mLexer.Lex(mIterator, nullptr, DoLex);
+
+ if (i == 8) {
+ EXPECT_TRUE(result.is<TerminalState>());
+ EXPECT_EQ(TerminalState::SUCCESS, result.as<TerminalState>());
+ } else {
+ EXPECT_TRUE(result.is<Yield>());
+ EXPECT_EQ(Yield::NEED_MORE_DATA, result.as<Yield>());
+ }
+ }
+
+ mSourceBuffer->Complete(NS_OK);
+}
diff --git a/image/test/gtest/TestSurfacePipeIntegration.cpp b/image/test/gtest/TestSurfacePipeIntegration.cpp
new file mode 100644
index 000000000..5e8c19fc2
--- /dev/null
+++ b/image/test/gtest/TestSurfacePipeIntegration.cpp
@@ -0,0 +1,508 @@
+/* -*- 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
+
+void
+CheckSurfacePipeMethodResults(SurfacePipe* aPipe,
+ Decoder* aDecoder,
+ const IntRect& aRect = IntRect(0, 0, 100, 100))
+{
+ // Check that the pipeline ended up in the state we expect. Note that we're
+ // explicitly testing the SurfacePipe versions of these methods, so we don't
+ // want to use AssertCorrectPipelineFinalState() here.
+ EXPECT_TRUE(aPipe->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect);
+
+ // Check the generated image.
+ CheckGeneratedImage(aDecoder, aRect);
+
+ // Reset and clear the image before the next test.
+ aPipe->ResetToFirstRow();
+ EXPECT_FALSE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ uint32_t count = 0;
+ auto result = aPipe->WritePixels<uint32_t>([&]() {
+ ++count;
+ return AsVariant(BGRAColor::Transparent().AsPixel());
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u * 100u, count);
+
+ EXPECT_TRUE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect);
+
+ aPipe->ResetToFirstRow();
+ EXPECT_FALSE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+}
+
+void
+CheckPalettedSurfacePipeMethodResults(SurfacePipe* aPipe,
+ Decoder* aDecoder,
+ const IntRect& aRect
+ = IntRect(0, 0, 100, 100))
+{
+ // Check that the pipeline ended up in the state we expect. Note that we're
+ // explicitly testing the SurfacePipe versions of these methods, so we don't
+ // want to use AssertCorrectPipelineFinalState() here.
+ EXPECT_TRUE(aPipe->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect);
+
+ // Check the generated image.
+ CheckGeneratedPalettedImage(aDecoder, aRect);
+
+ // Reset and clear the image before the next test.
+ aPipe->ResetToFirstRow();
+ EXPECT_FALSE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ uint32_t count = 0;
+ auto result = aPipe->WritePixels<uint8_t>([&]() {
+ ++count;
+ return AsVariant(uint8_t(0));
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u * 100u, count);
+
+ EXPECT_TRUE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, 0, 100, 100), invalidRect->mOutputSpaceRect);
+
+ aPipe->ResetToFirstRow();
+ EXPECT_FALSE(aPipe->IsSurfaceFinished());
+ invalidRect = aPipe->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+}
+
+class ImageSurfacePipeIntegration : public ::testing::Test
+{
+protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(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, 0, IntSize(100, 100),
+ SurfaceFormat::B8G8R8A8, false });
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ pipe = TestSurfacePipeFactory::SurfacePipeFromPipeline(sink);
+
+ // Test that WritePixels() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ auto result = pipe.WritePixels<uint32_t>([&]() {
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u * 100u, count);
+ CheckSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Create a buffer the same size as one row of the surface, containing all
+ // green pixels. We'll use this for the WriteBuffer() tests.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = BGRAColor::Green().AsPixel();
+ }
+
+ // Test that WriteBuffer() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteBuffer(buffer);
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Test that the 3 argument version of WriteBuffer() gets passed through to
+ // the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteBuffer(buffer, 0, 100);
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Test that WriteEmptyRow() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteEmptyRow();
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckSurfacePipeMethodResults(&pipe, decoder, IntRect(0, 0, 0, 0));
+ }
+
+ // Mark the frame as finished so we don't get an assertion.
+ RawAccessFrameRef currentFrame = decoder->GetCurrentFrameRef();
+ currentFrame->Finish();
+}
+
+TEST_F(ImageSurfacePipeIntegration, PalettedSurfacePipe)
+{
+ // Create a SurfacePipe containing a PalettedSurfaceSink.
+ RefPtr<Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ auto sink = MakeUnique<PalettedSurfaceSink>();
+ nsresult rv =
+ sink->Configure(PalettedSurfaceConfig { decoder, 0, IntSize(100, 100),
+ IntRect(0, 0, 100, 100),
+ SurfaceFormat::B8G8R8A8,
+ 8, false });
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ SurfacePipe pipe = TestSurfacePipeFactory::SurfacePipeFromPipeline(sink);
+
+ // Test that WritePixels() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ auto result = pipe.WritePixels<uint8_t>([&]() {
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u * 100u, count);
+ CheckPalettedSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Create a buffer the same size as one row of the surface, containing all
+ // 255 pixels. We'll use this for the WriteBuffer() tests.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 255;
+ }
+
+ // Test that WriteBuffer() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteBuffer(buffer);
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckPalettedSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Test that the 3 argument version of WriteBuffer() gets passed through to
+ // the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteBuffer(buffer, 0, 100);
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckPalettedSurfacePipeMethodResults(&pipe, decoder);
+ }
+
+ // Test that WriteEmptyRow() gets passed through to the underlying pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = pipe.WriteEmptyRow();
+ ++count;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+ CheckPalettedSurfacePipeMethodResults(&pipe, decoder, IntRect(0, 0, 0, 0));
+ }
+
+ // Mark the frame as finished so we don't get an assertion.
+ RawAccessFrameRef currentFrame = decoder->GetCurrentFrameRef();
+ currentFrame->Finish();
+}
+
+TEST_F(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_F(ImageSurfacePipeIntegration, RemoveFrameRectBottomRightDownscaleWritePixels)
+{
+ // This test case uses a frame rect that extends beyond the borders of the
+ // image to the bottom and to the right. It looks roughly like this (with the
+ // box made of '#'s representing the frame rect):
+ //
+ // +------------+
+ // + +
+ // + +------------+
+ // + +############+
+ // +------+############+
+ // +############+
+ // +------------+
+
+ 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_F(ImageSurfacePipeIntegration, RemoveFrameRectTopLeftDownscaleWritePixels)
+{
+ // This test case uses a frame rect that extends beyond the borders of the
+ // image to the top and to the left. It looks roughly like this (with the
+ // box made of '#'s representing the frame rect):
+ //
+ // +------------+
+ // +############+
+ // +############+------+
+ // +############+ +
+ // +------------+ +
+ // + +
+ // +------------+
+
+ 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(0, 0, 100, 100)),
+ /* aOutputWriteRect = */ Some(IntRect(0, 0, 10, 10)),
+ /* aFuzz = */ 0x21);
+ };
+
+ 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_F(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_F(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_F(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_F(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 });
+}
+
+TEST_F(ImageSurfacePipeIntegration, ConfiguringHugeDeinterlacingBufferFails)
+{
+ RefPtr<Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ // When DownscalingFilter is used, we may succeed in allocating an output
+ // surface for huge images, because we only need to store the scaled-down
+ // version of the image. However, regardless of downscaling,
+ // DeinterlacingFilter needs to allocate a buffer as large as the size of the
+ // input. This can cause OOMs on operating systems that allow overcommit. This
+ // test makes sure that we reject such allocations.
+ AssertConfiguringPipelineFails(decoder,
+ DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true},
+ DownscalingConfig { IntSize(60000, 60000),
+ SurfaceFormat::B8G8R8A8 },
+ SurfaceConfig { decoder, 0, IntSize(600, 600),
+ SurfaceFormat::B8G8R8A8, false });
+}
diff --git a/image/test/gtest/TestSurfaceSink.cpp b/image/test/gtest/TestSurfaceSink.cpp
new file mode 100644
index 000000000..ccf9be3ec
--- /dev/null
+++ b/image/test/gtest/TestSurfaceSink.cpp
@@ -0,0 +1,1491 @@
+/* -*- 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 });
+}
+
+void
+ResetForNextPass(SurfaceFilter* aSink)
+{
+ aSink->ResetToFirstRow();
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+}
+
+template <typename WriteFunc, typename CheckFunc> void
+DoCheckIterativeWrite(SurfaceFilter* aSink,
+ WriteFunc aWriteFunc,
+ CheckFunc aCheckFunc)
+{
+ // Write the buffer to successive rows until every row of the surface
+ // has been written.
+ uint32_t row = 0;
+ WriteState result = WriteState::NEED_MORE_DATA;
+ while (result == WriteState::NEED_MORE_DATA) {
+ result = aWriteFunc(row);
+ ++row;
+ }
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, row);
+
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Check that the generated image is correct.
+ aCheckFunc();
+}
+
+template <typename WriteFunc> void
+CheckIterativeWrite(Decoder* aDecoder,
+ SurfaceSink* aSink,
+ const IntRect& aOutputRect,
+ WriteFunc aWriteFunc)
+{
+ // Ignore the row passed to WriteFunc, since no callers use it.
+ auto writeFunc = [&](uint32_t) {
+ return aWriteFunc();
+ };
+
+ DoCheckIterativeWrite(aSink, writeFunc, [&]{
+ CheckGeneratedImage(aDecoder, aOutputRect);
+ });
+}
+
+template <typename WriteFunc> void
+CheckPalettedIterativeWrite(Decoder* aDecoder,
+ PalettedSurfaceSink* aSink,
+ const IntRect& aOutputRect,
+ WriteFunc aWriteFunc)
+{
+ // Ignore the row passed to WriteFunc, since no callers use it.
+ auto writeFunc = [&](uint32_t) {
+ return aWriteFunc();
+ };
+
+ DoCheckIterativeWrite(aSink, writeFunc, [&]{
+ CheckGeneratedPalettedImage(aDecoder, aOutputRect);
+ });
+}
+
+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());
+
+ uint32_t source = BGRAColor::Red().AsPixel();
+ result = sink.WriteBuffer(&source);
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_TRUE(sink.IsSurfaceFinished());
+ invalidRect = sink.TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ result = sink.WriteBuffer(&source, 0, 1);
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_TRUE(sink.IsSurfaceFinished());
+ invalidRect = sink.TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ result = sink.WriteEmptyRow();
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_TRUE(sink.IsSurfaceFinished());
+ invalidRect = sink.TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ result = sink.WriteUnsafeComputedRow<uint32_t>([&](uint32_t* aRow,
+ uint32_t aLength) {
+ gotCalled = true;
+ for (uint32_t col = 0; col < aLength; ++col, ++aRow) {
+ *aRow = BGRAColor::Red().AsPixel();
+ }
+ });
+ 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, SurfaceSinkInitialization)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Check initial state.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ // Check that the surface is zero-initialized. We verify this by calling
+ // CheckGeneratedImage() and telling it that we didn't write to the surface
+ // anyway (i.e., we wrote to the empty rect); it will then expect the entire
+ // surface to be transparent, which is what it should be if it was
+ // zero-initialied.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 0, 0));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixels)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ CheckWritePixels(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);
+
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // 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->GetSourceSurface();
+ EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Transparent()));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsEarlyExit)
+{
+ auto checkEarlyExit =
+ [](Decoder* aDecoder, SurfaceSink* aSink, WriteState aState) {
+ // Write half a row of green pixels and then exit early with |aState|. If
+ // the lambda keeps getting called, we'll write red pixels, which will cause
+ // the test to fail.
+ uint32_t count = 0;
+ auto result = aSink->WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> {
+ if (count == 50) {
+ return AsVariant(aState);
+ }
+ return count++ < 50 ? AsVariant(BGRAColor::Green().AsPixel())
+ : AsVariant(BGRAColor::Red().AsPixel());
+ });
+
+ EXPECT_EQ(aState, result);
+ EXPECT_EQ(50u, count);
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1));
+
+ if (aState != WriteState::FINISHED) {
+ // We should still be able to write more at this point.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ // Verify that we can resume writing. We'll finish up the same row.
+ count = 0;
+ 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());
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 1));
+
+ return;
+ }
+
+ // We should've finished the surface at this point.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // 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 still correct.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1));
+ };
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsToRow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Write the first 99 rows of our 100x100 surface and verify that even
+ // though our lambda will yield pixels forever, only one row is written per
+ // call to WritePixelsToRow().
+ for (int row = 0; row < 99; ++row) {
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint32_t>([&]{
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(100u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, row, 100, 1), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, row, 100, 1), invalidRect->mOutputSpaceRect);
+
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, row + 1));
+ }
+
+ // Write the final line, which should finish the surface.
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint32_t>([&]{
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+
+ // Note that the final invalid rect we expect here is only the last row;
+ // that's because we called TakeInvalidRect() repeatedly in the loop above.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 99, 100, 1),
+ IntRect(0, 99, 100, 1));
+
+ // Check that the generated image is correct.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelsToRow<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 still correct.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 100));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsToRowEarlyExit)
+{
+ auto checkEarlyExit =
+ [](Decoder* aDecoder, SurfaceSink* aSink, WriteState aState) {
+ // Write half a row of green pixels and then exit early with |aState|. If
+ // the lambda keeps getting called, we'll write red pixels, which will cause
+ // the test to fail.
+ uint32_t count = 0;
+ auto result = aSink->WritePixelsToRow<uint32_t>([&]() -> NextPixel<uint32_t> {
+ if (count == 50) {
+ return AsVariant(aState);
+ }
+ return count++ < 50 ? AsVariant(BGRAColor::Green().AsPixel())
+ : AsVariant(BGRAColor::Red().AsPixel());
+ });
+
+ EXPECT_EQ(aState, result);
+ EXPECT_EQ(50u, count);
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1));
+
+ if (aState != WriteState::FINISHED) {
+ // We should still be able to write more at this point.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ // Verify that we can resume the same row and still stop at the end.
+ count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint32_t>([&]{
+ ++count;
+ return AsVariant(BGRAColor::Green().AsPixel());
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(50u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 100, 1));
+
+ return;
+ }
+
+ // We should've finished the surface at this point.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelsToRow<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 still correct.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 50, 1));
+ };
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>([&](Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBuffer)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Create a green buffer the same size as one row of the surface (which is 100x100),
+ // containing 60 pixels of green in the middle and 20 transparent pixels on
+ // either side.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 20 <= i && i < 80 ? BGRAColor::Green().AsPixel()
+ : BGRAColor::Transparent().AsPixel();
+ }
+
+ // Write the buffer to every row of the surface and check that the generated
+ // image is correct.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), [&]{
+ return aSink->WriteBuffer(buffer);
+ });
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Create a buffer the same size as one row of the surface, containing all
+ // green pixels.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = BGRAColor::Green().AsPixel();
+ }
+
+ // Write the buffer to the middle 60 pixels of every row of the surface and
+ // check that the generated image is correct.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), [&]{
+ return aSink->WriteBuffer(buffer, 20, 60);
+ });
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRowStartColOverflow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Create a buffer the same size as one row of the surface, containing all
+ // green pixels.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = BGRAColor::Green().AsPixel();
+ }
+
+ {
+ // Write the buffer to successive rows until every row of the surface
+ // has been written. We place the start column beyond the end of the row,
+ // which will prevent us from writing anything, so we check that the
+ // generated image is entirely transparent.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteBuffer(buffer, 100, 100);
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write the buffer to successive rows until every row of the surface
+ // has been written. We use column 50 as the start column, but we still
+ // write the buffer, which means we overflow the right edge of the surface
+ // by 50 pixels. We check that the left half of the generated image is
+ // transparent and the right half is green.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteBuffer(buffer, 50, 100);
+ });
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBufferPartialRowBufferOverflow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Create a buffer twice as large as a row of the surface. The first half
+ // (which is as large as a row of the image) will contain green pixels,
+ // while the second half will contain red pixels.
+ uint32_t buffer[200];
+ for (int i = 0; i < 200; ++i) {
+ buffer[i] = i < 100 ? BGRAColor::Green().AsPixel()
+ : BGRAColor::Red().AsPixel();
+ }
+
+ {
+ // Write the buffer to successive rows until every row of the surface has
+ // been written. The buffer extends 100 pixels to the right of a row of
+ // the surface, but bounds checking will prevent us from overflowing the
+ // buffer. We check that the generated image is entirely green since the
+ // pixels on the right side of the buffer shouldn't have been written to
+ // the surface.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 100, 100), [&]{
+ return aSink->WriteBuffer(buffer, 0, 200);
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write from the buffer to the middle of each row of the surface. That
+ // means that the left side of each row should be transparent, since we
+ // didn't write anything there. A buffer overflow would cause us to write
+ // buffer contents into the left side of each row. We check that the
+ // generated image is transparent on the left side and green on the right.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteBuffer(buffer, 50, 200);
+ });
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBufferFromNullSource)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Calling WriteBuffer() with a null pointer should fail without making any
+ // changes to the surface.
+ uint32_t* nullBuffer = nullptr;
+ WriteState result = aSink->WriteBuffer(nullBuffer);
+
+ EXPECT_EQ(WriteState::FAILURE, result);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ // Check that nothing got written to the surface.
+ CheckGeneratedImage(aDecoder, IntRect(0, 0, 0, 0));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteEmptyRow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ {
+ // Write an empty row to each row of the surface. We check that the
+ // generated image is entirely transparent.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteEmptyRow();
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write a partial row before we begin calling WriteEmptyRow(). We check
+ // that the generated image is entirely transparent, which is to be
+ // expected since WriteEmptyRow() overwrites the current row even if some
+ // data has already been written to it.
+ 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());
+
+ CheckIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteEmptyRow();
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Create a buffer the same size as one row of the surface, containing all
+ // green pixels.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = BGRAColor::Green().AsPixel();
+ }
+
+ // Write an empty row to the middle 60 rows of the surface. The first 20
+ // and last 20 rows will be green. (We need to use DoCheckIterativeWrite()
+ // here because we need a custom function to check the output, since it
+ // can't be described by a simple rect.)
+ auto writeFunc = [&](uint32_t aRow) {
+ if (aRow < 20 || aRow >= 80) {
+ return aSink->WriteBuffer(buffer);
+ } else {
+ return aSink->WriteEmptyRow();
+ }
+ };
+
+ auto checkFunc = [&]{
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Green()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 20, 60, BGRAColor::Transparent()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Green()));
+ };
+
+ DoCheckIterativeWrite(aSink, writeFunc, checkFunc);
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteUnsafeComputedRow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](Decoder* aDecoder, SurfaceSink* aSink) {
+ // Create a green buffer the same size as one row of the surface.
+ uint32_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = BGRAColor::Green().AsPixel();
+ }
+
+ // Write the buffer to successive rows until every row of the surface
+ // has been written. We only write to the right half of each row, so we
+ // check that the left side of the generated image is transparent and the
+ // right side is green.
+ CheckIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteUnsafeComputedRow<uint32_t>([&](uint32_t* aRow,
+ uint32_t aLength) {
+ EXPECT_EQ(100u, aLength );
+ memcpy(aRow + 50, buffer, 50 * sizeof(uint32_t));
+ });
+ });
+ });
+}
+
+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->GetSourceSurface();
+ EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+ }
+
+ {
+ ResetForNextPass(aSink);
+
+ // Check that the generated image is still the first pass image.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+ 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->GetSourceSurface();
+ 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->GetSourceSurface();
+ 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->GetSourceSurface();
+ EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Red()));
+ }
+
+ {
+ ResetForNextPass(aSink);
+
+ // Check that the generated image is still the first pass image.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+ 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->GetSourceSurface();
+ 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->GetSourceSurface();
+ EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green()));
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkInitialization)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Check initial state.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ // Check that the paletted image data is zero-initialized.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ uint8_t* imageData = nullptr;
+ uint32_t imageLength = 0;
+ currentFrame->GetImageData(&imageData, &imageLength);
+ ASSERT_TRUE(imageData != nullptr);
+ ASSERT_EQ(100u * 100u, imageLength);
+ for (uint32_t i = 0; i < imageLength; ++i) {
+ ASSERT_EQ(uint8_t(0), imageData[i]);
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsFor0_0_100_100)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ CheckPalettedWritePixels(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, 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, 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, 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, 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, PalettedSurfaceSinkWritePixelsFinish)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Write nothing into the surface; just finish immediately.
+ uint32_t count = 0;
+ auto result = aSink->WritePixels<uint8_t>([&]{
+ count++;
+ return AsVariant(WriteState::FINISHED);
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(1u, count);
+
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixels<uint8_t>([&]() {
+ count++;
+ return AsVariant(uint8_t(128));
+ });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is correct.
+ EXPECT_TRUE(IsSolidPalettedColor(aDecoder, 0));
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsEarlyExit)
+{
+ auto checkEarlyExit =
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink, WriteState aState) {
+ // Write half a row of green pixels and then exit early with |aState|. If
+ // the lambda keeps getting called, we'll write red pixels, which will cause
+ // the test to fail.
+ uint32_t count = 0;
+ auto result = aSink->WritePixels<uint8_t>([&]() -> NextPixel<uint8_t> {
+ if (count == 50) {
+ return AsVariant(aState);
+ }
+ return count++ < 50 ? AsVariant(uint8_t(255)) : AsVariant(uint8_t(128));
+ });
+
+ EXPECT_EQ(aState, result);
+ EXPECT_EQ(50u, count);
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 50, 1));
+
+ if (aState != WriteState::FINISHED) {
+ // We should still be able to write more at this point.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ // Verify that we can resume writing. We'll finish up the same row.
+ count = 0;
+ result = aSink->WritePixels<uint8_t>([&]() -> NextPixel<uint8_t> {
+ if (count == 50) {
+ return AsVariant(WriteState::NEED_MORE_DATA);
+ }
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(50u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 100, 1));
+
+ return;
+ }
+
+ // We should've finished the surface at this point.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixels<uint8_t>([&]{
+ count++;
+ return AsVariant(uint8_t(128));
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is still correct.
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 50, 1));
+ };
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsToRow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Write the first 99 rows of our 100x100 surface and verify that even
+ // though our lambda will yield pixels forever, only one row is written per
+ // call to WritePixelsToRow().
+ for (int row = 0; row < 99; ++row) {
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint8_t>([&]{
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(100u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isSome());
+ EXPECT_EQ(IntRect(0, row, 100, 1), invalidRect->mInputSpaceRect);
+ EXPECT_EQ(IntRect(0, row, 100, 1), invalidRect->mOutputSpaceRect);
+
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 100, row + 1));
+ }
+
+ // Write the final line, which should finish the surface.
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint8_t>([&]{
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+
+ // Note that the final invalid rect we expect here is only the last row;
+ // that's because we called TakeInvalidRect() repeatedly in the loop above.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 99, 100, 1),
+ IntRect(0, 99, 100, 1));
+
+ // Check that the generated image is correct.
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelsToRow<uint8_t>([&]{
+ count++;
+ return AsVariant(uint8_t(128));
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is still correct.
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 100, 100));
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWritePixelsToRowEarlyExit)
+{
+ auto checkEarlyExit =
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink, WriteState aState) {
+ // Write half a row of 255s and then exit early with |aState|. If the lambda
+ // keeps getting called, we'll write 128s, which will cause the test to
+ // fail.
+ uint32_t count = 0;
+ auto result = aSink->WritePixelsToRow<uint8_t>([&]() -> NextPixel<uint8_t> {
+ if (count == 50) {
+ return AsVariant(aState);
+ }
+ return count++ < 50 ? AsVariant(uint8_t(255))
+ : AsVariant(uint8_t(128));
+ });
+
+ EXPECT_EQ(aState, result);
+ EXPECT_EQ(50u, count);
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 50, 1));
+
+ if (aState != WriteState::FINISHED) {
+ // We should still be able to write more at this point.
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ // Verify that we can resume the same row and still stop at the end.
+ count = 0;
+ WriteState result = aSink->WritePixelsToRow<uint8_t>([&]{
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(50u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 100, 1));
+
+ return;
+ }
+
+ // We should've finished the surface at this point.
+ AssertCorrectPipelineFinalState(aSink,
+ IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelsToRow<uint8_t>([&]{
+ count++;
+ return AsVariant(uint8_t(128));
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is still correct.
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 50, 1));
+ };
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [&](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteBuffer)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Create a buffer the same size as one row of the surface (which is 100x100),
+ // containing 60 pixels of 255 in the middle and 20 transparent pixels of 0 on
+ // either side.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 20 <= i && i < 80 ? 255 : 0;
+ }
+
+ // Write the buffer to every row of the surface and check that the generated
+ // image is correct.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), [&]{
+ return aSink->WriteBuffer(buffer);
+ });
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteBufferPartialRow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Create a buffer the same size as one row of the surface, containing all
+ // 255 pixels.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 255;
+ }
+
+ // Write the buffer to the middle 60 pixels of every row of the surface and
+ // check that the generated image is correct.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(20, 0, 60, 100), [&]{
+ return aSink->WriteBuffer(buffer, 20, 60);
+ });
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteBufferPartialRowStartColOverflow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Create a buffer the same size as one row of the surface, containing all
+ // 255 pixels.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 255;
+ }
+
+ {
+ // Write the buffer to successive rows until every row of the surface
+ // has been written. We place the start column beyond the end of the row,
+ // which will prevent us from writing anything, so we check that the
+ // generated image is entirely 0.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteBuffer(buffer, 100, 100);
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write the buffer to successive rows until every row of the surface
+ // has been written. We use column 50 as the start column, but we still
+ // write the buffer, which means we overflow the right edge of the surface
+ // by 50 pixels. We check that the left half of the generated image is
+ // 0 and the right half is 255.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteBuffer(buffer, 50, 100);
+ });
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteBufferPartialRowBufferOverflow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Create a buffer twice as large as a row of the surface. The first half
+ // (which is as large as a row of the image) will contain 255 pixels,
+ // while the second half will contain 128 pixels.
+ uint8_t buffer[200];
+ for (int i = 0; i < 200; ++i) {
+ buffer[i] = i < 100 ? 255 : 128;
+ }
+
+ {
+ // Write the buffer to successive rows until every row of the surface has
+ // been written. The buffer extends 100 pixels to the right of a row of
+ // the surface, but bounds checking will prevent us from overflowing the
+ // buffer. We check that the generated image is entirely 255 since the
+ // pixels on the right side of the buffer shouldn't have been written to
+ // the surface.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(0, 0, 100, 100), [&]{
+ return aSink->WriteBuffer(buffer, 0, 200);
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write from the buffer to the middle of each row of the surface. That
+ // means that the left side of each row should be 0, since we didn't write
+ // anything there. A buffer overflow would cause us to write buffer
+ // contents into the left side of each row. We check that the generated
+ // image is 0 on the left side and 255 on the right.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteBuffer(buffer, 50, 200);
+ });
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteBufferFromNullSource)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Calling WriteBuffer() with a null pointer should fail without making any
+ // changes to the surface.
+ uint8_t* nullBuffer = nullptr;
+ WriteState result = aSink->WriteBuffer(nullBuffer);
+
+ EXPECT_EQ(WriteState::FAILURE, result);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+ Maybe<SurfaceInvalidRect> invalidRect = aSink->TakeInvalidRect();
+ EXPECT_TRUE(invalidRect.isNothing());
+
+ // Check that nothing got written to the surface.
+ CheckGeneratedPalettedImage(aDecoder, IntRect(0, 0, 0, 0));
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteEmptyRow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ {
+ // Write an empty row to each row of the surface. We check that the
+ // generated image is entirely 0.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteEmptyRow();
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Write a partial row before we begin calling WriteEmptyRow(). We check
+ // that the generated image is entirely 0, which is to be expected since
+ // WriteEmptyRow() overwrites the current row even if some data has
+ // already been written to it.
+ uint32_t count = 0;
+ auto result = aSink->WritePixels<uint8_t>([&]() -> NextPixel<uint8_t> {
+ if (count == 50) {
+ return AsVariant(WriteState::NEED_MORE_DATA);
+ }
+ ++count;
+ return AsVariant(uint8_t(255));
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ EXPECT_EQ(50u, count);
+ EXPECT_FALSE(aSink->IsSurfaceFinished());
+
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(0, 0, 0, 0), [&]{
+ return aSink->WriteEmptyRow();
+ });
+ }
+
+ ResetForNextPass(aSink);
+
+ {
+ // Create a buffer the same size as one row of the surface, containing all
+ // 255 pixels.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 255;
+ }
+
+ // Write an empty row to the middle 60 rows of the surface. The first 20
+ // and last 20 rows will be 255. (We need to use DoCheckIterativeWrite()
+ // here because we need a custom function to check the output, since it
+ // can't be described by a simple rect.)
+ auto writeFunc = [&](uint32_t aRow) {
+ if (aRow < 20 || aRow >= 80) {
+ return aSink->WriteBuffer(buffer);
+ } else {
+ return aSink->WriteEmptyRow();
+ }
+ };
+
+ auto checkFunc = [&]{
+ EXPECT_TRUE(PalettedRowsAreSolidColor(aDecoder, 0, 20, 255));
+ EXPECT_TRUE(PalettedRowsAreSolidColor(aDecoder, 20, 60, 0));
+ EXPECT_TRUE(PalettedRowsAreSolidColor(aDecoder, 80, 20, 255));
+ };
+
+ DoCheckIterativeWrite(aSink, writeFunc, checkFunc);
+ }
+ });
+}
+
+TEST(ImageSurfaceSink, PalettedSurfaceSinkWriteUnsafeComputedRow)
+{
+ WithPalettedSurfaceSink(IntRect(0, 0, 100, 100),
+ [](Decoder* aDecoder, PalettedSurfaceSink* aSink) {
+ // Create an all-255 buffer the same size as one row of the surface.
+ uint8_t buffer[100];
+ for (int i = 0; i < 100; ++i) {
+ buffer[i] = 255;
+ }
+
+ // Write the buffer to successive rows until every row of the surface has
+ // been written. We only write to the right half of each row, so we check
+ // that the left side of the generated image is 0 and the right side is 255.
+ CheckPalettedIterativeWrite(aDecoder, aSink, IntRect(50, 0, 50, 100), [&]{
+ return aSink->WriteUnsafeComputedRow<uint8_t>([&](uint8_t* aRow,
+ uint32_t aLength) {
+ EXPECT_EQ(100u, aLength );
+ memcpy(aRow + 50, buffer, 50 * sizeof(uint8_t));
+ });
+ });
+ });
+}
diff --git a/image/test/gtest/animated-with-extra-image-sub-blocks.gif b/image/test/gtest/animated-with-extra-image-sub-blocks.gif
new file mode 100644
index 000000000..a145c814a
--- /dev/null
+++ b/image/test/gtest/animated-with-extra-image-sub-blocks.gif
Binary files differ
diff --git a/image/test/gtest/corrupt-with-bad-bmp-height.ico b/image/test/gtest/corrupt-with-bad-bmp-height.ico
new file mode 100644
index 000000000..ee4a90fcd
--- /dev/null
+++ b/image/test/gtest/corrupt-with-bad-bmp-height.ico
Binary files differ
diff --git a/image/test/gtest/corrupt-with-bad-bmp-width.ico b/image/test/gtest/corrupt-with-bad-bmp-width.ico
new file mode 100644
index 000000000..aa4051cd0
--- /dev/null
+++ b/image/test/gtest/corrupt-with-bad-bmp-width.ico
Binary files differ
diff --git a/image/test/gtest/corrupt.jpg b/image/test/gtest/corrupt.jpg
new file mode 100644
index 000000000..555a416d7
--- /dev/null
+++ b/image/test/gtest/corrupt.jpg
Binary files differ
diff --git a/image/test/gtest/downscaled.bmp b/image/test/gtest/downscaled.bmp
new file mode 100644
index 000000000..9e6a29e62
--- /dev/null
+++ b/image/test/gtest/downscaled.bmp
Binary files differ
diff --git a/image/test/gtest/downscaled.gif b/image/test/gtest/downscaled.gif
new file mode 100644
index 000000000..ff9a20bcd
--- /dev/null
+++ b/image/test/gtest/downscaled.gif
Binary files differ
diff --git a/image/test/gtest/downscaled.ico b/image/test/gtest/downscaled.ico
new file mode 100644
index 000000000..ee112af0a
--- /dev/null
+++ b/image/test/gtest/downscaled.ico
Binary files differ
diff --git a/image/test/gtest/downscaled.icon b/image/test/gtest/downscaled.icon
new file mode 100644
index 000000000..19785f5dc
--- /dev/null
+++ b/image/test/gtest/downscaled.icon
Binary files differ
diff --git a/image/test/gtest/downscaled.jpg b/image/test/gtest/downscaled.jpg
new file mode 100644
index 000000000..5a4b3cd03
--- /dev/null
+++ b/image/test/gtest/downscaled.jpg
Binary files differ
diff --git a/image/test/gtest/downscaled.png b/image/test/gtest/downscaled.png
new file mode 100644
index 000000000..b71b4652d
--- /dev/null
+++ b/image/test/gtest/downscaled.png
Binary files differ
diff --git a/image/test/gtest/first-frame-green.gif b/image/test/gtest/first-frame-green.gif
new file mode 100644
index 000000000..cd3c7d3db
--- /dev/null
+++ b/image/test/gtest/first-frame-green.gif
Binary files differ
diff --git a/image/test/gtest/first-frame-green.png b/image/test/gtest/first-frame-green.png
new file mode 100644
index 000000000..115f035d8
--- /dev/null
+++ b/image/test/gtest/first-frame-green.png
Binary files differ
diff --git a/image/test/gtest/first-frame-padding.gif b/image/test/gtest/first-frame-padding.gif
new file mode 100644
index 000000000..e6d7c4932
--- /dev/null
+++ b/image/test/gtest/first-frame-padding.gif
Binary files differ
diff --git a/image/test/gtest/green-1x1-truncated.gif b/image/test/gtest/green-1x1-truncated.gif
new file mode 100644
index 000000000..0829f9694
--- /dev/null
+++ b/image/test/gtest/green-1x1-truncated.gif
Binary files differ
diff --git a/image/test/gtest/green.bmp b/image/test/gtest/green.bmp
new file mode 100644
index 000000000..f79dd672a
--- /dev/null
+++ b/image/test/gtest/green.bmp
Binary files differ
diff --git a/image/test/gtest/green.gif b/image/test/gtest/green.gif
new file mode 100644
index 000000000..ef215dfc9
--- /dev/null
+++ b/image/test/gtest/green.gif
Binary files differ
diff --git a/image/test/gtest/green.ico b/image/test/gtest/green.ico
new file mode 100644
index 000000000..c5dfa8b53
--- /dev/null
+++ b/image/test/gtest/green.ico
Binary files differ
diff --git a/image/test/gtest/green.icon b/image/test/gtest/green.icon
new file mode 100644
index 000000000..c74e62fee
--- /dev/null
+++ b/image/test/gtest/green.icon
Binary files differ
diff --git a/image/test/gtest/green.jpg b/image/test/gtest/green.jpg
new file mode 100644
index 000000000..48c454d27
--- /dev/null
+++ b/image/test/gtest/green.jpg
Binary files differ
diff --git a/image/test/gtest/green.png b/image/test/gtest/green.png
new file mode 100644
index 000000000..7df25f33b
--- /dev/null
+++ b/image/test/gtest/green.png
Binary files differ
diff --git a/image/test/gtest/invalid-truncated-metadata.bmp b/image/test/gtest/invalid-truncated-metadata.bmp
new file mode 100644
index 000000000..228c5c999
--- /dev/null
+++ b/image/test/gtest/invalid-truncated-metadata.bmp
Binary files differ
diff --git a/image/test/gtest/moz.build b/image/test/gtest/moz.build
new file mode 100644
index 000000000..5cf6d5116
--- /dev/null
+++ b/image/test/gtest/moz.build
@@ -0,0 +1,78 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Library('imagetest')
+
+UNIFIED_SOURCES = [
+ 'Common.cpp',
+ 'TestADAM7InterpolatingFilter.cpp',
+ 'TestCopyOnWrite.cpp',
+ 'TestDecoders.cpp',
+ 'TestDecodeToSurface.cpp',
+ 'TestDeinterlacingFilter.cpp',
+ 'TestMetadata.cpp',
+ 'TestRemoveFrameRectFilter.cpp',
+ 'TestSourceBuffer.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 += [
+ 'animated-with-extra-image-sub-blocks.gif',
+ 'corrupt-with-bad-bmp-height.ico',
+ 'corrupt-with-bad-bmp-width.ico',
+ 'corrupt.jpg',
+ 'downscaled.bmp',
+ 'downscaled.gif',
+ 'downscaled.ico',
+ 'downscaled.icon',
+ 'downscaled.jpg',
+ 'downscaled.png',
+ 'first-frame-green.gif',
+ 'first-frame-green.png',
+ 'first-frame-padding.gif',
+ 'green-1x1-truncated.gif',
+ 'green.bmp',
+ 'green.gif',
+ 'green.ico',
+ 'green.icon',
+ 'green.jpg',
+ 'green.png',
+ 'invalid-truncated-metadata.bmp',
+ 'no-frame-delay.gif',
+ 'rle4.bmp',
+ 'rle8.bmp',
+ 'transparent-ico-with-and-mask.ico',
+ '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'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/image/test/gtest/no-frame-delay.gif b/image/test/gtest/no-frame-delay.gif
new file mode 100644
index 000000000..1c50b6743
--- /dev/null
+++ b/image/test/gtest/no-frame-delay.gif
Binary files differ
diff --git a/image/test/gtest/rle4.bmp b/image/test/gtest/rle4.bmp
new file mode 100644
index 000000000..78a092787
--- /dev/null
+++ b/image/test/gtest/rle4.bmp
Binary files differ
diff --git a/image/test/gtest/rle8.bmp b/image/test/gtest/rle8.bmp
new file mode 100644
index 000000000..bd793b6b6
--- /dev/null
+++ b/image/test/gtest/rle8.bmp
Binary files differ
diff --git a/image/test/gtest/transparent-ico-with-and-mask.ico b/image/test/gtest/transparent-ico-with-and-mask.ico
new file mode 100644
index 000000000..ab0dc4bce
--- /dev/null
+++ b/image/test/gtest/transparent-ico-with-and-mask.ico
Binary files differ
diff --git a/image/test/gtest/transparent-if-within-ico.bmp b/image/test/gtest/transparent-if-within-ico.bmp
new file mode 100644
index 000000000..4dc04c181
--- /dev/null
+++ b/image/test/gtest/transparent-if-within-ico.bmp
Binary files differ
diff --git a/image/test/gtest/transparent.gif b/image/test/gtest/transparent.gif
new file mode 100644
index 000000000..48f5c7caf
--- /dev/null
+++ b/image/test/gtest/transparent.gif
Binary files differ
diff --git a/image/test/gtest/transparent.png b/image/test/gtest/transparent.png
new file mode 100644
index 000000000..fc8002053
--- /dev/null
+++ b/image/test/gtest/transparent.png
Binary files differ