summaryrefslogtreecommitdiffstats
path: root/image/test/gtest
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /image/test/gtest
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'image/test/gtest')
-rw-r--r--image/test/gtest/Common.cpp737
-rw-r--r--image/test/gtest/Common.h548
-rw-r--r--image/test/gtest/TestADAM7InterpolatingFilter.cpp595
-rw-r--r--image/test/gtest/TestAnimationFrameBuffer.cpp896
-rw-r--r--image/test/gtest/TestBlendAnimationFilter.cpp450
-rw-r--r--image/test/gtest/TestContainers.cpp105
-rw-r--r--image/test/gtest/TestCopyOnWrite.cpp237
-rw-r--r--image/test/gtest/TestDecodeToSurface.cpp171
-rw-r--r--image/test/gtest/TestDecoders.cpp994
-rw-r--r--image/test/gtest/TestDecodersPerf.cpp159
-rw-r--r--image/test/gtest/TestDeinterlacingFilter.cpp636
-rw-r--r--image/test/gtest/TestDownscalingFilter.cpp231
-rw-r--r--image/test/gtest/TestDownscalingFilterNoSkia.cpp55
-rw-r--r--image/test/gtest/TestFrameAnimator.cpp122
-rw-r--r--image/test/gtest/TestLoader.cpp103
-rw-r--r--image/test/gtest/TestMetadata.cpp253
-rw-r--r--image/test/gtest/TestRemoveFrameRectFilter.cpp311
-rw-r--r--image/test/gtest/TestSourceBuffer.cpp822
-rw-r--r--image/test/gtest/TestStreamingLexer.cpp935
-rw-r--r--image/test/gtest/TestSurfaceCache.cpp159
-rw-r--r--image/test/gtest/TestSurfacePipeIntegration.cpp349
-rw-r--r--image/test/gtest/TestSurfaceSink.cpp980
-rw-r--r--image/test/gtest/TestSwizzleFilter.cpp120
-rw-r--r--image/test/gtest/animated-with-extra-image-sub-blocks.gifbin0 -> 434 bytes
-rw-r--r--image/test/gtest/blend.gifbin0 -> 412 bytes
-rw-r--r--image/test/gtest/blend.pngbin0 -> 339 bytes
-rw-r--r--image/test/gtest/blend.webpbin0 -> 160 bytes
-rw-r--r--image/test/gtest/bug-1655846.avifbin0 -> 256 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-with-bad-ico-bpp.icobin0 -> 3270 bytes
-rw-r--r--image/test/gtest/corrupt.jpgbin0 -> 2477 bytes
-rw-r--r--image/test/gtest/downscaled.avifbin0 -> 862 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 -> 40005 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/downscaled.webpbin0 -> 56 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-green.webpbin0 -> 154 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-large-bmp.icobin0 -> 270398 bytes
-rw-r--r--image/test/gtest/green-large-png.icobin0 -> 341 bytes
-rw-r--r--image/test/gtest/green-multiple-sizes.icobin0 -> 14144 bytes
-rw-r--r--image/test/gtest/green.avifbin0 -> 433 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.icc_srgb.webpbin0 -> 3092 bytes
-rw-r--r--image/test/gtest/green.icobin0 -> 41662 bytes
-rw-r--r--image/test/gtest/green.iconbin0 -> 40004 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/green.webpbin0 -> 42 bytes
-rw-r--r--image/test/gtest/invalid-truncated-metadata.bmpbin0 -> 54 bytes
-rw-r--r--image/test/gtest/large.avifbin0 -> 81500 bytes
-rw-r--r--image/test/gtest/large.webpbin0 -> 168686 bytes
-rw-r--r--image/test/gtest/moz.build124
-rw-r--r--image/test/gtest/multilayer.avifbin0 -> 64613 bytes
-rw-r--r--image/test/gtest/no-frame-delay.gifbin0 -> 317 bytes
-rw-r--r--image/test/gtest/perf_cmyk.jpgbin0 -> 18960 bytes
-rw-r--r--image/test/gtest/perf_gray.jpgbin0 -> 4479 bytes
-rw-r--r--image/test/gtest/perf_gray.pngbin0 -> 4472 bytes
-rw-r--r--image/test/gtest/perf_gray_alpha.pngbin0 -> 6847 bytes
-rw-r--r--image/test/gtest/perf_srgb.gifbin0 -> 1743 bytes
-rw-r--r--image/test/gtest/perf_srgb.pngbin0 -> 2993 bytes
-rw-r--r--image/test/gtest/perf_srgb_alpha.pngbin0 -> 3203 bytes
-rw-r--r--image/test/gtest/perf_srgb_alpha_lossless.webpbin0 -> 3134 bytes
-rw-r--r--image/test/gtest/perf_srgb_alpha_lossy.webpbin0 -> 5014 bytes
-rw-r--r--image/test/gtest/perf_srgb_lossless.webpbin0 -> 3134 bytes
-rw-r--r--image/test/gtest/perf_srgb_lossy.webpbin0 -> 4948 bytes
-rw-r--r--image/test/gtest/perf_ycbcr.jpgbin0 -> 15049 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/stackcheck.avifbin0 -> 194817 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-no-alpha-header.webpbin0 -> 120 bytes
-rw-r--r--image/test/gtest/transparent.avifbin0 -> 209480 bytes
-rw-r--r--image/test/gtest/transparent.gifbin0 -> 355 bytes
-rw-r--r--image/test/gtest/transparent.pngbin0 -> 419 bytes
-rw-r--r--image/test/gtest/transparent.webpbin0 -> 120 bytes
85 files changed, 10092 insertions, 0 deletions
diff --git a/image/test/gtest/Common.cpp b/image/test/gtest/Common.cpp
new file mode 100644
index 0000000000..ae5522037a
--- /dev/null
+++ b/image/test/gtest/Common.cpp
@@ -0,0 +1,737 @@
+/* -*- 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 "gfxPlatform.h"
+
+#include "imgITools.h"
+#include "mozilla/Preferences.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDirectoryServiceDefs.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::vector;
+
+static bool sImageLibInitialized = false;
+
+AutoInitializeImageLib::AutoInitializeImageLib() {
+ if (MOZ_LIKELY(sImageLibInitialized)) {
+ return;
+ }
+
+ EXPECT_TRUE(NS_IsMainThread());
+ sImageLibInitialized = true;
+
+ // Ensure WebP is enabled to run decoder tests.
+ nsresult rv = Preferences::SetBool("image.webp.enabled", true);
+ EXPECT_TRUE(rv == NS_OK);
+
+ // Ensure AVIF is enabled to run decoder tests.
+ rv = Preferences::SetBool("image.avif.enabled", true);
+ EXPECT_TRUE(rv == NS_OK);
+
+ // Ensure that ImageLib services are initialized.
+ nsCOMPtr<imgITools> imgTools =
+ do_CreateInstance("@mozilla.org/image/tools;1");
+ EXPECT_TRUE(imgTools != nullptr);
+
+ // Ensure gfxPlatform is initialized.
+ gfxPlatform::GetPlatform();
+
+ // Ensure we always color manage images with gtests.
+ gfxPlatform::GetCMSMode();
+ gfxPlatform::SetCMSModeOverride(eCMSMode_All);
+
+ // Depending on initialization order, it is possible that our pref changes
+ // have not taken effect yet because there are pending gfx-related events on
+ // the main thread.
+ SpinPendingEvents();
+}
+
+void ImageBenchmarkBase::SetUp() {
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(mTestCase.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.
+ mSourceBuffer = new SourceBuffer();
+ mSourceBuffer->ExpectLength(length);
+ rv = mSourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ mSourceBuffer->Complete(NS_OK);
+}
+
+void ImageBenchmarkBase::TearDown() {}
+
+///////////////////////////////////////////////////////////////////////////////
+// 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; \
+ }
+
+void SpinPendingEvents() {
+ nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+ EXPECT_TRUE(mainThread != nullptr);
+
+ bool processed;
+ do {
+ processed = false;
+ nsresult rv = mainThread->ProcessNextEvent(false, &processed);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ } while (processed);
+}
+
+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.forget(), 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 RowsAreSolidColor(SourceSurface* aSurface, int32_t aStartRow,
+ int32_t aRowCount, BGRAColor aColor,
+ uint8_t aFuzz /* = 0 */) {
+ IntSize size = aSurface->GetSize();
+ return RectIsSolidColor(
+ aSurface, IntRect(0, aStartRow, size.width, aRowCount), aColor, aFuzz);
+}
+
+bool RectIsSolidColor(SourceSurface* aSurface, const IntRect& aRect,
+ BGRAColor aColor, uint8_t aFuzz /* = 0 */) {
+ IntSize surfaceSize = aSurface->GetSize();
+ IntRect rect =
+ aRect.Intersect(IntRect(0, 0, surfaceSize.width, surfaceSize.height));
+
+ RefPtr<DataSourceSurface> dataSurface = aSurface->GetDataSurface();
+ ASSERT_TRUE_OR_RETURN(dataSurface != nullptr, false);
+
+ DataSourceSurface::ScopedMap mapping(dataSurface,
+ DataSourceSurface::MapType::READ);
+ ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false);
+ ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false);
+
+ uint8_t* data = mapping.GetData();
+ ASSERT_TRUE_OR_RETURN(data != nullptr, false);
+
+ BGRAColor pmColor = aColor.Premultiply();
+ uint32_t expectedPixel = pmColor.AsPixel();
+
+ int32_t rowLength = mapping.GetStride();
+ 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;
+ uint32_t gotPixel = *reinterpret_cast<uint32_t*>(data + i);
+ if (expectedPixel != gotPixel) {
+ BGRAColor gotColor = BGRAColor::FromPixel(gotPixel);
+ if (abs(pmColor.mBlue - gotColor.mBlue) > aFuzz ||
+ abs(pmColor.mGreen - gotColor.mGreen) > aFuzz ||
+ abs(pmColor.mRed - gotColor.mRed) > aFuzz ||
+ abs(pmColor.mAlpha - gotColor.mAlpha) > aFuzz) {
+ EXPECT_EQ(pmColor.mBlue, gotColor.mBlue);
+ EXPECT_EQ(pmColor.mGreen, gotColor.mGreen);
+ EXPECT_EQ(pmColor.mRed, gotColor.mRed);
+ EXPECT_EQ(pmColor.mAlpha, gotColor.mAlpha);
+ ASSERT_EQ_OR_RETURN(expectedPixel, gotPixel, 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);
+
+ DataSourceSurface::ScopedMap mapping(dataSurface,
+ DataSourceSurface::MapType::READ);
+ ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false);
+ ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false);
+
+ uint8_t* data = mapping.GetData();
+ ASSERT_TRUE_OR_RETURN(data != nullptr, false);
+
+ int32_t rowLength = mapping.GetStride();
+ for (int32_t col = 0; col < surfaceSize.width; ++col) {
+ int32_t i = aRow * rowLength + col * 4;
+ uint32_t gotPixelData = *reinterpret_cast<uint32_t*>(data + i);
+ BGRAColor gotPixel = BGRAColor::FromPixel(gotPixelData);
+ EXPECT_EQ(aPixels[col].mBlue, gotPixel.mBlue);
+ EXPECT_EQ(aPixels[col].mGreen, gotPixel.mGreen);
+ EXPECT_EQ(aPixels[col].mRed, gotPixel.mRed);
+ EXPECT_EQ(aPixels[col].mAlpha, gotPixel.mAlpha);
+ ASSERT_EQ_OR_RETURN(aPixels[col].AsPixel(), gotPixelData, false);
+ }
+
+ return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// SurfacePipe Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+already_AddRefed<Decoder> CreateTrivialDecoder() {
+ DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+ auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+ RefPtr<Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, Nothing(), DefaultDecoderFlags(),
+ 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();
+ CheckGeneratedSurface(surface, aRect, BGRAColor::Green(),
+ BGRAColor::Transparent(), aFuzz);
+}
+
+void CheckGeneratedSurface(SourceSurface* aSurface, const IntRect& aRect,
+ const BGRAColor& aInnerColor,
+ const BGRAColor& aOuterColor,
+ uint8_t aFuzz /* = 0 */) {
+ const IntSize surfaceSize = aSurface->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 the inner color. (Region 'C'.)
+ EXPECT_TRUE(RectIsSolidColor(aSurface, aRect, aInnerColor, aFuzz));
+
+ // Check that the area above the output rect is the outer color. (Region 'A'.)
+ EXPECT_TRUE(RectIsSolidColor(aSurface,
+ IntRect(0, 0, surfaceSize.width, aRect.Y()),
+ aOuterColor, aFuzz));
+
+ // Check that the area to the left of the output rect is the outer color.
+ // (Region 'B'.)
+ EXPECT_TRUE(RectIsSolidColor(aSurface,
+ IntRect(0, aRect.Y(), aRect.X(), aRect.YMost()),
+ aOuterColor, aFuzz));
+
+ // Check that the area to the right of the output rect is the outer color.
+ // (Region 'D'.)
+ const int32_t widthOnRight = surfaceSize.width - aRect.XMost();
+ EXPECT_TRUE(RectIsSolidColor(
+ aSurface, IntRect(aRect.XMost(), aRect.Y(), widthOnRight, aRect.YMost()),
+ aOuterColor, aFuzz));
+
+ // Check that the area below the output rect is the outer color. (Region 'E'.)
+ const int32_t heightBelow = surfaceSize.height - aRect.YMost();
+ EXPECT_TRUE(RectIsSolidColor(
+ aSurface, IntRect(0, aRect.YMost(), surfaceSize.width, heightBelow),
+ aOuterColor, aFuzz));
+}
+
+void CheckWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter,
+ const Maybe<IntRect>& aOutputRect /* = Nothing() */,
+ const Maybe<IntRect>& aInputRect /* = Nothing() */,
+ const Maybe<IntRect>& aInputWriteRect /* = Nothing() */,
+ const Maybe<IntRect>& aOutputWriteRect /* = Nothing() */,
+ uint8_t aFuzz /* = 0 */) {
+ CheckTransformedWritePixels(aDecoder, aFilter, BGRAColor::Green(),
+ BGRAColor::Green(), aOutputRect, aInputRect,
+ aInputWriteRect, aOutputWriteRect, aFuzz);
+}
+
+void CheckTransformedWritePixels(
+ Decoder* aDecoder, SurfaceFilter* aFilter, const BGRAColor& aInputColor,
+ const BGRAColor& aOutputColor,
+ const Maybe<IntRect>& aOutputRect /* = Nothing() */,
+ const Maybe<IntRect>& aInputRect /* = Nothing() */,
+ const Maybe<IntRect>& aInputWriteRect /* = Nothing() */,
+ const 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(aInputColor.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(aInputColor.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.
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ RefPtr<SourceSurface> surface = currentFrame->GetSourceSurface();
+ CheckGeneratedSurface(surface, outputWriteRect, aOutputColor,
+ BGRAColor::Transparent(), aFuzz);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// 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 GreenWebPTestCase() {
+ return ImageTestCase("green.webp", "image/webp", IntSize(100, 100));
+}
+
+// Forcing sRGB is required until nsAVIFDecoder supports ICC profiles
+// See bug 1634741
+ImageTestCase GreenAVIFTestCase() {
+ return ImageTestCase("green.avif", "image/avif", IntSize(100, 100))
+ .WithSurfaceFlags(SurfaceFlags::TO_SRGB_COLORSPACE);
+}
+
+// Forcing sRGB is required until nsAVIFDecoder supports ICC profiles
+// See bug 1634741
+ImageTestCase StackCheckAVIFTestCase() {
+ return ImageTestCase("stackcheck.avif", "image/avif", IntSize(4096, 2924),
+ TEST_CASE_IGNORE_OUTPUT)
+ .WithSurfaceFlags(SurfaceFlags::TO_SRGB_COLORSPACE);
+}
+
+// Forcing sRGB is required until nsAVIFDecoder supports ICC profiles
+// See bug 1634741
+// Add TEST_CASE_IGNORE_OUTPUT since this isn't a solid green image and we just
+// want to test that it decodes correctly.
+ImageTestCase MultiLayerAVIFTestCase() {
+ return ImageTestCase("multilayer.avif", "image/avif", IntSize(1280, 720),
+ TEST_CASE_IGNORE_OUTPUT)
+ .WithSurfaceFlags(SurfaceFlags::TO_SRGB_COLORSPACE);
+}
+
+ImageTestCase LargeWebPTestCase() {
+ return ImageTestCase("large.webp", "image/webp", IntSize(1200, 660),
+ TEST_CASE_IGNORE_OUTPUT);
+}
+
+ImageTestCase LargeAVIFTestCase() {
+ return ImageTestCase("large.avif", "image/avif", IntSize(1200, 660),
+ TEST_CASE_IGNORE_OUTPUT);
+}
+
+ImageTestCase GreenWebPIccSrgbTestCase() {
+ return ImageTestCase("green.icc_srgb.webp", "image/webp", IntSize(100, 100));
+}
+
+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 GreenFirstFrameAnimatedWebPTestCase() {
+ return ImageTestCase("first-frame-green.webp", "image/webp",
+ IntSize(100, 100), TEST_CASE_IS_ANIMATED);
+}
+
+ImageTestCase BlendAnimatedGIFTestCase() {
+ return ImageTestCase("blend.gif", "image/gif", IntSize(100, 100),
+ TEST_CASE_IS_ANIMATED);
+}
+
+ImageTestCase BlendAnimatedPNGTestCase() {
+ return ImageTestCase("blend.png", "image/png", IntSize(100, 100),
+ TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED);
+}
+
+ImageTestCase BlendAnimatedWebPTestCase() {
+ return ImageTestCase("blend.webp", "image/webp", 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 CorruptICOWithBadBppTestCase() {
+ // This test case is an ICO with a BPP (15) in the ICO header which differs
+ // from that in the BMP header itself (1). It should ignore the ICO BPP when
+ // the BMP BPP is available and thus correctly decode the image.
+ return ImageTestCase("corrupt-with-bad-ico-bpp.ico", "image/x-icon",
+ IntSize(100, 100), TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase CorruptAVIFTestCase() {
+ return ImageTestCase("bug-1655846.avif", "image/avif", IntSize(100, 100),
+ TEST_CASE_HAS_ERROR);
+}
+
+ImageTestCase TransparentAVIFTestCase() {
+ return ImageTestCase("transparent.avif", "image/avif", IntSize(1200, 1200),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+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 TransparentWebPTestCase() {
+ ImageTestCase test("transparent.webp", "image/webp", IntSize(100, 100),
+ TEST_CASE_IS_TRANSPARENT);
+ test.mColor = BGRAColor::Transparent();
+ return test;
+}
+
+ImageTestCase TransparentNoAlphaHeaderWebPTestCase() {
+ ImageTestCase test("transparent-no-alpha-header.webp", "image/webp",
+ IntSize(100, 100), TEST_CASE_IS_FUZZY);
+ test.mColor = BGRAColor(0x00, 0x00, 0x00, 0xFF); // black
+ return test;
+}
+
+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 DownscaledWebPTestCase() {
+ return ImageTestCase("downscaled.webp", "image/webp", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+ImageTestCase DownscaledAVIFTestCase() {
+ return ImageTestCase("downscaled.avif", "image/avif", IntSize(100, 100),
+ IntSize(20, 20));
+}
+
+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));
+}
+
+ImageTestCase LargeICOWithBMPTestCase() {
+ return ImageTestCase("green-large-bmp.ico", "image/x-icon", IntSize(256, 256),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase LargeICOWithPNGTestCase() {
+ return ImageTestCase("green-large-png.ico", "image/x-icon", IntSize(512, 512),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase GreenMultipleSizesICOTestCase() {
+ return ImageTestCase("green-multiple-sizes.ico", "image/x-icon",
+ IntSize(256, 256));
+}
+
+ImageTestCase PerfGrayJPGTestCase() {
+ return ImageTestCase("perf_gray.jpg", "image/jpeg", IntSize(1000, 1000));
+}
+
+ImageTestCase PerfCmykJPGTestCase() {
+ return ImageTestCase("perf_cmyk.jpg", "image/jpeg", IntSize(1000, 1000));
+}
+
+ImageTestCase PerfYCbCrJPGTestCase() {
+ return ImageTestCase("perf_ycbcr.jpg", "image/jpeg", IntSize(1000, 1000));
+}
+
+ImageTestCase PerfRgbPNGTestCase() {
+ return ImageTestCase("perf_srgb.png", "image/png", IntSize(1000, 1000));
+}
+
+ImageTestCase PerfRgbAlphaPNGTestCase() {
+ return ImageTestCase("perf_srgb_alpha.png", "image/png", IntSize(1000, 1000),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase PerfGrayPNGTestCase() {
+ return ImageTestCase("perf_gray.png", "image/png", IntSize(1000, 1000));
+}
+
+ImageTestCase PerfGrayAlphaPNGTestCase() {
+ return ImageTestCase("perf_gray_alpha.png", "image/png", IntSize(1000, 1000),
+ TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase PerfRgbLosslessWebPTestCase() {
+ return ImageTestCase("perf_srgb_lossless.webp", "image/webp",
+ IntSize(1000, 1000));
+}
+
+ImageTestCase PerfRgbAlphaLosslessWebPTestCase() {
+ return ImageTestCase("perf_srgb_alpha_lossless.webp", "image/webp",
+ IntSize(1000, 1000), TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase PerfRgbLossyWebPTestCase() {
+ return ImageTestCase("perf_srgb_lossy.webp", "image/webp",
+ IntSize(1000, 1000));
+}
+
+ImageTestCase PerfRgbAlphaLossyWebPTestCase() {
+ return ImageTestCase("perf_srgb_alpha_lossy.webp", "image/webp",
+ IntSize(1000, 1000), TEST_CASE_IS_TRANSPARENT);
+}
+
+ImageTestCase PerfRgbGIFTestCase() {
+ return ImageTestCase("perf_srgb.gif", "image/gif", IntSize(1000, 1000));
+}
+
+} // namespace image
+} // namespace mozilla
diff --git a/image/test/gtest/Common.h b/image/test/gtest/Common.h
new file mode 100644
index 0000000000..0c7ba50275
--- /dev/null
+++ b/image/test/gtest/Common.h
@@ -0,0 +1,548 @@
+/* -*- 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/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+#include "Decoder.h"
+#include "gfxColor.h"
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "SurfaceFlags.h"
+#include "SurfacePipe.h"
+#include "SurfacePipeFactory.h"
+
+class nsIInputStream;
+
+namespace mozilla {
+namespace image {
+
+///////////////////////////////////////////////////////////////////////////////
+// Types
+///////////////////////////////////////////////////////////////////////////////
+
+struct BGRAColor {
+ BGRAColor() : BGRAColor(0, 0, 0, 0) {}
+
+ BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha,
+ bool aPremultiplied = false, bool asRGB = true)
+ : mBlue(aBlue),
+ mGreen(aGreen),
+ mRed(aRed),
+ mAlpha(aAlpha),
+ mPremultiplied(aPremultiplied),
+ msRGB(asRGB) {}
+
+ 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); }
+
+ static BGRAColor FromPixel(uint32_t aPixel) {
+ uint8_t r, g, b, a;
+ r = (aPixel >> gfx::SurfaceFormatBit::OS_R) & 0xFF;
+ g = (aPixel >> gfx::SurfaceFormatBit::OS_G) & 0xFF;
+ b = (aPixel >> gfx::SurfaceFormatBit::OS_B) & 0xFF;
+ a = (aPixel >> gfx::SurfaceFormatBit::OS_A) & 0xFF;
+ return BGRAColor(b, g, r, a, true);
+ }
+
+ BGRAColor DeviceColor() const {
+ MOZ_ASSERT(!mPremultiplied);
+ if (msRGB) {
+ gfx::DeviceColor color = gfx::ToDeviceColor(
+ gfx::sRGBColor(float(mRed) / 255.0f, float(mGreen) / 255.0f,
+ float(mBlue) / 255.0f, 1.0));
+ return BGRAColor(uint8_t(color.b * 255.0f), uint8_t(color.g * 255.0f),
+ uint8_t(color.r * 255.0f), mAlpha, mPremultiplied,
+ /* asRGB */ false);
+ }
+ return *this;
+ }
+
+ BGRAColor sRGBColor() const {
+ MOZ_ASSERT(msRGB);
+ MOZ_ASSERT(!mPremultiplied);
+ return *this;
+ }
+
+ BGRAColor Premultiply() const {
+ if (!mPremultiplied) {
+ return BGRAColor(gfxPreMultiply(mBlue, mAlpha),
+ gfxPreMultiply(mGreen, mAlpha),
+ gfxPreMultiply(mRed, mAlpha), mAlpha, true);
+ }
+ return *this;
+ }
+
+ uint32_t AsPixel() const {
+ if (!mPremultiplied) {
+ return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue);
+ }
+ return gfxPackedPixelNoPreMultiply(mAlpha, mRed, mGreen, mBlue);
+ }
+
+ uint8_t mBlue;
+ uint8_t mGreen;
+ uint8_t mRed;
+ uint8_t mAlpha;
+ bool mPremultiplied;
+ bool msRGB;
+};
+
+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,
+ TEST_CASE_ASSUME_SRGB_OUTPUT = 1 << 5,
+};
+
+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),
+ mSurfaceFlags(DefaultSurfaceFlags()),
+ mColor(BGRAColor::Green()) {}
+
+ 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),
+ mSurfaceFlags(DefaultSurfaceFlags()),
+ mColor(BGRAColor::Green()) {}
+
+ ImageTestCase WithSurfaceFlags(SurfaceFlags aSurfaceFlags) const {
+ ImageTestCase self = *this;
+ self.mSurfaceFlags = aSurfaceFlags;
+ return self;
+ }
+
+ ImageTestCase WithFlags(uint32_t aFlags) const {
+ ImageTestCase self = *this;
+ self.mFlags = aFlags;
+ return self;
+ }
+
+ BGRAColor ChooseColor(const BGRAColor& aColor) const {
+ // If we are forcing the output to be sRGB via the surface flag, or the
+ // test case is marked as assuming sRGB (used when the image itself is not
+ // explicitly tagged, and as a result, imagelib won't perform any color
+ // conversion), we should use the sRGB presentation of the color.
+ if ((mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) ||
+ (mFlags & TEST_CASE_ASSUME_SRGB_OUTPUT)) {
+ return aColor.sRGBColor();
+ }
+ return aColor.DeviceColor();
+ }
+
+ BGRAColor Color() const { return ChooseColor(mColor); }
+
+ uint8_t Fuzz() const {
+ // If we are using device space, there can easily be off by 1 channel errors
+ // depending on the color profile and how the rounding went.
+ if (mFlags & TEST_CASE_IS_FUZZY ||
+ !(mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE)) {
+ return 1;
+ }
+ return 0;
+ }
+
+ const char* mPath;
+ const char* mMimeType;
+ gfx::IntSize mSize;
+ gfx::IntSize mOutputSize;
+ uint32_t mFlags;
+ SurfaceFlags mSurfaceFlags;
+ BGRAColor mColor;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// 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.
+ */
+class AutoInitializeImageLib {
+ public:
+ AutoInitializeImageLib();
+};
+
+/**
+ * A test fixture class used for benchmark tests. It preloads the image data
+ * from disk to avoid including that in the timing.
+ */
+class ImageBenchmarkBase : public ::testing::Test {
+ protected:
+ ImageBenchmarkBase(const ImageTestCase& aTestCase) : mTestCase(aTestCase) {}
+
+ void SetUp() override;
+ void TearDown() override;
+
+ AutoInitializeImageLib mInit;
+ ImageTestCase mTestCase;
+ RefPtr<SourceBuffer> mSourceBuffer;
+};
+
+/// Spins on the main thread to process any pending events.
+void SpinPendingEvents();
+
+/// 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 in the range of rows specified by @aStartRow and
+ * @aRowCount of @aSurface is @aColor.
+ *
+ * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
+ * component. This may be necessary for tests that involve JPEG images or
+ * downscaling.
+ */
+bool RowsAreSolidColor(gfx::SourceSurface* aSurface, int32_t aStartRow,
+ int32_t aRowCount, BGRAColor aColor, uint8_t aFuzz = 0);
+
+/**
+ * @returns true if every pixel in the rect specified by @aRect is @aColor.
+ *
+ * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
+ * component. This may be necessary for tests that involve JPEG images or
+ * downscaling.
+ */
+bool RectIsSolidColor(gfx::SourceSurface* aSurface, const gfx::IntRect& aRect,
+ BGRAColor aColor, uint8_t aFuzz = 0);
+
+/**
+ * @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 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, bool aFinish,
+ const Configs&... aConfigs) {
+ auto pipe = MakeUnique<typename detail::FilterPipeline<Configs...>::Type>();
+ nsresult rv = pipe->Configure(aConfigs...);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ aFunc(aDecoder, pipe.get());
+
+ if (aFinish) {
+ RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+ if (currentFrame) {
+ currentFrame->Finish();
+ }
+ }
+}
+
+template <typename Func, typename... Configs>
+void WithFilterPipeline(Decoder* aDecoder, Func aFunc,
+ const Configs&... aConfigs) {
+ WithFilterPipeline(aDecoder, aFunc, true, aConfigs...);
+}
+
+/**
+ * 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,
+ const 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 surface for correctness. Reports any unexpected deviation
+ * from the expected image as GTest failures.
+ *
+ * @param aSurface The surface to check.
+ * @param aRect The region in the space of the output surface that the filter
+ * pipeline will actually write to.
+ * @param aInnerColor Check that pixels inside of aRect are this color.
+ * @param aOuterColor Check that pixels outside of aRect are this color.
+ * @param aFuzz The amount of fuzz to use in pixel comparisons.
+ */
+void CheckGeneratedSurface(gfx::SourceSurface* aSurface,
+ const gfx::IntRect& aRect,
+ const BGRAColor& aInnerColor,
+ const BGRAColor& aOuterColor, uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WritePixels() using the provided SurfaceFilter
+ * pipeline. The pipeline must be a normal (i.e., non-paletted) pipeline.
+ *
+ * The arguments are specified in the an order intended to minimize the number
+ * of arguments that most test cases need to pass.
+ *
+ * @param aDecoder The decoder whose current frame will be written to.
+ * @param aFilter The SurfaceFilter pipeline to use.
+ * @param aOutputRect The region in the space of the output surface that will be
+ * invalidated by the filter pipeline. Defaults to
+ * (0, 0, 100, 100).
+ * @param aInputRect The region in the space of the input image that will be
+ * invalidated by the filter pipeline. Defaults to
+ * (0, 0, 100, 100).
+ * @param aInputWriteRect The region in the space of the input image that the
+ * filter pipeline will allow writes to. Note the
+ * difference from @aInputRect: @aInputRect is the actual
+ * region invalidated, while @aInputWriteRect is the
+ * region that is written to. These can differ in cases
+ * where the input is not clipped to the size of the
+ * image. Defaults to the entire input rect.
+ * @param aOutputWriteRect The region in the space of the output surface that
+ * the filter pipeline will actually write to. It's
+ * expected that pixels in this region are green, while
+ * pixels outside this region are transparent. Defaults
+ * to the entire output rect.
+ */
+void CheckWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter,
+ const Maybe<gfx::IntRect>& aOutputRect = Nothing(),
+ const Maybe<gfx::IntRect>& aInputRect = Nothing(),
+ const Maybe<gfx::IntRect>& aInputWriteRect = Nothing(),
+ const Maybe<gfx::IntRect>& aOutputWriteRect = Nothing(),
+ uint8_t aFuzz = 0);
+
+/**
+ * Tests the result of calling WritePixels() using the provided SurfaceFilter
+ * pipeline. Allows for control over the input color to write, and the expected
+ * output color.
+ * @see CheckWritePixels() for documentation of the arguments.
+ */
+void CheckTransformedWritePixels(
+ Decoder* aDecoder, SurfaceFilter* aFilter, const BGRAColor& aInputColor,
+ const BGRAColor& aOutputColor,
+ const Maybe<gfx::IntRect>& aOutputRect = Nothing(),
+ const Maybe<gfx::IntRect>& aInputRect = Nothing(),
+ const Maybe<gfx::IntRect>& aInputWriteRect = Nothing(),
+ const Maybe<gfx::IntRect>& aOutputWriteRect = Nothing(), uint8_t aFuzz = 0);
+
+///////////////////////////////////////////////////////////////////////////////
+// Decoder Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+// Friend class of Decoder to access internals for tests.
+class MOZ_STACK_CLASS DecoderTestHelper final {
+ public:
+ explicit DecoderTestHelper(Decoder* aDecoder) : mDecoder(aDecoder) {}
+
+ void PostIsAnimated(FrameTimeout aTimeout) {
+ mDecoder->PostIsAnimated(aTimeout);
+ }
+
+ void PostFrameStop(Opacity aOpacity) { mDecoder->PostFrameStop(aOpacity); }
+
+ private:
+ Decoder* mDecoder;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Test Data
+///////////////////////////////////////////////////////////////////////////////
+
+ImageTestCase GreenPNGTestCase();
+ImageTestCase GreenGIFTestCase();
+ImageTestCase GreenJPGTestCase();
+ImageTestCase GreenBMPTestCase();
+ImageTestCase GreenICOTestCase();
+ImageTestCase GreenIconTestCase();
+ImageTestCase GreenWebPTestCase();
+ImageTestCase GreenAVIFTestCase();
+
+ImageTestCase StackCheckAVIFTestCase();
+
+ImageTestCase LargeWebPTestCase();
+ImageTestCase GreenWebPIccSrgbTestCase();
+
+ImageTestCase GreenFirstFrameAnimatedGIFTestCase();
+ImageTestCase GreenFirstFrameAnimatedPNGTestCase();
+ImageTestCase GreenFirstFrameAnimatedWebPTestCase();
+
+ImageTestCase BlendAnimatedGIFTestCase();
+ImageTestCase BlendAnimatedPNGTestCase();
+ImageTestCase BlendAnimatedWebPTestCase();
+
+ImageTestCase CorruptTestCase();
+ImageTestCase CorruptBMPWithTruncatedHeader();
+ImageTestCase CorruptICOWithBadBMPWidthTestCase();
+ImageTestCase CorruptICOWithBadBMPHeightTestCase();
+ImageTestCase CorruptICOWithBadBppTestCase();
+
+ImageTestCase TransparentPNGTestCase();
+ImageTestCase TransparentGIFTestCase();
+ImageTestCase TransparentWebPTestCase();
+ImageTestCase TransparentNoAlphaHeaderWebPTestCase();
+ImageTestCase FirstFramePaddingGIFTestCase();
+ImageTestCase TransparentIfWithinICOBMPTestCase(TestCaseFlags aFlags);
+ImageTestCase NoFrameDelayGIFTestCase();
+ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase();
+
+ImageTestCase TransparentBMPWhenBMPAlphaEnabledTestCase();
+ImageTestCase RLE4BMPTestCase();
+ImageTestCase RLE8BMPTestCase();
+
+ImageTestCase DownscaledPNGTestCase();
+ImageTestCase DownscaledGIFTestCase();
+ImageTestCase DownscaledJPGTestCase();
+ImageTestCase DownscaledBMPTestCase();
+ImageTestCase DownscaledICOTestCase();
+ImageTestCase DownscaledIconTestCase();
+ImageTestCase DownscaledWebPTestCase();
+ImageTestCase DownscaledTransparentICOWithANDMaskTestCase();
+
+ImageTestCase TruncatedSmallGIFTestCase();
+
+ImageTestCase LargeICOWithBMPTestCase();
+ImageTestCase LargeICOWithPNGTestCase();
+ImageTestCase GreenMultipleSizesICOTestCase();
+
+ImageTestCase PerfGrayJPGTestCase();
+ImageTestCase PerfCmykJPGTestCase();
+ImageTestCase PerfYCbCrJPGTestCase();
+ImageTestCase PerfRgbPNGTestCase();
+ImageTestCase PerfRgbAlphaPNGTestCase();
+ImageTestCase PerfGrayPNGTestCase();
+ImageTestCase PerfGrayAlphaPNGTestCase();
+ImageTestCase PerfRgbLosslessWebPTestCase();
+ImageTestCase PerfRgbAlphaLosslessWebPTestCase();
+ImageTestCase PerfRgbLossyWebPTestCase();
+ImageTestCase PerfRgbAlphaLossyWebPTestCase();
+ImageTestCase PerfRgbGIFTestCase();
+
+ImageTestCase CorruptAVIFTestCase();
+ImageTestCase DownscaledAVIFTestCase();
+ImageTestCase LargeAVIFTestCase();
+ImageTestCase MultiLayerAVIFTestCase();
+ImageTestCase TransparentAVIFTestCase();
+} // 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 0000000000..b2ae6b5a58
--- /dev/null
+++ b/image/test/gtest/TestADAM7InterpolatingFilter.cpp
@@ -0,0 +1,595 @@
+/* -*- 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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ WithFilterPipeline(
+ decoder, std::forward<Func>(aFunc), ADAM7InterpolatingConfig{},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false});
+}
+
+void AssertConfiguringADAM7InterpolatingFilterFails(const IntSize& aSize) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ AssertConfiguringPipelineFails(
+ decoder, ADAM7InterpolatingConfig{},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, 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 = 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(image::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 = 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, [&](image::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(image::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, [&](image::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, [&](image::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));
+}
diff --git a/image/test/gtest/TestAnimationFrameBuffer.cpp b/image/test/gtest/TestAnimationFrameBuffer.cpp
new file mode 100644
index 0000000000..63c6e588f9
--- /dev/null
+++ b/image/test/gtest/TestAnimationFrameBuffer.cpp
@@ -0,0 +1,896 @@
+/* -*- 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 <utility>
+
+#include "AnimationFrameBuffer.h"
+#include "Common.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+static already_AddRefed<imgFrame> CreateEmptyFrame(
+ const gfx::IntSize& aSize = gfx::IntSize(1, 1),
+ const gfx::IntRect& aFrameRect = gfx::IntRect(0, 0, 1, 1),
+ bool aCanRecycle = true) {
+ RefPtr<imgFrame> frame = new imgFrame();
+ AnimationParams animParams{aFrameRect, FrameTimeout::Forever(),
+ /* aFrameNum */ 1, BlendMethod::OVER,
+ DisposalMethod::NOT_SPECIFIED};
+ nsresult rv =
+ frame->InitForDecoder(aSize, mozilla::gfx::SurfaceFormat::OS_RGBA, false,
+ Some(animParams), aCanRecycle);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ RawAccessFrameRef frameRef = frame->RawAccessRef();
+ frame->SetRawAccessOnly();
+ // Normally the blend animation filter would set the dirty rect, but since
+ // we aren't producing an actual animation here, we need to fake it.
+ frame->SetDirtyRect(aFrameRect);
+ frame->Finish();
+ return frame.forget();
+}
+
+static bool ReinitForRecycle(RawAccessFrameRef& aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+
+ AnimationParams animParams{aFrame->GetRect(), FrameTimeout::Forever(),
+ /* aFrameNum */ 1, BlendMethod::OVER,
+ DisposalMethod::NOT_SPECIFIED};
+ return NS_SUCCEEDED(aFrame->InitForDecoderRecycle(animParams));
+}
+
+static void PrepareForDiscardingQueue(AnimationFrameRetainedBuffer& aQueue) {
+ ASSERT_EQ(size_t(0), aQueue.Size());
+ ASSERT_LT(size_t(1), aQueue.Batch());
+
+ AnimationFrameBuffer::InsertStatus status = aQueue.Insert(CreateEmptyFrame());
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ while (true) {
+ status = aQueue.Insert(CreateEmptyFrame());
+ bool restartDecoder = aQueue.AdvanceTo(aQueue.Size() - 1);
+ EXPECT_FALSE(restartDecoder);
+
+ if (status == AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE) {
+ break;
+ }
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+ }
+
+ EXPECT_EQ(aQueue.Threshold(), aQueue.Size());
+}
+
+static void VerifyDiscardingQueueContents(
+ AnimationFrameDiscardingQueue& aQueue) {
+ auto frames = aQueue.Display();
+ for (auto i : frames) {
+ EXPECT_TRUE(i != nullptr);
+ }
+}
+
+static void VerifyInsertInternal(AnimationFrameBuffer& aQueue,
+ imgFrame* aFrame) {
+ // Determine the frame index where we just inserted the frame.
+ size_t frameIndex;
+ if (aQueue.MayDiscard()) {
+ const AnimationFrameDiscardingQueue& queue =
+ *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
+ frameIndex = queue.PendingInsert() == 0 ? queue.Size() - 1
+ : queue.PendingInsert() - 1;
+ } else {
+ ASSERT_FALSE(aQueue.SizeKnown());
+ frameIndex = aQueue.Size() - 1;
+ }
+
+ // Make sure we can get the frame from that index.
+ RefPtr<imgFrame> frame = aQueue.Get(frameIndex, false);
+ EXPECT_EQ(aFrame, frame.get());
+}
+
+static void VerifyAdvance(AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
+ bool aExpectedRestartDecoder) {
+ RefPtr<imgFrame> oldFrame;
+ size_t totalRecycled;
+ if (aQueue.IsRecycling()) {
+ AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ oldFrame = queue.Get(queue.Displayed(), false);
+ totalRecycled = queue.Recycle().size();
+ }
+
+ bool restartDecoder = aQueue.AdvanceTo(aExpectedFrame);
+ EXPECT_EQ(aExpectedRestartDecoder, restartDecoder);
+
+ if (aQueue.IsRecycling()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_FALSE(queue.Recycle().back().mDirtyRect.IsEmpty());
+ EXPECT_TRUE(
+ queue.Recycle().back().mDirtyRect.Contains(oldFrame->GetDirtyRect()));
+ EXPECT_EQ(totalRecycled + 1, queue.Recycle().size());
+ EXPECT_EQ(oldFrame.get(), queue.Recycle().back().mFrame.get());
+ }
+}
+
+static void VerifyInsertAndAdvance(
+ AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
+ AnimationFrameBuffer::InsertStatus aExpectedStatus) {
+ // Insert the decoded frame.
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status =
+ aQueue.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(aExpectedStatus, status);
+ EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
+ VerifyInsertInternal(aQueue, frame);
+
+ // Advance the display frame.
+ bool expectedRestartDecoder =
+ aExpectedStatus == AnimationFrameBuffer::InsertStatus::YIELD;
+ VerifyAdvance(aQueue, aExpectedFrame, expectedRestartDecoder);
+}
+
+static void VerifyMarkComplete(
+ AnimationFrameBuffer& aQueue, bool aExpectedContinue,
+ const gfx::IntRect& aRefreshArea = gfx::IntRect(0, 0, 1, 1)) {
+ if (aQueue.IsRecycling() && !aQueue.SizeKnown()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_EQ(queue.FirstFrame()->GetRect(), queue.FirstFrameRefreshArea());
+ }
+
+ bool keepDecoding = aQueue.MarkComplete(aRefreshArea);
+ EXPECT_EQ(aExpectedContinue, keepDecoding);
+
+ if (aQueue.IsRecycling()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_EQ(aRefreshArea, queue.FirstFrameRefreshArea());
+ }
+}
+
+static void VerifyInsert(AnimationFrameBuffer& aQueue,
+ AnimationFrameBuffer::InsertStatus aExpectedStatus) {
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status =
+ aQueue.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(aExpectedStatus, status);
+ EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
+ VerifyInsertInternal(aQueue, frame);
+}
+
+static void VerifyReset(AnimationFrameBuffer& aQueue, bool aExpectedContinue,
+ const imgFrame* aFirstFrame) {
+ bool keepDecoding = aQueue.Reset();
+ EXPECT_EQ(aExpectedContinue, keepDecoding);
+ EXPECT_EQ(aQueue.Batch() * 2, aQueue.PendingDecode());
+ EXPECT_EQ(aFirstFrame, aQueue.Get(0, true));
+
+ if (!aQueue.MayDiscard()) {
+ const AnimationFrameRetainedBuffer& queue =
+ *static_cast<AnimationFrameRetainedBuffer*>(&aQueue);
+ EXPECT_EQ(aFirstFrame, queue.Frames()[0].get());
+ EXPECT_EQ(aFirstFrame, aQueue.Get(0, false));
+ } else {
+ const AnimationFrameDiscardingQueue& queue =
+ *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
+ EXPECT_EQ(size_t(0), queue.PendingInsert());
+ EXPECT_EQ(size_t(0), queue.Display().size());
+ EXPECT_EQ(aFirstFrame, queue.FirstFrame());
+ EXPECT_EQ(nullptr, aQueue.Get(0, false));
+ }
+}
+
+class ImageAnimationFrameBuffer : public ::testing::Test {
+ public:
+ ImageAnimationFrameBuffer() {}
+
+ private:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageAnimationFrameBuffer, RetainedInitialState) {
+ const size_t kThreshold = 800;
+ const size_t kBatch = 100;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kThreshold, buffer.Threshold());
+ EXPECT_EQ(kBatch, buffer.Batch());
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_FALSE(buffer.MayDiscard());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, ThresholdTooSmall) {
+ const size_t kThreshold = 0;
+ const size_t kBatch = 10;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kBatch * 2 + 1, buffer.Threshold());
+ EXPECT_EQ(kBatch, buffer.Batch());
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, BatchTooSmall) {
+ const size_t kThreshold = 10;
+ const size_t kBatch = 0;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kThreshold, buffer.Threshold());
+ EXPECT_EQ(size_t(1), buffer.Batch());
+ EXPECT_EQ(size_t(2), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, BatchTooBig) {
+ const size_t kThreshold = 50;
+ const size_t kBatch = SIZE_MAX;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ // The rounding is important here (e.g. SIZE_MAX/4 * 2 != SIZE_MAX/2).
+ EXPECT_EQ(SIZE_MAX / 4, buffer.Batch());
+ EXPECT_EQ(buffer.Batch() * 2 + 1, buffer.Threshold());
+ EXPECT_EQ(buffer.Batch() * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, FinishUnderBatchAndThreshold) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 10;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+ const auto& frames = buffer.Frames();
+
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+
+ RefPtr<imgFrame> firstFrame;
+ for (size_t i = 0; i < 5; ++i) {
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ auto status = buffer.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_EQ(buffer.Size(), i + 1);
+
+ if (i == 4) {
+ EXPECT_EQ(size_t(15), buffer.PendingDecode());
+ bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(keepDecoding);
+ EXPECT_TRUE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_FALSE(buffer.HasRedecodeError());
+ }
+
+ EXPECT_FALSE(buffer.MayDiscard());
+
+ imgFrame* gotFrame = buffer.Get(i, false);
+ EXPECT_EQ(frame.get(), gotFrame);
+ ASSERT_EQ(i + 1, frames.Length());
+ EXPECT_EQ(frame.get(), frames[i].get());
+
+ if (i == 0) {
+ firstFrame = std::move(frame);
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ } else {
+ EXPECT_EQ(i - 1, buffer.Displayed());
+ bool restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_EQ(i, buffer.Displayed());
+ }
+
+ gotFrame = buffer.Get(0, false);
+ EXPECT_EQ(firstFrame.get(), gotFrame);
+ }
+
+ // Loop again over the animation and make sure it is still all there.
+ for (size_t i = 0; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+
+ bool restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+}
+
+TEST_F(ImageAnimationFrameBuffer, FinishMultipleBatchesUnderThreshold) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+ const auto& frames = buffer.Frames();
+
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+
+ // Add frames until it tells us to stop.
+ AnimationFrameBuffer::InsertStatus status;
+ do {
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+ } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);
+
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), frames.Length());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Progress through the animation until it lets us decode again.
+ bool restartDecoder = false;
+ size_t i = 0;
+ do {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ if (i > 0) {
+ restartDecoder = buffer.AdvanceTo(i);
+ }
+ ++i;
+ } while (!restartDecoder);
+
+ EXPECT_EQ(size_t(2), buffer.PendingDecode());
+ EXPECT_EQ(size_t(2), buffer.Displayed());
+
+ // Add the last frame.
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(keepDecoding);
+ EXPECT_TRUE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(5), frames.Length());
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Finish progressing through the animation.
+ for (; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Loop again over the animation and make sure it is still all there.
+ for (i = 0; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Loop to the third frame and then reset the animation.
+ for (i = 0; i < 3; ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Since we are below the threshold, we can reset the get index only.
+ // Nothing else should have changed.
+ restartDecoder = buffer.Reset();
+ EXPECT_FALSE(restartDecoder);
+ for (i = 0; i < 5; ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ }
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+}
+
+TEST_F(ImageAnimationFrameBuffer, StartAfterBeginning) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 7;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);
+
+ EXPECT_EQ(kStartFrame, buffer.PendingAdvance());
+
+ // Add frames until it tells us to stop. It should be later than before,
+ // because it auto-advances until its displayed frame is kStartFrame.
+ AnimationFrameBuffer::InsertStatus status;
+ size_t i = 0;
+ do {
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+
+ if (i <= kStartFrame) {
+ EXPECT_EQ(i, buffer.Displayed());
+ EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
+ } else {
+ EXPECT_EQ(kStartFrame, buffer.Displayed());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ }
+
+ i++;
+ } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);
+
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(10), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, StartAfterBeginningAndReset) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 7;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);
+
+ EXPECT_EQ(kStartFrame, buffer.PendingAdvance());
+
+ // Add frames until it tells us to stop. It should be later than before,
+ // because it auto-advances until its displayed frame is kStartFrame.
+ for (size_t i = 0; i < 5; ++i) {
+ AnimationFrameBuffer::InsertStatus status =
+ buffer.Insert(CreateEmptyFrame());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+ EXPECT_EQ(i, buffer.Displayed());
+ EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
+ }
+
+ // When we reset the animation, it goes back to the beginning. That means
+ // we can forget about what we were told to advance to at the start. While
+ // we have plenty of frames in our buffer, we still need one more because
+ // in the real scenario, the decoder thread is still running and it is easier
+ // to let it insert its last frame than to coordinate quitting earlier.
+ buffer.Reset();
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ EXPECT_EQ(size_t(1), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(5), buffer.Size());
+}
+
+static void TestDiscardingQueueLoop(AnimationFrameDiscardingQueue& aQueue,
+ const imgFrame* aFirstFrame,
+ size_t aThreshold, size_t aBatch,
+ size_t aStartFrame) {
+ // We should be advanced right up to the last decoded frame.
+ EXPECT_TRUE(aQueue.MayDiscard());
+ EXPECT_FALSE(aQueue.SizeKnown());
+ EXPECT_EQ(aBatch, aQueue.Batch());
+ EXPECT_EQ(aThreshold, aQueue.PendingInsert());
+ EXPECT_EQ(aThreshold, aQueue.Size());
+ EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ EXPECT_EQ(size_t(3), aQueue.PendingDecode());
+ VerifyDiscardingQueueContents(aQueue);
+
+ // Make sure frames get removed as we advance.
+ VerifyInsertAndAdvance(aQueue, 5,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ VerifyInsertAndAdvance(aQueue, 6,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+
+ // We actually will yield if we are recycling instead of continuing because
+ // the pending calculation is slightly different. We will actually request one
+ // less frame than we have to recycle.
+ if (aQueue.IsRecycling()) {
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ } else {
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ }
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+
+ // We should get throttled if we insert too much.
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(2), aQueue.Display().size());
+ EXPECT_EQ(size_t(1), aQueue.PendingDecode());
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
+ EXPECT_EQ(size_t(3), aQueue.Display().size());
+ EXPECT_EQ(size_t(0), aQueue.PendingDecode());
+
+ // We should get restarted if we advance.
+ VerifyAdvance(aQueue, 8, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ VerifyAdvance(aQueue, 9, false);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+
+ // We should continue decoding if we completed, since we are discarding.
+ VerifyMarkComplete(aQueue, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ EXPECT_EQ(size_t(10), aQueue.Size());
+ EXPECT_TRUE(aQueue.SizeKnown());
+ EXPECT_FALSE(aQueue.HasRedecodeError());
+
+ // Insert the first frames of the animation.
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
+ EXPECT_EQ(size_t(0), aQueue.PendingDecode());
+ EXPECT_EQ(size_t(10), aQueue.Size());
+
+ // Advance back at the beginning. The first frame should only match for
+ // display purposes.
+ VerifyAdvance(aQueue, 0, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ EXPECT_TRUE(aQueue.FirstFrame() != nullptr);
+ EXPECT_TRUE(aQueue.Get(0, false) != nullptr);
+ EXPECT_NE(aQueue.FirstFrame(), aQueue.Get(0, false));
+ EXPECT_EQ(aQueue.FirstFrame(), aQueue.Get(0, true));
+
+ // Reiterate one more time and make it loops back.
+ VerifyInsertAndAdvance(aQueue, 1,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 3,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 4, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 5,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 6, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 8, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ EXPECT_EQ(size_t(10), aQueue.PendingInsert());
+ VerifyMarkComplete(aQueue, true);
+ EXPECT_EQ(size_t(0), aQueue.PendingInsert());
+
+ VerifyInsertAndAdvance(aQueue, 9,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 1,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingLoop) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingLoop) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+
+ // We should not start with any recycled frames.
+ ASSERT_TRUE(buffer.Recycle().empty());
+
+ TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+
+ // All the frames we inserted should have been recycleable.
+ ASSERT_FALSE(buffer.Recycle().empty());
+ while (!buffer.Recycle().empty()) {
+ gfx::IntRect expectedRect(0, 0, 1, 1);
+ RefPtr<imgFrame> expectedFrame = buffer.Recycle().front().mFrame;
+ EXPECT_FALSE(expectedRect.IsEmpty());
+ EXPECT_TRUE(expectedFrame.get() != nullptr);
+
+ gfx::IntRect gotRect;
+ RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
+ EXPECT_EQ(expectedFrame.get(), gotFrame.get());
+ EXPECT_EQ(expectedRect, gotRect);
+ EXPECT_TRUE(ReinitForRecycle(gotFrame));
+ }
+
+ // Trying to pull a recycled frame when we have nothing should be safe too.
+ gfx::IntRect gotRect;
+ RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
+ EXPECT_TRUE(gotFrame.get() == nullptr);
+ EXPECT_FALSE(ReinitForRecycle(gotFrame));
+}
+
+static void TestDiscardingQueueReset(AnimationFrameDiscardingQueue& aQueue,
+ const imgFrame* aFirstFrame,
+ size_t aThreshold, size_t aBatch,
+ size_t aStartFrame) {
+ // We should be advanced right up to the last decoded frame.
+ EXPECT_TRUE(aQueue.MayDiscard());
+ EXPECT_FALSE(aQueue.SizeKnown());
+ EXPECT_EQ(aBatch, aQueue.Batch());
+ EXPECT_EQ(aThreshold, aQueue.PendingInsert());
+ EXPECT_EQ(aThreshold, aQueue.Size());
+ EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ EXPECT_EQ(size_t(4), aQueue.PendingDecode());
+ VerifyDiscardingQueueContents(aQueue);
+
+ // Reset should clear everything except the first frame.
+ VerifyReset(aQueue, false, aFirstFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingReset) {
+ const size_t kThreshold = 8;
+ const size_t kBatch = 3;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, ResetBeforeDiscardingThreshold) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // Get the starting buffer to just before the point where we need to switch
+ // to a discarding buffer, reset the animation so advancing points at the
+ // first frame, and insert the last frame to cross the threshold.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ bool restartDecoder = retained.Reset();
+ EXPECT_FALSE(restartDecoder);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ EXPECT_TRUE(firstFrame != nullptr);
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ const imgFrame* displayFirstFrame = buffer.Get(0, true);
+ const imgFrame* advanceFirstFrame = buffer.Get(0, false);
+ EXPECT_EQ(firstFrame, displayFirstFrame);
+ EXPECT_EQ(firstFrame, advanceFirstFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingTooFewFrames) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // First get us to a discarding buffer state.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ // Insert one more frame.
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ VerifyAdvance(buffer, 2, true);
+ VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Mark it as complete.
+ bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Insert one fewer frame than before.
+ VerifyAdvance(buffer, 3, true);
+ VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // When we mark it as complete, it should fail due to too few frames.
+ restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_TRUE(buffer.HasRedecodeError());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingTooManyFrames) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // First get us to a discarding buffer state.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ // Insert one more frame.
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ VerifyAdvance(buffer, 2, true);
+ VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Mark it as complete.
+ bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Advance and insert to get us back to the end on the redecode.
+ VerifyAdvance(buffer, 3, true);
+ VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 3, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Attempt to insert a 5th frame, it should fail.
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+ EXPECT_TRUE(buffer.HasRedecodeError());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingReset) {
+ const size_t kThreshold = 8;
+ const size_t kBatch = 3;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+ TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingResetBeforeComplete) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+ const gfx::IntSize kImageSize(100, 100);
+ const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+
+ // Get the starting buffer to just before the point where we need to switch
+ // to a discarding buffer, reset the animation so advancing points at the
+ // first frame, and insert the last frame to cross the threshold.
+ RefPtr<imgFrame> frame;
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
+ false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(retained, 1, true);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(20, 10), gfx::IntSize(1, 1)),
+ false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_YIELD, status);
+
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+ bool restartDecoding = buffer.Reset();
+ EXPECT_TRUE(restartDecoding);
+
+ // None of the buffers were recyclable.
+ EXPECT_FALSE(buffer.Recycle().empty());
+ while (!buffer.Recycle().empty()) {
+ gfx::IntRect recycleRect;
+ RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ }
+
+ // Reinsert the first two frames as recyclable and reset again.
+ frame = CreateEmptyFrame(kImageSize, kImageRect, true);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
+ true);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ restartDecoding = buffer.Reset();
+ EXPECT_TRUE(restartDecoding);
+
+ // Now both buffers should have been saved and the dirty rect replaced with
+ // the full image rect since we don't know the first frame refresh area yet.
+ EXPECT_EQ(size_t(2), buffer.Recycle().size());
+ for (const auto& entry : buffer.Recycle()) {
+ EXPECT_EQ(kImageRect, entry.mDirtyRect);
+ }
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingRect) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ const gfx::IntSize kImageSize(100, 100);
+ const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+
+ // Let's get to the recycling state while marking all of the frames as not
+ // recyclable, just like AnimationFrameBuffer / the decoders would do.
+ RefPtr<imgFrame> frame;
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(retained, 1, false);
+ VerifyAdvance(retained, 2, true);
+ VerifyAdvance(retained, 3, false);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE, status);
+
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+
+ // The first frame is now the candidate for recycling. Since it was marked as
+ // not recyclable, we should get nothing.
+ VerifyAdvance(buffer, 4, false);
+
+ gfx::IntRect recycleRect;
+ EXPECT_FALSE(buffer.Recycle().empty());
+ RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert a recyclable partial frame. Its dirty rect shouldn't matter since
+ // the previous frame was not recyclable.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(0, 0, 25, 25));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 5, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert a recyclable partial frame. Its dirty rect should match the recycle
+ // rect since it is the only frame in the buffer.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(25, 0, 50, 50));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 6, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(25, 0, 50, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert the last frame and mark us as complete. The next recycled frame is
+ // producing the first frame again, so we should use the first frame refresh
+ // area instead of its dirty rect.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(10, 10, 60, 10));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ bool continueDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 75, 50));
+ EXPECT_FALSE(continueDecoding);
+
+ VerifyAdvance(buffer, 7, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Now let's reinsert the first frame. The recycle rect should still be the
+ // first frame refresh area instead of the dirty rect of the first frame (e.g.
+ // the full frame).
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 0, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+}
diff --git a/image/test/gtest/TestBlendAnimationFilter.cpp b/image/test/gtest/TestBlendAnimationFilter.cpp
new file mode 100644
index 0000000000..7291fbc3f6
--- /dev/null
+++ b/image/test/gtest/TestBlendAnimationFilter.cpp
@@ -0,0 +1,450 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/gfx/2D.h"
+#include "skia/include/core/SkColorPriv.h" // for SkPMSrcOver
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfaceFilters.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+static already_AddRefed<image::Decoder> CreateTrivialBlendingDecoder() {
+ DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+ DecoderFlags decoderFlags = DefaultDecoderFlags();
+ SurfaceFlags surfaceFlags = DefaultSurfaceFlags();
+ auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+ return DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, Nothing(), decoderFlags, surfaceFlags);
+}
+
+template <typename Func>
+RawAccessFrameRef WithBlendAnimationFilter(image::Decoder* aDecoder,
+ const AnimationParams& aAnimParams,
+ const IntSize& aOutputSize,
+ Func aFunc) {
+ DecoderTestHelper decoderHelper(aDecoder);
+
+ if (!aDecoder->HasAnimation()) {
+ decoderHelper.PostIsAnimated(aAnimParams.mTimeout);
+ }
+
+ BlendAnimationConfig blendAnim{aDecoder};
+ SurfaceConfig surfaceSink{aDecoder, aOutputSize, SurfaceFormat::OS_RGBA,
+ false, Some(aAnimParams)};
+
+ auto func = [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ aFunc(aDecoder, aFilter);
+ };
+
+ WithFilterPipeline(aDecoder, func, false, blendAnim, surfaceSink);
+
+ RawAccessFrameRef current = aDecoder->GetCurrentFrameRef();
+ if (current) {
+ decoderHelper.PostFrameStop(Opacity::SOME_TRANSPARENCY);
+ }
+
+ return current;
+}
+
+void AssertConfiguringBlendAnimationFilterFails(const IntRect& aFrameRect,
+ const IntSize& aOutputSize) {
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams animParams{aFrameRect, FrameTimeout::FromRawMilliseconds(0),
+ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BlendAnimationConfig blendAnim{decoder};
+ SurfaceConfig surfaceSink{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false,
+ Some(animParams)};
+ AssertConfiguringPipelineFails(decoder, blendAnim, surfaceSink);
+}
+
+TEST(ImageBlendAnimationFilter, BlendFailsForNegativeFrameRect)
+{
+ // A negative frame rect size is disallowed.
+ AssertConfiguringBlendAnimationFilterFails(
+ IntRect(IntPoint(0, 0), IntSize(-1, -1)), IntSize(100, 100));
+}
+
+TEST(ImageBlendAnimationFilter, WriteFullFirstFrame)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)));
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+TEST(ImageBlendAnimationFilter, WritePartialFirstFrame)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params{
+ IntRect(25, 50, 50, 25), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)),
+ Nothing(), Some(IntRect(25, 50, 50, 25)),
+ Some(IntRect(25, 50, 50, 25)));
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+static void TestWithBlendAnimationFilterClear(BlendMethod aBlendMethod) {
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(BGRAColor::Green().AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::CLEAR};
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(BGRAColor::Red().AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+ ASSERT_TRUE(frame1.get() != nullptr);
+
+ RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+
+ AnimationParams params2{
+ IntRect(0, 50, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 2, aBlendMethod, DisposalMethod::KEEP};
+ RawAccessFrameRef frame2 = WithBlendAnimationFilter(
+ decoder, params2, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(BGRAColor::Blue().AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+
+ ASSERT_TRUE(frame2.get() != nullptr);
+
+ surface = frame2->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 10, BGRAColor::Transparent()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 50, 20, BGRAColor::Blue()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 70, 30, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, ClearWithOver)
+{ TestWithBlendAnimationFilterClear(BlendMethod::OVER); }
+
+TEST(ImageBlendAnimationFilter, ClearWithSource)
+{ TestWithBlendAnimationFilterClear(BlendMethod::SOURCE); }
+
+TEST(ImageBlendAnimationFilter, KeepWithSource)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(BGRAColor::Green().AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(BGRAColor::Red().AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+ ASSERT_TRUE(frame1.get() != nullptr);
+
+ RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, KeepWithOver)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor0.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::OVER, DisposalMethod::KEEP};
+ BGRAColor frameColor1(0, 0, 0xFF, 0x80);
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor1.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+ ASSERT_TRUE(frame1.get() != nullptr);
+
+ BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+ EXPECT_EQ(SkPMSrcOver(frameColor1.AsPixel(), frameColor0.AsPixel()),
+ blendedColor.AsPixel());
+
+ RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithOver)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor0.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS};
+ BGRAColor frameColor1 = BGRAColor::Green();
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor1.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+ AnimationParams params2{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 2, BlendMethod::OVER, DisposalMethod::KEEP};
+ BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+ RawAccessFrameRef frame2 = WithBlendAnimationFilter(
+ decoder, params2, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor2.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+ ASSERT_TRUE(frame2.get() != nullptr);
+
+ BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+ EXPECT_EQ(SkPMSrcOver(frameColor2.AsPixel(), frameColor0.AsPixel()),
+ blendedColor.AsPixel());
+
+ RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithSource)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor0.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS};
+ BGRAColor frameColor1 = BGRAColor::Green();
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor1.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+ AnimationParams params2{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 2, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+ RawAccessFrameRef frame2 = WithBlendAnimationFilter(
+ decoder, params2, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor2.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+ ASSERT_TRUE(frame2.get() != nullptr);
+
+ RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor2));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousClearWithSource)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(0, 0, 100, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor0 = BGRAColor::Red();
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor0.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ AnimationParams params1{
+ IntRect(0, 0, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 1, BlendMethod::SOURCE, DisposalMethod::CLEAR};
+ BGRAColor frameColor1 = BGRAColor::Blue();
+ RawAccessFrameRef frame1 = WithBlendAnimationFilter(
+ decoder, params1, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor1.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 20), frame1->GetDirtyRect());
+
+ AnimationParams params2{
+ IntRect(0, 10, 100, 80), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 2, BlendMethod::SOURCE, DisposalMethod::RESTORE_PREVIOUS};
+ BGRAColor frameColor2 = BGRAColor::Green();
+ RawAccessFrameRef frame2 = WithBlendAnimationFilter(
+ decoder, params2, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor2.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 90), frame2->GetDirtyRect());
+
+ AnimationParams params3{
+ IntRect(0, 40, 100, 20), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 3, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor3 = BGRAColor::Blue();
+ RawAccessFrameRef frame3 = WithBlendAnimationFilter(
+ decoder, params3, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor3.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 90), frame3->GetDirtyRect());
+
+ ASSERT_TRUE(frame3.get() != nullptr);
+
+ RefPtr<SourceSurface> surface = frame3->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Transparent()));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 20, 20, frameColor0));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor3));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, PartialOverlapFrameRect)
+{
+ RefPtr<image::Decoder> decoder = CreateTrivialBlendingDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AnimationParams params0{
+ IntRect(-10, -20, 110, 100), FrameTimeout::FromRawMilliseconds(0),
+ /* aFrameNum */ 0, BlendMethod::SOURCE, DisposalMethod::KEEP};
+ BGRAColor frameColor0 = BGRAColor::Red();
+ RawAccessFrameRef frame0 = WithBlendAnimationFilter(
+ decoder, params0, IntSize(100, 100),
+ [&](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ auto result = aFilter->WritePixels<uint32_t>(
+ [&] { return AsVariant(frameColor0.AsPixel()); });
+ EXPECT_EQ(WriteState::FINISHED, result);
+ });
+ EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+ RefPtr<SourceSurface> surface = frame0->GetSourceSurface();
+ EXPECT_TRUE(RowsAreSolidColor(surface, 0, 80, frameColor0));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Transparent()));
+}
diff --git a/image/test/gtest/TestContainers.cpp b/image/test/gtest/TestContainers.cpp
new file mode 100644
index 0000000000..98326e8764
--- /dev/null
+++ b/image/test/gtest/TestContainers.cpp
@@ -0,0 +1,105 @@
+/* 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 "BasicLayers.h"
+#include "Common.h"
+#include "imgIContainer.h"
+#include "ImageFactory.h"
+#include "ImageContainer.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/RefPtr.h"
+#include "nsIInputStream.h"
+#include "nsString.h"
+#include "ProgressTracker.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+class ImageContainers : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageContainers, RasterImageContainer) {
+ ImageTestCase testCase = GreenPNGTestCase();
+
+ // 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);
+
+ RefPtr<layers::LayerManager> layerManager =
+ new layers::BasicLayerManager(layers::BasicLayerManager::BLM_OFFSCREEN);
+
+ // Get at native size.
+ RefPtr<layers::ImageContainer> nativeContainer =
+ image->GetImageContainer(layerManager, imgIContainer::FLAG_SYNC_DECODE);
+ ASSERT_TRUE(nativeContainer != nullptr);
+ IntSize containerSize = nativeContainer->GetCurrentSize();
+ EXPECT_EQ(testCase.mSize.width, containerSize.width);
+ EXPECT_EQ(testCase.mSize.height, containerSize.height);
+
+ // Upscaling should give the native size.
+ ImgDrawResult drawResult;
+ IntSize requestedSize = testCase.mSize;
+ requestedSize.Scale(2, 2);
+ RefPtr<layers::ImageContainer> upscaleContainer;
+ drawResult = image->GetImageContainerAtSize(
+ layerManager, requestedSize, Nothing(),
+ imgIContainer::FLAG_SYNC_DECODE |
+ imgIContainer::FLAG_HIGH_QUALITY_SCALING,
+ getter_AddRefs(upscaleContainer));
+ EXPECT_EQ(drawResult, ImgDrawResult::SUCCESS);
+ ASSERT_TRUE(upscaleContainer != nullptr);
+ containerSize = upscaleContainer->GetCurrentSize();
+ EXPECT_EQ(testCase.mSize.width, containerSize.width);
+ EXPECT_EQ(testCase.mSize.height, containerSize.height);
+
+ // Downscaling should give the downscaled size.
+ requestedSize = testCase.mSize;
+ requestedSize.width /= 2;
+ requestedSize.height /= 2;
+ RefPtr<layers::ImageContainer> downscaleContainer;
+ drawResult = image->GetImageContainerAtSize(
+ layerManager, requestedSize, Nothing(),
+ imgIContainer::FLAG_SYNC_DECODE |
+ imgIContainer::FLAG_HIGH_QUALITY_SCALING,
+ getter_AddRefs(downscaleContainer));
+ EXPECT_EQ(drawResult, ImgDrawResult::SUCCESS);
+ ASSERT_TRUE(downscaleContainer != nullptr);
+ containerSize = downscaleContainer->GetCurrentSize();
+ EXPECT_EQ(requestedSize.width, containerSize.width);
+ EXPECT_EQ(requestedSize.height, containerSize.height);
+
+ // Get at native size again. Should give same container.
+ RefPtr<layers::ImageContainer> againContainer;
+ drawResult = image->GetImageContainerAtSize(
+ layerManager, testCase.mSize, Nothing(), imgIContainer::FLAG_SYNC_DECODE,
+ getter_AddRefs(againContainer));
+ EXPECT_EQ(drawResult, ImgDrawResult::SUCCESS);
+ ASSERT_EQ(nativeContainer.get(), againContainer.get());
+}
diff --git a/image/test/gtest/TestCopyOnWrite.cpp b/image/test/gtest/TestCopyOnWrite.cpp
new file mode 100644
index 0000000000..d5ad3e4f6b
--- /dev/null
+++ b/image/test/gtest/TestCopyOnWrite.cpp
@@ -0,0 +1,237 @@
+/* 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 0000000000..0ab9edd667
--- /dev/null
+++ b/image/test/gtest/TestDecodeToSurface.cpp
@@ -0,0 +1,171 @@
+/* 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 "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,
+ ImageOps::ImageBuffer* aImageBuffer,
+ const ImageTestCase& aTestCase)
+ : mozilla::Runnable("DecodeToSurfaceRunnable"),
+ mSurface(aSurface),
+ mInputStream(aInputStream),
+ mImageBuffer(aImageBuffer),
+ mTestCase(aTestCase) {}
+
+ NS_IMETHOD Run() override {
+ Go();
+ return NS_OK;
+ }
+
+ void Go() {
+ Maybe<IntSize> outputSize;
+ if (mTestCase.mOutputSize != mTestCase.mSize) {
+ outputSize.emplace(mTestCase.mOutputSize);
+ }
+
+ uint32_t flags = FromSurfaceFlags(mTestCase.mSurfaceFlags);
+
+ if (mImageBuffer) {
+ mSurface = ImageOps::DecodeToSurface(
+ mImageBuffer, nsDependentCString(mTestCase.mMimeType), flags,
+ outputSize);
+ } else {
+ mSurface = ImageOps::DecodeToSurface(
+ mInputStream.forget(), nsDependentCString(mTestCase.mMimeType), flags,
+ outputSize);
+ }
+ ASSERT_TRUE(mSurface != nullptr);
+
+ EXPECT_TRUE(mSurface->IsDataSourceSurface());
+ EXPECT_TRUE(mSurface->GetFormat() == SurfaceFormat::OS_RGBX ||
+ mSurface->GetFormat() == SurfaceFormat::OS_RGBA);
+
+ if (outputSize) {
+ EXPECT_EQ(*outputSize, mSurface->GetSize());
+ } else {
+ EXPECT_EQ(mTestCase.mSize, mSurface->GetSize());
+ }
+
+ EXPECT_TRUE(IsSolidColor(mSurface, mTestCase.Color(), mTestCase.Fuzz()));
+ }
+
+ private:
+ RefPtr<SourceSurface>& mSurface;
+ nsCOMPtr<nsIInputStream> mInputStream;
+ RefPtr<ImageOps::ImageBuffer> mImageBuffer;
+ ImageTestCase mTestCase;
+};
+
+static void RunDecodeToSurface(const ImageTestCase& aTestCase,
+ ImageOps::ImageBuffer* aImageBuffer = nullptr) {
+ nsCOMPtr<nsIInputStream> inputStream;
+ if (!aImageBuffer) {
+ inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+ }
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv =
+ NS_NewNamedThread("DecodeToSurface", 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, aImageBuffer, 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, WebP) { RunDecodeToSurface(GreenWebPTestCase()); }
+
+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.forget(), nsDependentCString(testCase.mMimeType),
+ imgIContainer::DECODE_FLAGS_DEFAULT);
+ EXPECT_TRUE(surface == nullptr);
+}
+
+TEST_F(ImageDecodeToSurface, ICOMultipleSizes) {
+ ImageTestCase testCase = GreenMultipleSizesICOTestCase();
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ RefPtr<ImageOps::ImageBuffer> buffer =
+ ImageOps::CreateImageBuffer(inputStream.forget());
+ ASSERT_TRUE(buffer != nullptr);
+
+ ImageMetadata metadata;
+ nsresult rv = ImageOps::DecodeMetadata(
+ buffer, nsDependentCString(testCase.mMimeType), metadata);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(metadata.HasSize());
+ EXPECT_EQ(testCase.mSize, metadata.GetSize());
+
+ const nsTArray<IntSize>& nativeSizes = metadata.GetNativeSizes();
+ ASSERT_EQ(6u, nativeSizes.Length());
+
+ IntSize expectedSizes[] = {
+ IntSize(16, 16), IntSize(32, 32), IntSize(64, 64),
+ IntSize(128, 128), IntSize(256, 256), IntSize(256, 128),
+ };
+
+ for (int i = 0; i < 6; ++i) {
+ EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+
+ // Request decoding at native size
+ testCase.mOutputSize = nativeSizes[i];
+ RunDecodeToSurface(testCase, buffer);
+ }
+}
diff --git a/image/test/gtest/TestDecoders.cpp b/image/test/gtest/TestDecoders.cpp
new file mode 100644
index 0000000000..eb369df028
--- /dev/null
+++ b/image/test/gtest/TestDecoders.cpp
@@ -0,0 +1,994 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "Common.h"
+#include "AnimationSurfaceProvider.h"
+#include "DecodePool.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "decoders/nsBMPDecoder.h"
+#include "IDecodingTask.h"
+#include "ImageOps.h"
+#include "imgIContainer.h"
+#include "ImageFactory.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.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, image::Decoder* aDecoder) {
+ // image::Decoder should match what we asked for in the MIME type.
+ EXPECT_NE(aDecoder->GetType(), DecoderType::UNKNOWN);
+ EXPECT_EQ(aDecoder->GetType(),
+ DecoderFactory::GetDecoderType(aTestCase.mMimeType));
+
+ 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_TRUE(surface->IsDataSourceSurface());
+ EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX ||
+ surface->GetFormat() == SurfaceFormat::OS_RGBA);
+ EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize());
+
+ return surface.forget();
+}
+
+static void CheckDecoderResults(const ImageTestCase& aTestCase,
+ image::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, aTestCase.Color(), aTestCase.Fuzz()));
+}
+
+template <typename Func>
+void WithBadBufferDecode(const ImageTestCase& aTestCase,
+ const Maybe<IntSize>& aOutputSize,
+ Func aResultChecker) {
+ // Prepare a SourceBuffer with an error that will immediately move iterators
+ // to COMPLETE.
+ auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+ sourceBuffer->ExpectLength(SIZE_MAX);
+
+ // Create a decoder.
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false);
+
+ // Run the full decoder synchronously on the main thread.
+ task->Run();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(decoder);
+}
+
+static void CheckDecoderBadBuffer(const ImageTestCase& aTestCase) {
+ WithBadBufferDecode(aTestCase, Nothing(), [&](image::Decoder* aDecoder) {
+ CheckDecoderResults(aTestCase, aDecoder);
+ });
+}
+
+template <typename Func>
+void WithSingleChunkDecode(const ImageTestCase& aTestCase,
+ const Maybe<IntSize>& aOutputSize,
+ bool aUseDecodePool, 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.
+ auto sourceBuffer = MakeNotNull<RefPtr<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<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false);
+
+ if (aUseDecodePool) {
+ DecodePool::Singleton()->AsyncRun(task.get());
+
+ while (!decoder->GetDecodeDone()) {
+ task->Resume();
+ }
+ } else { // Run the full decoder synchronously on the main thread.
+ task->Run();
+ }
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(decoder);
+}
+
+static void CheckDecoderSingleChunk(const ImageTestCase& aTestCase,
+ bool aUseDecodePool = false) {
+ WithSingleChunkDecode(aTestCase, Nothing(), aUseDecodePool,
+ [&](image::Decoder* aDecoder) {
+ CheckDecoderResults(aTestCase, aDecoder);
+ });
+}
+
+template <typename Func>
+void WithDelayedChunkDecode(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));
+
+ // Prepare an empty SourceBuffer.
+ auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+
+ // Create a decoder.
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ true);
+
+ // Run the full decoder synchronously. It should now be waiting on
+ // the iterator to yield some data since we haven't written anything yet.
+ task->Run();
+
+ // Writing all of the data should wake up the decoder to complete.
+ sourceBuffer->ExpectLength(length);
+ rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ sourceBuffer->Complete(NS_OK);
+
+ // It would have gotten posted to the main thread to avoid mutex contention.
+ SpinPendingEvents();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(decoder);
+}
+
+static void CheckDecoderDelayedChunk(const ImageTestCase& aTestCase) {
+ WithDelayedChunkDecode(aTestCase, Nothing(), [&](image::Decoder* aDecoder) {
+ CheckDecoderResults(aTestCase, aDecoder);
+ });
+}
+
+static void CheckDecoderMultiChunk(const ImageTestCase& aTestCase,
+ uint64_t aChunkSize = 1) {
+ 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.
+ auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+ sourceBuffer->ExpectLength(length);
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, Nothing(), DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ true);
+
+ // Run the full decoder synchronously. It should now be waiting on
+ // the iterator to yield some data since we haven't written anything yet.
+ task->Run();
+
+ while (length > 0) {
+ uint64_t read = length > aChunkSize ? aChunkSize : length;
+ length -= read;
+
+ uint64_t available = 0;
+ rv = inputStream->Available(&available);
+ ASSERT_TRUE(available >= read);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Writing any data should wake up the decoder to complete.
+ rv = sourceBuffer->AppendFromInputStream(inputStream, read);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // It would have gotten posted to the main thread to avoid mutex contention.
+ SpinPendingEvents();
+ }
+
+ sourceBuffer->Complete(NS_OK);
+ SpinPendingEvents();
+
+ 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), /* aUseDecodePool */ false,
+ [&](image::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,
+ aTestCase.ChooseColor(BGRAColor::Green()),
+ /* aFuzz = */ 47));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3,
+ aTestCase.ChooseColor(BGRAColor::Red()),
+ /* aFuzz = */ 27));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(),
+ /* aFuzz = */ 47));
+ EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4,
+ aTestCase.ChooseColor(BGRAColor::Red()),
+ /* aFuzz = */ 27));
+ });
+}
+
+static void CheckAnimationDecoderResults(const ImageTestCase& aTestCase,
+ AnimationSurfaceProvider* aProvider,
+ image::Decoder* aDecoder) {
+ EXPECT_TRUE(aDecoder->GetDecodeDone());
+ EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), aDecoder->HasError());
+
+ if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) {
+ return; // That's all we can check for bad images.
+ }
+
+ // The decoder should get the correct size.
+ IntSize size = aDecoder->Size();
+ EXPECT_EQ(aTestCase.mSize.width, size.width);
+ EXPECT_EQ(aTestCase.mSize.height, size.height);
+
+ if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) {
+ return;
+ }
+
+ // Check the output.
+ AutoTArray<BGRAColor, 2> framePixels;
+ framePixels.AppendElement(aTestCase.ChooseColor(BGRAColor::Green()));
+ framePixels.AppendElement(
+ aTestCase.ChooseColor(BGRAColor(0x7F, 0x7F, 0x7F, 0xFF)));
+
+ DrawableSurface drawableSurface(WrapNotNull(aProvider));
+ for (size_t i = 0; i < framePixels.Length(); ++i) {
+ nsresult rv = drawableSurface.Seek(i);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ // Check the first frame, all green.
+ RawAccessFrameRef rawFrame = drawableSurface->RawAccessRef();
+ RefPtr<SourceSurface> surface = rawFrame->GetSourceSurface();
+
+ // Verify that the resulting surfaces matches our expectations.
+ EXPECT_TRUE(surface->IsDataSourceSurface());
+ EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX ||
+ surface->GetFormat() == SurfaceFormat::OS_RGBA);
+ EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize());
+ EXPECT_TRUE(IsSolidColor(surface, framePixels[i], aTestCase.Fuzz()));
+ }
+
+ // Should be no more frames.
+ nsresult rv = drawableSurface.Seek(framePixels.Length());
+ EXPECT_TRUE(NS_FAILED(rv));
+}
+
+template <typename Func>
+static void WithSingleChunkAnimationDecode(const ImageTestCase& aTestCase,
+ Func aResultChecker) {
+ // Create an image.
+ RefPtr<Image> image = ImageFactory::CreateAnonymousImage(
+ nsDependentCString(aTestCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ NotNull<RefPtr<RasterImage>> rasterImage =
+ WrapNotNull(static_cast<RasterImage*>(image.get()));
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into a SourceBuffer.
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ sourceBuffer->ExpectLength(length);
+ rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ sourceBuffer->Complete(NS_OK);
+
+ // Create a metadata decoder first, because otherwise RasterImage will get
+ // unhappy about finding out the image is animated during a full decode.
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<IDecodingTask> task = DecoderFactory::CreateMetadataDecoder(
+ decoderType, rasterImage, sourceBuffer);
+ ASSERT_TRUE(task != nullptr);
+
+ // Run the metadata decoder synchronously.
+ task->Run();
+
+ // Create a decoder.
+ DecoderFlags decoderFlags = DefaultDecoderFlags();
+ SurfaceFlags surfaceFlags = aTestCase.mSurfaceFlags;
+ RefPtr<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, Nothing(), decoderFlags, surfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+
+ // Create an AnimationSurfaceProvider which will manage the decoding process
+ // and make this decoder's output available in the surface cache.
+ SurfaceKey surfaceKey = RasterSurfaceKey(aTestCase.mOutputSize, surfaceFlags,
+ PlaybackType::eAnimated);
+ RefPtr<AnimationSurfaceProvider> provider = new AnimationSurfaceProvider(
+ rasterImage, surfaceKey, WrapNotNull(decoder),
+ /* aCurrentFrame */ 0);
+
+ // Run the full decoder synchronously.
+ provider->Run();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(provider, decoder);
+}
+
+static void CheckAnimationDecoderSingleChunk(const ImageTestCase& aTestCase) {
+ WithSingleChunkAnimationDecode(
+ aTestCase,
+ [&](AnimationSurfaceProvider* aProvider, image::Decoder* aDecoder) {
+ CheckAnimationDecoderResults(aTestCase, aProvider, aDecoder);
+ });
+}
+
+static void CheckDecoderFrameFirst(const ImageTestCase& aTestCase) {
+ // 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(aTestCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.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();
+
+ auto unlock = mozilla::MakeScopeExit([&] { image->UnlockImage(); });
+
+ // 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(aTestCase.mSize.width, imageSize.width);
+ EXPECT_EQ(aTestCase.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, aTestCase.mSurfaceFlags,
+ PlaybackType::eStatic),
+ /* aMarkUsed = */ false);
+ 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, aTestCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ false);
+ 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, aTestCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ RefPtr<imgFrame> partialFrame = result.Surface().GetFrame(1);
+ EXPECT_TRUE(bool(partialFrame));
+ }
+
+ // Ensure that the static version is still around.
+ {
+ LookupResult result = SurfaceCache::Lookup(
+ ImageKey(image.get()),
+ RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags,
+ PlaybackType::eStatic),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+ EXPECT_TRUE(bool(result.Surface()));
+ }
+}
+
+static void CheckDecoderFrameCurrent(const ImageTestCase& aTestCase) {
+ // 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(aTestCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.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(aTestCase.mSize.width, imageSize.width);
+ EXPECT_EQ(aTestCase.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, aTestCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ RefPtr<imgFrame> partialFrame = result.Surface().GetFrame(1);
+ EXPECT_TRUE(bool(partialFrame));
+ }
+
+ // Ensure that we didn't decode the static version of the image.
+ {
+ LookupResult result = SurfaceCache::Lookup(
+ ImageKey(image.get()),
+ RasterSurfaceKey(imageSize, aTestCase.mSurfaceFlags,
+ PlaybackType::eStatic),
+ /* aMarkUsed = */ false);
+ 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, aTestCase.mSurfaceFlags,
+ PlaybackType::eStatic),
+ /* aMarkUsed = */ true);
+ 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, aTestCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ RefPtr<imgFrame> partialFrame = result.Surface().GetFrame(1);
+ EXPECT_TRUE(bool(partialFrame));
+ }
+}
+
+class ImageDecoders : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+#define IMAGE_GTEST_DECODER_BASE_F(test_prefix) \
+ TEST_F(ImageDecoders, test_prefix##SingleChunk) { \
+ CheckDecoderSingleChunk(Green##test_prefix##TestCase()); \
+ } \
+ \
+ TEST_F(ImageDecoders, test_prefix##DelayedChunk) { \
+ CheckDecoderDelayedChunk(Green##test_prefix##TestCase()); \
+ } \
+ \
+ TEST_F(ImageDecoders, test_prefix##MultiChunk) { \
+ CheckDecoderMultiChunk(Green##test_prefix##TestCase()); \
+ } \
+ \
+ TEST_F(ImageDecoders, test_prefix##DownscaleDuringDecode) { \
+ CheckDownscaleDuringDecode(Downscaled##test_prefix##TestCase()); \
+ } \
+ \
+ TEST_F(ImageDecoders, test_prefix##ForceSRGB) { \
+ CheckDecoderSingleChunk(Green##test_prefix##TestCase().WithSurfaceFlags( \
+ SurfaceFlags::TO_SRGB_COLORSPACE)); \
+ } \
+ \
+ TEST_F(ImageDecoders, test_prefix##BadBuffer) { \
+ CheckDecoderBadBuffer(Green##test_prefix##TestCase().WithFlags( \
+ TEST_CASE_HAS_ERROR | TEST_CASE_IGNORE_OUTPUT)); \
+ }
+
+IMAGE_GTEST_DECODER_BASE_F(PNG)
+IMAGE_GTEST_DECODER_BASE_F(GIF)
+IMAGE_GTEST_DECODER_BASE_F(JPG)
+IMAGE_GTEST_DECODER_BASE_F(BMP)
+IMAGE_GTEST_DECODER_BASE_F(ICO)
+IMAGE_GTEST_DECODER_BASE_F(Icon)
+IMAGE_GTEST_DECODER_BASE_F(WebP)
+
+TEST_F(ImageDecoders, ICOWithANDMaskDownscaleDuringDecode) {
+ CheckDownscaleDuringDecode(DownscaledTransparentICOWithANDMaskTestCase());
+}
+
+TEST_F(ImageDecoders, WebPLargeMultiChunk) {
+ CheckDecoderMultiChunk(LargeWebPTestCase(), /* aChunkSize */ 64);
+}
+
+TEST_F(ImageDecoders, WebPIccSrgbMultiChunk) {
+ CheckDecoderMultiChunk(GreenWebPIccSrgbTestCase());
+}
+
+TEST_F(ImageDecoders, WebPTransparentSingleChunk) {
+ CheckDecoderSingleChunk(TransparentWebPTestCase());
+}
+
+TEST_F(ImageDecoders, WebPTransparentNoAlphaHeaderSingleChunk) {
+ CheckDecoderSingleChunk(TransparentNoAlphaHeaderWebPTestCase());
+}
+
+TEST_F(ImageDecoders, AVIFSingleChunk) {
+ CheckDecoderSingleChunk(GreenAVIFTestCase());
+}
+
+TEST_F(ImageDecoders, AVIFMultiLayerSingleChunk) {
+ CheckDecoderSingleChunk(MultiLayerAVIFTestCase());
+}
+
+// This test must use the decode pool in order to check for regressions
+// of crashing the dav1d decoder when the ImgDecoder threads have a standard-
+// sized stack.
+TEST_F(ImageDecoders, AVIFStackCheck) {
+ CheckDecoderSingleChunk(StackCheckAVIFTestCase(), /* aUseDecodePool */ true);
+}
+
+TEST_F(ImageDecoders, AVIFDelayedChunk) {
+ CheckDecoderDelayedChunk(GreenAVIFTestCase());
+}
+
+TEST_F(ImageDecoders, AVIFMultiChunk) {
+ CheckDecoderMultiChunk(GreenAVIFTestCase());
+}
+
+TEST_F(ImageDecoders, AVIFLargeMultiChunk) {
+ CheckDecoderMultiChunk(LargeAVIFTestCase(), /* aChunkSize */ 64);
+}
+
+TEST_F(ImageDecoders, AVIFDownscaleDuringDecode) {
+ CheckDownscaleDuringDecode(DownscaledAVIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFSingleChunk) {
+ CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFMultiChunk) {
+ CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFWithBlendedFrames) {
+ CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedPNGSingleChunk) {
+ CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedPNGMultiChunk) {
+ CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedPNGWithBlendedFrames) {
+ CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedWebPSingleChunk) {
+ CheckDecoderSingleChunk(GreenFirstFrameAnimatedWebPTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedWebPMultiChunk) {
+ CheckDecoderMultiChunk(GreenFirstFrameAnimatedWebPTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedWebPWithBlendedFrames) {
+ CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedWebPTestCase());
+}
+
+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, CorruptICOWithBadBppSingleChunk) {
+ CheckDecoderSingleChunk(CorruptICOWithBadBppTestCase());
+}
+
+// Running this test under emulation for Android 7 on x86_64 seems to result
+// in the large allocation succeeding, but leaving so little memory left the
+// system falls over and it kills the test run, so we skip it instead.
+// See bug 1655846 for more details.
+#ifndef ANDROID
+TEST_F(ImageDecoders, CorruptAVIFSingleChunk) {
+ CheckDecoderSingleChunk(CorruptAVIFTestCase());
+}
+#endif
+
+TEST_F(ImageDecoders, AnimatedGIFWithFRAME_FIRST) {
+ CheckDecoderFrameFirst(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedGIFWithFRAME_CURRENT) {
+ CheckDecoderFrameCurrent(GreenFirstFrameAnimatedGIFTestCase());
+}
+
+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, testCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ RefPtr<imgFrame> partialFrame = result.Surface().GetFrame(1);
+ EXPECT_TRUE(bool(partialFrame));
+}
+
+TEST_F(ImageDecoders, AnimatedWebPWithFRAME_FIRST) {
+ CheckDecoderFrameFirst(GreenFirstFrameAnimatedWebPTestCase());
+}
+
+TEST_F(ImageDecoders, AnimatedWebPWithFRAME_CURRENT) {
+ CheckDecoderFrameCurrent(GreenFirstFrameAnimatedWebPTestCase());
+}
+
+TEST_F(ImageDecoders, TruncatedSmallGIFSingleChunk) {
+ CheckDecoderSingleChunk(TruncatedSmallGIFTestCase());
+}
+
+TEST_F(ImageDecoders, LargeICOWithBMPSingleChunk) {
+ CheckDecoderSingleChunk(LargeICOWithBMPTestCase());
+}
+
+TEST_F(ImageDecoders, LargeICOWithBMPMultiChunk) {
+ CheckDecoderMultiChunk(LargeICOWithBMPTestCase(), /* aChunkSize */ 64);
+}
+
+TEST_F(ImageDecoders, LargeICOWithPNGSingleChunk) {
+ CheckDecoderSingleChunk(LargeICOWithPNGTestCase());
+}
+
+TEST_F(ImageDecoders, LargeICOWithPNGMultiChunk) {
+ CheckDecoderMultiChunk(LargeICOWithPNGTestCase());
+}
+
+TEST_F(ImageDecoders, MultipleSizesICOSingleChunk) {
+ ImageTestCase testCase = GreenMultipleSizesICOTestCase();
+
+ // 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);
+
+ nsTArray<IntSize> nativeSizes;
+ rv = image->GetNativeSizes(nativeSizes);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_EQ(6u, nativeSizes.Length());
+
+ IntSize expectedSizes[] = {IntSize(16, 16), IntSize(32, 32),
+ IntSize(64, 64), IntSize(128, 128),
+ IntSize(256, 256), IntSize(256, 128)};
+
+ for (int i = 0; i < 6; ++i) {
+ EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+ }
+
+ RefPtr<Image> image90 =
+ ImageOps::Orient(image, Orientation(Angle::D90, Flip::Unflipped));
+ rv = image90->GetNativeSizes(nativeSizes);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_EQ(6u, nativeSizes.Length());
+
+ for (int i = 0; i < 5; ++i) {
+ EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+ }
+ EXPECT_EQ(IntSize(128, 256), nativeSizes[5]);
+
+ RefPtr<Image> image180 =
+ ImageOps::Orient(image, Orientation(Angle::D180, Flip::Unflipped));
+ rv = image180->GetNativeSizes(nativeSizes);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_EQ(6u, nativeSizes.Length());
+
+ for (int i = 0; i < 6; ++i) {
+ EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+ }
+}
diff --git a/image/test/gtest/TestDecodersPerf.cpp b/image/test/gtest/TestDecodersPerf.cpp
new file mode 100644
index 0000000000..e9ada6ded9
--- /dev/null
+++ b/image/test/gtest/TestDecodersPerf.cpp
@@ -0,0 +1,159 @@
+/* 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 "gtest/MozGTestBench.h"
+
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "IDecodingTask.h"
+#include "mozilla/RefPtr.h"
+#include "ProgressTracker.h"
+#include "SourceBuffer.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+namespace {
+
+static void CheckDecoderState(const ImageTestCase& aTestCase,
+ image::Decoder* aDecoder,
+ const IntSize& aOutputSize) {
+ // image::Decoder should match what we asked for in the MIME type.
+ EXPECT_NE(aDecoder->GetType(), DecoderType::UNKNOWN);
+ EXPECT_EQ(aDecoder->GetType(),
+ DecoderFactory::GetDecoderType(aTestCase.mMimeType));
+
+ EXPECT_TRUE(aDecoder->GetDecodeDone());
+ EXPECT_FALSE(aDecoder->HasError());
+
+ // Verify that the decoder made the expected progress.
+ Progress progress = aDecoder->TakeProgress();
+ EXPECT_FALSE(bool(progress & FLAG_HAS_ERROR));
+ EXPECT_FALSE(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR));
+
+ 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_TRUE(surface->IsDataSourceSurface());
+ EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::OS_RGBX ||
+ surface->GetFormat() == SurfaceFormat::OS_RGBA);
+ EXPECT_EQ(aOutputSize, surface->GetSize());
+}
+
+template <typename Func>
+static void WithSingleChunkDecode(const ImageTestCase& aTestCase,
+ SourceBuffer* aSourceBuffer,
+ const Maybe<IntSize>& aOutputSize,
+ Func aResultChecker) {
+ auto sourceBuffer = WrapNotNull(RefPtr<SourceBuffer>(aSourceBuffer));
+
+ // Create a decoder.
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<image::Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(
+ decoderType, sourceBuffer, aOutputSize, DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false);
+
+ // Run the full decoder synchronously.
+ task->Run();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(decoder);
+}
+
+static void CheckDecode(const ImageTestCase& aTestCase,
+ SourceBuffer* aSourceBuffer) {
+ WithSingleChunkDecode(
+ aTestCase, aSourceBuffer, Nothing(), [&](image::Decoder* aDecoder) {
+ CheckDecoderState(aTestCase, aDecoder, aTestCase.mSize);
+ });
+}
+
+static void CheckDownscaleDuringDecode(const ImageTestCase& aTestCase,
+ SourceBuffer* aSourceBuffer) {
+ IntSize outputSize(20, 20);
+ WithSingleChunkDecode(aTestCase, aSourceBuffer, Some(outputSize),
+ [&](image::Decoder* aDecoder) {
+ CheckDecoderState(aTestCase, aDecoder, outputSize);
+ });
+}
+
+#define IMAGE_GTEST_BENCH_FIXTURE(test_fixture, test_case) \
+ class test_fixture : public ImageBenchmarkBase { \
+ protected: \
+ test_fixture() : ImageBenchmarkBase(test_case()) {} \
+ };
+
+#define IMAGE_GTEST_NATIVE_BENCH_F(test_fixture) \
+ MOZ_GTEST_BENCH_F(test_fixture, Native, \
+ [this] { CheckDecode(mTestCase, mSourceBuffer); });
+
+#define IMAGE_GTEST_DOWNSCALE_BENCH_F(test_fixture) \
+ MOZ_GTEST_BENCH_F(test_fixture, Downscale, [this] { \
+ CheckDownscaleDuringDecode(mTestCase, mSourceBuffer); \
+ });
+
+#define IMAGE_GTEST_NO_COLOR_MANAGEMENT_BENCH_F(test_fixture) \
+ MOZ_GTEST_BENCH_F(test_fixture, NoColorManagement, [this] { \
+ ImageTestCase testCase = mTestCase; \
+ testCase.mSurfaceFlags |= SurfaceFlags::NO_COLORSPACE_CONVERSION; \
+ CheckDecode(testCase, mSourceBuffer); \
+ });
+
+#define IMAGE_GTEST_NO_PREMULTIPLY_BENCH_F(test_fixture) \
+ MOZ_GTEST_BENCH_F(test_fixture, NoPremultiplyAlpha, [this] { \
+ ImageTestCase testCase = mTestCase; \
+ testCase.mSurfaceFlags |= SurfaceFlags::NO_PREMULTIPLY_ALPHA; \
+ CheckDecode(testCase, mSourceBuffer); \
+ });
+
+#define IMAGE_GTEST_BENCH_F(type, test) \
+ IMAGE_GTEST_BENCH_FIXTURE(ImageDecodersPerf_##type##_##test, \
+ Perf##test##type##TestCase) \
+ IMAGE_GTEST_NATIVE_BENCH_F(ImageDecodersPerf_##type##_##test) \
+ IMAGE_GTEST_DOWNSCALE_BENCH_F(ImageDecodersPerf_##type##_##test) \
+ IMAGE_GTEST_NO_COLOR_MANAGEMENT_BENCH_F(ImageDecodersPerf_##type##_##test)
+
+#define IMAGE_GTEST_BENCH_ALPHA_F(type, test) \
+ IMAGE_GTEST_BENCH_F(type, test) \
+ IMAGE_GTEST_NO_PREMULTIPLY_BENCH_F(ImageDecodersPerf_##type##_##test)
+
+IMAGE_GTEST_BENCH_F(JPG, YCbCr)
+IMAGE_GTEST_BENCH_F(JPG, Cmyk)
+IMAGE_GTEST_BENCH_F(JPG, Gray)
+
+IMAGE_GTEST_BENCH_F(PNG, Rgb)
+IMAGE_GTEST_BENCH_F(PNG, Gray)
+IMAGE_GTEST_BENCH_ALPHA_F(PNG, RgbAlpha)
+IMAGE_GTEST_BENCH_ALPHA_F(PNG, GrayAlpha)
+
+IMAGE_GTEST_BENCH_F(WebP, RgbLossless)
+IMAGE_GTEST_BENCH_F(WebP, RgbLossy)
+IMAGE_GTEST_BENCH_ALPHA_F(WebP, RgbAlphaLossless)
+IMAGE_GTEST_BENCH_ALPHA_F(WebP, RgbAlphaLossy)
+
+IMAGE_GTEST_BENCH_F(GIF, Rgb)
+
+} // namespace
diff --git a/image/test/gtest/TestDeinterlacingFilter.cpp b/image/test/gtest/TestDeinterlacingFilter.cpp
new file mode 100644
index 0000000000..5e3e920271
--- /dev/null
+++ b/image/test/gtest/TestDeinterlacingFilter.cpp
@@ -0,0 +1,636 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/gfx/2D.h"
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfaceFilters.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+template <typename Func>
+void WithDeinterlacingFilter(const IntSize& aSize, bool aProgressiveDisplay,
+ Func aFunc) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ WithFilterPipeline(
+ decoder, std::forward<Func>(aFunc),
+ DeinterlacingConfig<uint32_t>{aProgressiveDisplay},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false});
+}
+
+void AssertConfiguringDeinterlacingFilterFails(const IntSize& aSize) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AssertConfiguringPipelineFails(
+ decoder, DeinterlacingConfig<uint32_t>{/* mProgressiveDisplay = */ true},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false});
+}
+
+class ImageDeinterlacingFilter : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageDeinterlacingFilter, WritePixels100_100) {
+ WithDeinterlacingFilter(
+ IntSize(100, 100), /* aProgressiveDisplay = */ true,
+ [](image::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,
+ [](image::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,
+ [](image::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,
+ [](image::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,
+ [](image::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,
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckWritePixels(
+ aDecoder, aFilter,
+ /* aOutputRect = */ Some(IntRect(0, 0, 1, 1)),
+ /* aInputRect = */ Some(IntRect(0, 0, 1, 1)));
+ });
+}
+
+TEST_F(ImageDeinterlacingFilter, WritePixelsNonProgressiveOutput51_52) {
+ WithDeinterlacingFilter(
+ IntSize(51, 52), /* aProgressiveDisplay = */ false,
+ [](image::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,
+ [](image::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,
+ [](image::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,
+ [](image::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,
+ [](image::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(image::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,
+ [](image::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,
+ [](image::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 0000000000..d00f67d188
--- /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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ WithFilterPipeline(
+ decoder, std::forward<Func>(aFunc),
+ DownscalingConfig{aInputSize, SurfaceFormat::OS_RGBA},
+ SurfaceConfig{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false});
+}
+
+void AssertConfiguringDownscalingFilterFails(const IntSize& aInputSize,
+ const IntSize& aOutputSize) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AssertConfiguringPipelineFails(
+ decoder, DownscalingConfig{aInputSize, SurfaceFormat::OS_RGBA},
+ SurfaceConfig{decoder, aOutputSize, SurfaceFormat::OS_RGBA, false});
+}
+
+TEST(ImageDownscalingFilter, WritePixels100_100to99_99)
+{
+ WithDownscalingFilter(IntSize(100, 100), IntSize(99, 99),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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));
+ });
+}
diff --git a/image/test/gtest/TestDownscalingFilterNoSkia.cpp b/image/test/gtest/TestDownscalingFilterNoSkia.cpp
new file mode 100644
index 0000000000..063a0adbd8
--- /dev/null
+++ b/image/test/gtest/TestDownscalingFilterNoSkia.cpp
@@ -0,0 +1,55 @@
+/* -*- 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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(bool(decoder));
+
+ // Configuring a DownscalingFilter should fail without Skia.
+ AssertConfiguringPipelineFails(
+ decoder, DownscalingConfig{IntSize(100, 100), SurfaceFormat::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(50, 50), SurfaceFormat::OS_RGBA, false});
+}
diff --git a/image/test/gtest/TestFrameAnimator.cpp b/image/test/gtest/TestFrameAnimator.cpp
new file mode 100644
index 0000000000..a0c54f3f09
--- /dev/null
+++ b/image/test/gtest/TestFrameAnimator.cpp
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "Common.h"
+#include "AnimationSurfaceProvider.h"
+#include "Decoder.h"
+#include "ImageFactory.h"
+#include "nsIInputStream.h"
+#include "RasterImage.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+static void CheckFrameAnimatorBlendResults(const ImageTestCase& aTestCase,
+ RasterImage* aImage) {
+ // Allow the animation to actually begin.
+ aImage->IncrementAnimationConsumers();
+
+ // Initialize for the first frame so we can advance.
+ TimeStamp now = TimeStamp::Now();
+ aImage->RequestRefresh(now);
+
+ RefPtr<SourceSurface> surface =
+ aImage->GetFrame(imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_NONE);
+ ASSERT_TRUE(surface != nullptr);
+
+ CheckGeneratedSurface(surface, IntRect(0, 0, 50, 50),
+ BGRAColor::Transparent(),
+ aTestCase.ChooseColor(BGRAColor::Red()));
+
+ // Advance to the next/final frame.
+ now = TimeStamp::Now() + TimeDuration::FromMilliseconds(500);
+ aImage->RequestRefresh(now);
+
+ surface =
+ aImage->GetFrame(imgIContainer::FRAME_CURRENT, imgIContainer::FLAG_NONE);
+ ASSERT_TRUE(surface != nullptr);
+ CheckGeneratedSurface(surface, IntRect(0, 0, 50, 50),
+ aTestCase.ChooseColor(BGRAColor::Green()),
+ aTestCase.ChooseColor(BGRAColor::Red()));
+}
+
+template <typename Func>
+static void WithFrameAnimatorDecode(const ImageTestCase& aTestCase,
+ Func aResultChecker) {
+ // Create an image.
+ RefPtr<Image> image = ImageFactory::CreateAnonymousImage(
+ nsDependentCString(aTestCase.mMimeType));
+ ASSERT_TRUE(!image->HasError());
+
+ NotNull<RefPtr<RasterImage>> rasterImage =
+ WrapNotNull(static_cast<RasterImage*>(image.get()));
+
+ nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+ ASSERT_TRUE(inputStream != nullptr);
+
+ // Figure out how much data we have.
+ uint64_t length;
+ nsresult rv = inputStream->Available(&length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Write the data into a SourceBuffer.
+ NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+ sourceBuffer->ExpectLength(length);
+ rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ sourceBuffer->Complete(NS_OK);
+
+ // Create a metadata decoder first, because otherwise RasterImage will get
+ // unhappy about finding out the image is animated during a full decode.
+ DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+ RefPtr<IDecodingTask> task = DecoderFactory::CreateMetadataDecoder(
+ decoderType, rasterImage, sourceBuffer);
+ ASSERT_TRUE(task != nullptr);
+
+ // Run the metadata decoder synchronously.
+ task->Run();
+ task = nullptr;
+
+ // Create an AnimationSurfaceProvider which will manage the decoding process
+ // and make this decoder's output available in the surface cache.
+ DecoderFlags decoderFlags = DefaultDecoderFlags();
+ SurfaceFlags surfaceFlags = aTestCase.mSurfaceFlags;
+ rv = DecoderFactory::CreateAnimationDecoder(
+ decoderType, rasterImage, sourceBuffer, aTestCase.mSize, decoderFlags,
+ surfaceFlags, 0, getter_AddRefs(task));
+ EXPECT_EQ(rv, NS_OK);
+ ASSERT_TRUE(task != nullptr);
+
+ // Run the full decoder synchronously.
+ task->Run();
+
+ // Call the lambda to verify the expected results.
+ aResultChecker(rasterImage.get());
+}
+
+static void CheckFrameAnimatorBlend(const ImageTestCase& aTestCase) {
+ WithFrameAnimatorDecode(aTestCase, [&](RasterImage* aImage) {
+ CheckFrameAnimatorBlendResults(aTestCase, aImage);
+ });
+}
+
+class ImageFrameAnimator : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageFrameAnimator, BlendGIFWithFilter) {
+ CheckFrameAnimatorBlend(BlendAnimatedGIFTestCase());
+}
+
+TEST_F(ImageFrameAnimator, BlendPNGWithFilter) {
+ CheckFrameAnimatorBlend(BlendAnimatedPNGTestCase());
+}
+
+TEST_F(ImageFrameAnimator, BlendWebPWithFilter) {
+ CheckFrameAnimatorBlend(BlendAnimatedWebPTestCase());
+}
diff --git a/image/test/gtest/TestLoader.cpp b/image/test/gtest/TestLoader.cpp
new file mode 100644
index 0000000000..289cebae9c
--- /dev/null
+++ b/image/test/gtest/TestLoader.cpp
@@ -0,0 +1,103 @@
+/* -*- 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 "Common.h"
+#include "imgLoader.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+static void CheckMimeType(const char* aContents, size_t aLength,
+ const char* aExpected) {
+ nsAutoCString detected;
+ nsresult rv = imgLoader::GetMimeTypeFromContent(aContents, aLength, detected);
+ if (aExpected) {
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ EXPECT_TRUE(detected.EqualsASCII(aExpected));
+ } else {
+ ASSERT_TRUE(NS_FAILED(rv));
+ EXPECT_TRUE(detected.IsEmpty());
+ }
+}
+
+class ImageLoader : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageLoader, DetectGIF) {
+ const char buffer[] = "GIF87a";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_GIF);
+}
+
+TEST_F(ImageLoader, DetectPNG) {
+ const char buffer[] = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_PNG);
+}
+
+TEST_F(ImageLoader, DetectJPEG) {
+ const char buffer[] = "\xFF\xD8\xFF";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_JPEG);
+}
+
+TEST_F(ImageLoader, DetectART) {
+ const char buffer[] = "\x4A\x47\xFF\xFF\x00";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_ART);
+}
+
+TEST_F(ImageLoader, DetectBMP) {
+ const char buffer[] = "BM";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_BMP);
+}
+
+TEST_F(ImageLoader, DetectICO) {
+ const char buffer[] = "\x00\x00\x01\x00";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_ICO);
+}
+
+TEST_F(ImageLoader, DetectWebP) {
+ const char buffer[] = "RIFF\xFF\xFF\xFF\xFFWEBPVP8L";
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_WEBP);
+}
+
+TEST_F(ImageLoader, DetectAVIFMajorBrand) {
+ const char buffer[] =
+ "\x00\x00\x00\x20" // box length
+ "ftyp" // box type
+ "avif" // major brand
+ "\x00\x00\x00\x00" // minor version
+ "avifmif1miafMA1B"; // compatible brands
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_AVIF);
+}
+
+TEST_F(ImageLoader, DetectAVIFCompatibleBrand) {
+ const char buffer[] =
+ "\x00\x00\x00\x20" // box length
+ "ftyp" // box type
+ "XXXX" // major brand
+ "\x00\x00\x00\x00" // minor version
+ "avifmif1miafMA1B"; // compatible brands
+ CheckMimeType(buffer, sizeof(buffer), IMAGE_AVIF);
+}
+
+TEST_F(ImageLoader, DetectNonImageMP4) {
+ const char buffer[] =
+ "\x00\x00\x00\x1c" // box length
+ "ftyp" // box type
+ "isom" // major brand
+ "\x00\x00\x02\x00" // minor version
+ "isomiso2mp41"; // compatible brands
+ CheckMimeType(buffer, sizeof(buffer), nullptr);
+}
+
+TEST_F(ImageLoader, DetectNone) {
+ const char buffer[] = "abcdefghijklmnop";
+ CheckMimeType(buffer, sizeof(buffer), nullptr);
+}
diff --git a/image/test/gtest/TestMetadata.cpp b/image/test/gtest/TestMetadata.cpp
new file mode 100644
index 0000000000..209ad42d52
--- /dev/null
+++ b/image/test/gtest/TestMetadata.cpp
@@ -0,0 +1,253 @@
+/* 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 "ImageFactory.h"
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.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.
+ auto sourceBuffer = MakeNotNull<RefPtr<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<image::Decoder> decoder =
+ DecoderFactory::CreateAnonymousMetadataDecoder(decoderType, sourceBuffer);
+ ASSERT_TRUE(decoder != nullptr);
+ RefPtr<IDecodingTask> task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false);
+
+ 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);
+ if (aBMPWithinICO == BMPWithinICO::YES) {
+ // Half the data is considered to be part of the AND mask if embedded
+ EXPECT_EQ(aTestCase.mSize.height / 2, metadataSize.height);
+ } else {
+ 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(), DecoderFlags::FIRST_FRAME_ONLY,
+ aTestCase.mSurfaceFlags);
+ ASSERT_TRUE(decoder != nullptr);
+ task =
+ new AnonymousDecodingTask(WrapNotNull(decoder), /* aResumable */ false);
+
+ 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, TransparentAVIF) {
+ CheckMetadata(TransparentAVIFTestCase());
+}
+
+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, WebP) { CheckMetadata(GreenWebPTestCase()); }
+
+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, testCase.mSurfaceFlags,
+ PlaybackType::eAnimated),
+ /* aMarkUsed = */ true);
+ ASSERT_EQ(MatchType::EXACT, result.Type());
+
+ EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+ EXPECT_TRUE(bool(result.Surface()));
+
+ RefPtr<imgFrame> partialFrame = result.Surface().GetFrame(1);
+ EXPECT_TRUE(bool(partialFrame));
+}
diff --git a/image/test/gtest/TestRemoveFrameRectFilter.cpp b/image/test/gtest/TestRemoveFrameRectFilter.cpp
new file mode 100644
index 0000000000..59549f3ddb
--- /dev/null
+++ b/image/test/gtest/TestRemoveFrameRectFilter.cpp
@@ -0,0 +1,311 @@
+/* -*- 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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ WithFilterPipeline(
+ decoder, std::forward<Func>(aFunc), RemoveFrameRectConfig{aFrameRect},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false});
+}
+
+void AssertConfiguringRemoveFrameRectFilterFails(const IntSize& aSize,
+ const IntRect& aFrameRect) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ AssertConfiguringPipelineFails(
+ decoder, RemoveFrameRectConfig{aFrameRect},
+ SurfaceConfig{decoder, aSize, SurfaceFormat::OS_RGBA, false});
+}
+
+TEST(ImageRemoveFrameRectFilter, WritePixels100_100_to_0_0_100_100)
+{
+ WithRemoveFrameRectFilter(
+ IntSize(100, 100), IntRect(0, 0, 100, 100),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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),
+ [](image::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));
+}
diff --git a/image/test/gtest/TestSourceBuffer.cpp b/image/test/gtest/TestSourceBuffer.cpp
new file mode 100644
index 0000000000..5c937becb8
--- /dev/null
+++ b/image/test/gtest/TestSourceBuffer.cpp
@@ -0,0 +1,822 @@
+/* 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 <cstdint>
+#include <utility>
+
+#include "Common.h"
+#include "SourceBuffer.h"
+#include "SurfaceCache.h"
+#include "gtest/gtest.h"
+#include "nsIInputStream.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, ExpectLengthAllocatesRequestedCapacity) {
+ 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 two chunks, because we trust the
+ // initial guess of ExpectLength() but after that it will only allocate chunks
+ // of at least 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 first chunk with 1 byte, and a second chunk
+ // with the remaining data.
+ CheckedAdvanceIterator(iterator, 1, 1, 1);
+ CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY - 1, 2,
+ SourceBuffer::MIN_CHUNK_CAPACITY);
+ CheckIteratorIsComplete(iterator, 2, 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_INVALID_ARG);
+}
+
+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, OversizedAppendsAllocateAtMostOneChunk) {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+ // Allocate some data we'll use below.
+ constexpr size_t writeLength = SourceBuffer::MAX_CHUNK_CAPACITY + 1;
+
+ // Write SourceBuffer::MAX_CHUNK_CAPACITY + 1 bytes of test data to the
+ // buffer in a single Append() call. This should cause one chunk to be
+ // allocated because we wrote it as a single block.
+ CheckedAppendToBufferInChunks(writeLength, writeLength);
+
+ // Verify that the iterator sees a MAX_CHUNK_CAPACITY+1-length chunk.
+ CheckedAdvanceIterator(iterator, writeLength);
+
+ CheckedCompleteBuffer(NS_OK);
+ CheckIteratorIsComplete(iterator, 1, writeLength);
+}
+
+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 tmpIterator = GetIterator();
+ SourceBufferIterator movedIterator(std::move(tmpIterator));
+ 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.
+ tmpIterator = GetIterator();
+ movedIterator = std::move(tmpIterator);
+ 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);
+}
+
+TEST_F(ImageSourceBuffer, CompleteSuccessWithSameReadLength) {
+ SourceBufferIterator iterator = mSourceBuffer->Iterator(1);
+
+ // 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, CompleteSuccessWithSmallerReadLength) {
+ // Create an iterator limited to one byte.
+ SourceBufferIterator iterator = mSourceBuffer->Iterator(1);
+
+ // Write two bytes 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, 2);
+ CheckedCompleteBuffer(iterator, 2);
+
+ // We should be able to advance once (to read the single byte) and then should
+ // reach the COMPLETE state with a successful status, because our iterator is
+ // limited to a single byte, rather than the full length.
+ CheckedAdvanceIterator(iterator, 1);
+ CheckIteratorIsComplete(iterator, 1);
+}
+
+TEST_F(ImageSourceBuffer, CompleteSuccessWithGreaterReadLength) {
+ // Create an iterator limited to one byte.
+ SourceBufferIterator iterator = mSourceBuffer->Iterator(2);
+
+ // 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. Our iterator lets us
+ // read more but the underlying buffer has been completed.
+ CheckedAdvanceIterator(iterator, 1);
+ CheckIteratorIsComplete(iterator, 1);
+}
diff --git a/image/test/gtest/TestStreamingLexer.cpp b/image/test/gtest/TestStreamingLexer.cpp
new file mode 100644
index 0000000000..c83569a7b9
--- /dev/null
+++ b/image/test/gtest/TestStreamingLexer.cpp
@@ -0,0 +1,935 @@
+/* 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 "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/TestSurfaceCache.cpp b/image/test/gtest/TestSurfaceCache.cpp
new file mode 100644
index 0000000000..6838d6f695
--- /dev/null
+++ b/image/test/gtest/TestSurfaceCache.cpp
@@ -0,0 +1,159 @@
+/* -*- 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 "gtest/gtest.h"
+
+#include "Common.h"
+#include "imgIContainer.h"
+#include "ImageFactory.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_image.h"
+#include "nsIInputStream.h"
+#include "nsString.h"
+#include "ProgressTracker.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+class ImageSurfaceCache : public ::testing::Test {
+ protected:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageSurfaceCache, Factor2) {
+ ImageTestCase testCase = GreenPNGTestCase();
+
+ // 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));
+
+ // Ensures we meet the threshold for FLAG_SYNC_DECODE_IF_FAST to do sync
+ // decoding without the implications of FLAG_SYNC_DECODE.
+ ASSERT_LT(length,
+ static_cast<uint64_t>(
+ StaticPrefs::image_mem_decode_bytes_at_a_time_AtStartup()));
+
+ // 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);
+
+ const uint32_t whichFrame = imgIContainer::FRAME_CURRENT;
+
+ // FLAG_SYNC_DECODE will make RasterImage::LookupFrame use
+ // SurfaceCache::Lookup to force an exact match lookup (and potential decode).
+ const uint32_t exactFlags = imgIContainer::FLAG_HIGH_QUALITY_SCALING |
+ imgIContainer::FLAG_SYNC_DECODE;
+
+ // If the data stream is small enough, as we assert above,
+ // FLAG_SYNC_DECODE_IF_FAST will allow us to decode sync, but avoid forcing
+ // SurfaceCache::Lookup. Instead it will use SurfaceCache::LookupBestMatch.
+ const uint32_t bestMatchFlags = imgIContainer::FLAG_HIGH_QUALITY_SCALING |
+ imgIContainer::FLAG_SYNC_DECODE_IF_FAST;
+
+ // We need the default threshold to be enabled (otherwise we should disable
+ // this test).
+ int32_t threshold = StaticPrefs::image_cache_factor2_threshold_surfaces();
+ ASSERT_TRUE(threshold >= 0);
+
+ // We need to know what the native sizes are, otherwise factor of 2 mode will
+ // be disabled.
+ size_t nativeSizes = image->GetNativeSizesLength();
+ ASSERT_EQ(nativeSizes, 1u);
+
+ // Threshold is the native size count and the pref threshold added together.
+ // Make sure the image is big enough that we can simply decrement and divide
+ // off the size as we please and not hit unexpected duplicates.
+ int32_t totalThreshold = static_cast<int32_t>(nativeSizes) + threshold;
+ ASSERT_TRUE(testCase.mSize.width > totalThreshold * 4);
+
+ // Request a bunch of slightly different sizes. We won't trip factor of 2 mode
+ // in this loop.
+ IntSize size = testCase.mSize;
+ for (int32_t i = 0; i <= totalThreshold; ++i) {
+ RefPtr<SourceSurface> surf =
+ image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize(), size);
+
+ size.width -= 1;
+ size.height -= 1;
+ }
+
+ // Now let's ask for a new size. Despite this being sync, it will return
+ // the closest factor of 2 size we have and not the requested size.
+ RefPtr<SourceSurface> surf =
+ image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+
+ EXPECT_EQ(surf->GetSize(), testCase.mSize);
+
+ // Now we should be in factor of 2 mode but unless we trigger a decode no
+ // pruning of the old sized surfaces should happen.
+ size = testCase.mSize;
+ for (int32_t i = 0; i < totalThreshold; ++i) {
+ RefPtr<SourceSurface> surf =
+ image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize(), size);
+
+ size.width -= 1;
+ size.height -= 1;
+ }
+
+ // Now force an existing surface to be marked as explicit so that it
+ // won't get freed upon pruning (gets marked in the Lookup).
+ size.width += 1;
+ size.height += 1;
+ surf = image->GetFrameAtSize(size, whichFrame, exactFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize(), size);
+
+ // Now force a new decode to happen by getting a new factor of 2 size.
+ size.width = testCase.mSize.width / 2 - 1;
+ size.height = testCase.mSize.height / 2 - 1;
+ surf = image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize().width, testCase.mSize.width / 2);
+ EXPECT_EQ(surf->GetSize().height, testCase.mSize.height / 2);
+
+ // The decode above would have forced a pruning to happen, so now if
+ // we request all of the sizes we used to have decoded, only the explicit
+ // size should have been kept.
+ size = testCase.mSize;
+ for (int32_t i = 0; i < totalThreshold - 1; ++i) {
+ RefPtr<SourceSurface> surf =
+ image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize(), testCase.mSize);
+
+ size.width -= 1;
+ size.height -= 1;
+ }
+
+ // This lookup finds the surface that already existed that we later marked
+ // as explicit. It should still exist after pruning.
+ surf = image->GetFrameAtSize(size, whichFrame, bestMatchFlags);
+ ASSERT_TRUE(surf);
+ EXPECT_EQ(surf->GetSize(), size);
+}
diff --git a/image/test/gtest/TestSurfacePipeIntegration.cpp b/image/test/gtest/TestSurfacePipeIntegration.cpp
new file mode 100644
index 0000000000..0c6c4c27df
--- /dev/null
+++ b/image/test/gtest/TestSurfacePipeIntegration.cpp
@@ -0,0 +1,349 @@
+/* -*- 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 pipe;
+ }
+
+ template <typename T>
+ static SurfacePipe SurfacePipeFromPipeline(T&& aPipeline) {
+ return SurfacePipe{std::move(aPipeline)};
+ }
+
+ private:
+ TestSurfacePipeFactory() {}
+};
+
+} // namespace image
+} // namespace mozilla
+
+void CheckSurfacePipeMethodResults(SurfacePipe* aPipe, image::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());
+}
+
+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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ auto sink = MakeUnique<SurfaceSink>();
+ nsresult rv = sink->Configure(
+ SurfaceConfig{decoder, IntSize(100, 100), SurfaceFormat::OS_RGBA, 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 WritePixelBlocks() gets passed through to the underlying
+ // pipeline.
+ {
+ uint32_t count = 0;
+ WriteState result = pipe.WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ ++count;
+ EXPECT_EQ(int32_t(100), aLength);
+ memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t));
+ return MakeTuple(int32_t(100), Maybe<WriteState>());
+ });
+
+ 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, DeinterlaceDownscaleWritePixels) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ auto test = [](image::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::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(25, 25), SurfaceFormat::OS_RGBA, 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<image::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 = [](image::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::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, 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<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ auto test = [](image::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::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, false});
+}
+
+TEST_F(ImageSurfacePipeIntegration, DeinterlaceRemoveFrameRectWritePixels) {
+ RefPtr<image::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 = [](image::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, IntSize(100, 100), SurfaceFormat::OS_RGBA, false});
+}
+
+TEST_F(ImageSurfacePipeIntegration,
+ DeinterlaceRemoveFrameRectDownscaleWritePixels) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ auto test = [](image::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::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(20, 20), SurfaceFormat::OS_RGBA, false});
+}
+
+TEST_F(ImageSurfacePipeIntegration, ConfiguringHugeDeinterlacingBufferFails) {
+ RefPtr<image::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::OS_RGBA},
+ SurfaceConfig{decoder, IntSize(600, 600), SurfaceFormat::OS_RGBA, false});
+}
diff --git a/image/test/gtest/TestSurfaceSink.cpp b/image/test/gtest/TestSurfaceSink.cpp
new file mode 100644
index 0000000000..c71f857263
--- /dev/null
+++ b/image/test/gtest/TestSurfaceSink.cpp
@@ -0,0 +1,980 @@
+/* -*- 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 };
+
+static void InitializeRowBuffer(uint32_t* aBuffer, size_t aSize,
+ size_t aStartPixel, size_t aEndPixel,
+ uint32_t aSetPixel) {
+ uint32_t transparentPixel = BGRAColor::Transparent().AsPixel();
+ for (size_t i = 0; i < aStartPixel && i < aSize; ++i) {
+ aBuffer[i] = transparentPixel;
+ }
+ for (size_t i = aStartPixel; i < aEndPixel && i < aSize; ++i) {
+ aBuffer[i] = aSetPixel;
+ }
+ for (size_t i = aEndPixel; i < aSize; ++i) {
+ aBuffer[i] = transparentPixel;
+ }
+}
+
+template <Orient Orientation, typename Func>
+void WithSurfaceSink(Func aFunc) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ const bool flipVertically = Orientation == Orient::FLIP_VERTICALLY;
+
+ WithFilterPipeline(decoder, std::forward<Func>(aFunc),
+ SurfaceConfig{decoder, IntSize(100, 100),
+ SurfaceFormat::OS_RGBA, flipVertically});
+}
+
+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(image::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); });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkInitialization)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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>(
+ [](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ CheckWritePixels(aDecoder, aSink);
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsFinish)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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 = [](image::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>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelsToRow)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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 = [](image::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>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::NEED_MORE_DATA);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FAILURE);
+ });
+
+ WithSurfaceSink<Orient::NORMAL>(
+ [&](image::Decoder* aDecoder, SurfaceSink* aSink) {
+ checkEarlyExit(aDecoder, aSink, WriteState::FINISHED);
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWriteBuffer)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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];
+ InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().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>(
+ [](image::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>([](image::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>([](image::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>(
+ [](image::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>([](image::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>(
+ [](image::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, SurfaceSinkWritePixelBlocks)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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];
+ InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().AsPixel());
+
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ ++count;
+ EXPECT_EQ(int32_t(100), aLength);
+ memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t));
+ return MakeTuple(int32_t(100), Maybe<WriteState>());
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(100u, count);
+
+ AssertCorrectPipelineFinalState(aSink, IntRect(0, 0, 100, 100),
+ IntRect(0, 0, 100, 100));
+
+ // Check that the generated image is correct.
+ CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ count++;
+ for (int32_t i = 0; i < aLength; ++i) {
+ aBlockStart[i] = BGRAColor::Red().AsPixel();
+ }
+ return MakeTuple(aLength, Maybe<WriteState>());
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is still correct.
+ CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkWritePixelBlocksPartialRow)
+{
+ WithSurfaceSink<Orient::NORMAL>([](image::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];
+ InitializeRowBuffer(buffer, 100, 20, 80, BGRAColor::Green().AsPixel());
+
+ // 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) {
+ for (int32_t written = 0; written < 100;) {
+ WriteState result = aSink->WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ // When we write the final block of pixels, it will request we
+ // start another row. We should abort at that point.
+ if (aLength == int32_t(100) && written == int32_t(100)) {
+ return MakeTuple(int32_t(0), Some(WriteState::NEED_MORE_DATA));
+ }
+
+ // It should always request enough data to fill the row. So it
+ // should request 100, 75, 50, and finally 25 pixels.
+ EXPECT_EQ(int32_t(100) - written, aLength);
+
+ // Only write one quarter of the pixels for the row.
+ memcpy(aBlockStart, &buffer[written], 25 * sizeof(uint32_t));
+ written += 25;
+
+ // We've written the last pixels remaining for the row.
+ if (written == int32_t(100)) {
+ return MakeTuple(int32_t(25), Maybe<WriteState>());
+ }
+
+ // We've written another quarter of the row but not yet all of it.
+ return MakeTuple(int32_t(25), Some(WriteState::NEED_MORE_DATA));
+ });
+
+ EXPECT_EQ(WriteState::NEED_MORE_DATA, result);
+ }
+
+ 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(20, 0, 60, row + 1));
+ }
+
+ // Write the final line, which should finish the surface.
+ uint32_t count = 0;
+ WriteState result = aSink->WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ ++count;
+ EXPECT_EQ(int32_t(100), aLength);
+ memcpy(aBlockStart, buffer, 100 * sizeof(uint32_t));
+ return MakeTuple(int32_t(100), Maybe<WriteState>());
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(1u, 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(20, 0, 60, 100));
+
+ // Attempt to write more and make sure that nothing gets written.
+ count = 0;
+ result = aSink->WritePixelBlocks<uint32_t>(
+ [&](uint32_t* aBlockStart, int32_t aLength) {
+ count++;
+ for (int32_t i = 0; i < aLength; ++i) {
+ aBlockStart[i] = BGRAColor::Red().AsPixel();
+ }
+ return MakeTuple(aLength, Maybe<WriteState>());
+ });
+
+ EXPECT_EQ(WriteState::FINISHED, result);
+ EXPECT_EQ(0u, count);
+ EXPECT_TRUE(aSink->IsSurfaceFinished());
+
+ // Check that the generated image is still correct.
+ CheckGeneratedImage(aDecoder, IntRect(20, 0, 60, 100));
+ });
+}
+
+TEST(ImageSurfaceSink, SurfaceSinkProgressivePasses)
+{
+ WithSurfaceSink<Orient::NORMAL>(
+ [](image::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>([](image::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>([](image::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()));
+ }
+ });
+}
diff --git a/image/test/gtest/TestSwizzleFilter.cpp b/image/test/gtest/TestSwizzleFilter.cpp
new file mode 100644
index 0000000000..65faf85155
--- /dev/null
+++ b/image/test/gtest/TestSwizzleFilter.cpp
@@ -0,0 +1,120 @@
+/* -*- 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 "SurfaceFilters.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+template <typename Func>
+void WithSwizzleFilter(const IntSize& aSize, SurfaceFormat aInputFormat,
+ SurfaceFormat aOutputFormat, bool aPremultiplyAlpha,
+ Func aFunc) {
+ RefPtr<image::Decoder> decoder = CreateTrivialDecoder();
+ ASSERT_TRUE(decoder != nullptr);
+
+ WithFilterPipeline(
+ decoder, std::forward<Func>(aFunc),
+ SwizzleConfig{aInputFormat, aOutputFormat, aPremultiplyAlpha},
+ SurfaceConfig{decoder, aSize, aOutputFormat, false});
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_BGRA)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8A8,
+ false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter, BGRAColor::Blue(),
+ BGRAColor::Red());
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_BGRA)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8A8, true,
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(
+ aDecoder, aFilter, BGRAColor(0x26, 0x00, 0x00, 0x7F, true),
+ BGRAColor(0x00, 0x00, 0x26, 0x7F), Nothing(), Nothing(), Nothing(),
+ Nothing(), /* aFuzz */ 1);
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_BGRX)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8X8,
+ false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter,
+ BGRAColor(0x26, 0x00, 0x00, 0x7F, true),
+ BGRAColor(0x00, 0x00, 0x26, 0xFF));
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_BGRX)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::B8G8R8X8, true,
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter,
+ BGRAColor(0x26, 0x00, 0x00, 0x7F, true),
+ BGRAColor(0x00, 0x00, 0x13, 0xFF));
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_RGBX)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::R8G8B8X8,
+ false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter,
+ BGRAColor(0x00, 0x00, 0x26, 0x7F, true),
+ BGRAColor(0x00, 0x00, 0x26, 0xFF));
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_RGBA_to_Premultiplied_RGRX)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::R8G8B8A8, SurfaceFormat::R8G8B8X8, true,
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter,
+ BGRAColor(0x00, 0x00, 0x26, 0x7F, true),
+ BGRAColor(0x00, 0x00, 0x13, 0xFF));
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_BGRA_to_BGRX)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::B8G8R8A8, SurfaceFormat::B8G8R8X8,
+ false, [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(aDecoder, aFilter,
+ BGRAColor(0x10, 0x26, 0x00, 0x7F, true),
+ BGRAColor(0x10, 0x26, 0x00, 0xFF));
+ });
+}
+
+TEST(ImageSwizzleFilter, WritePixels_BGRA_to_Premultiplied_BGRA)
+{
+ WithSwizzleFilter(
+ IntSize(100, 100), SurfaceFormat::B8G8R8A8, SurfaceFormat::B8G8R8A8, true,
+ [](image::Decoder* aDecoder, SurfaceFilter* aFilter) {
+ CheckTransformedWritePixels(
+ aDecoder, aFilter, BGRAColor(0x10, 0x26, 0x00, 0x7F, true),
+ BGRAColor(0x10, 0x26, 0x00, 0x7F), Nothing(), Nothing(), Nothing(),
+ Nothing(), /* aFuzz */ 1);
+ });
+}
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 0000000000..a145c814a6
--- /dev/null
+++ b/image/test/gtest/animated-with-extra-image-sub-blocks.gif
Binary files differ
diff --git a/image/test/gtest/blend.gif b/image/test/gtest/blend.gif
new file mode 100644
index 0000000000..2f7391454c
--- /dev/null
+++ b/image/test/gtest/blend.gif
Binary files differ
diff --git a/image/test/gtest/blend.png b/image/test/gtest/blend.png
new file mode 100644
index 0000000000..c4e739f068
--- /dev/null
+++ b/image/test/gtest/blend.png
Binary files differ
diff --git a/image/test/gtest/blend.webp b/image/test/gtest/blend.webp
new file mode 100644
index 0000000000..1b95e6f377
--- /dev/null
+++ b/image/test/gtest/blend.webp
Binary files differ
diff --git a/image/test/gtest/bug-1655846.avif b/image/test/gtest/bug-1655846.avif
new file mode 100644
index 0000000000..31c7e42454
--- /dev/null
+++ b/image/test/gtest/bug-1655846.avif
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 0000000000..ee4a90fcd7
--- /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 0000000000..aa4051cd07
--- /dev/null
+++ b/image/test/gtest/corrupt-with-bad-bmp-width.ico
Binary files differ
diff --git a/image/test/gtest/corrupt-with-bad-ico-bpp.ico b/image/test/gtest/corrupt-with-bad-ico-bpp.ico
new file mode 100644
index 0000000000..5db4922e34
--- /dev/null
+++ b/image/test/gtest/corrupt-with-bad-ico-bpp.ico
Binary files differ
diff --git a/image/test/gtest/corrupt.jpg b/image/test/gtest/corrupt.jpg
new file mode 100644
index 0000000000..555a416d7d
--- /dev/null
+++ b/image/test/gtest/corrupt.jpg
Binary files differ
diff --git a/image/test/gtest/downscaled.avif b/image/test/gtest/downscaled.avif
new file mode 100644
index 0000000000..15aa14d240
--- /dev/null
+++ b/image/test/gtest/downscaled.avif
Binary files differ
diff --git a/image/test/gtest/downscaled.bmp b/image/test/gtest/downscaled.bmp
new file mode 100644
index 0000000000..9e6a29e62b
--- /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 0000000000..ff9a20bcdb
--- /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 0000000000..ee112af0a9
--- /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 0000000000..0ec9139866
--- /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 0000000000..5a4b3cd036
--- /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 0000000000..b71b4652d5
--- /dev/null
+++ b/image/test/gtest/downscaled.png
Binary files differ
diff --git a/image/test/gtest/downscaled.webp b/image/test/gtest/downscaled.webp
new file mode 100644
index 0000000000..c2db6d6446
--- /dev/null
+++ b/image/test/gtest/downscaled.webp
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 0000000000..cd3c7d3db8
--- /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 0000000000..115f035d89
--- /dev/null
+++ b/image/test/gtest/first-frame-green.png
Binary files differ
diff --git a/image/test/gtest/first-frame-green.webp b/image/test/gtest/first-frame-green.webp
new file mode 100644
index 0000000000..44db5c71c3
--- /dev/null
+++ b/image/test/gtest/first-frame-green.webp
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 0000000000..e6d7c49322
--- /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 0000000000..0829f9694d
--- /dev/null
+++ b/image/test/gtest/green-1x1-truncated.gif
Binary files differ
diff --git a/image/test/gtest/green-large-bmp.ico b/image/test/gtest/green-large-bmp.ico
new file mode 100644
index 0000000000..3962cea29d
--- /dev/null
+++ b/image/test/gtest/green-large-bmp.ico
Binary files differ
diff --git a/image/test/gtest/green-large-png.ico b/image/test/gtest/green-large-png.ico
new file mode 100644
index 0000000000..27b9f43cdd
--- /dev/null
+++ b/image/test/gtest/green-large-png.ico
Binary files differ
diff --git a/image/test/gtest/green-multiple-sizes.ico b/image/test/gtest/green-multiple-sizes.ico
new file mode 100644
index 0000000000..b9463d0c89
--- /dev/null
+++ b/image/test/gtest/green-multiple-sizes.ico
Binary files differ
diff --git a/image/test/gtest/green.avif b/image/test/gtest/green.avif
new file mode 100644
index 0000000000..226ed21180
--- /dev/null
+++ b/image/test/gtest/green.avif
Binary files differ
diff --git a/image/test/gtest/green.bmp b/image/test/gtest/green.bmp
new file mode 100644
index 0000000000..f79dd672ad
--- /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 0000000000..ef215dfc94
--- /dev/null
+++ b/image/test/gtest/green.gif
Binary files differ
diff --git a/image/test/gtest/green.icc_srgb.webp b/image/test/gtest/green.icc_srgb.webp
new file mode 100644
index 0000000000..2a869b447b
--- /dev/null
+++ b/image/test/gtest/green.icc_srgb.webp
Binary files differ
diff --git a/image/test/gtest/green.ico b/image/test/gtest/green.ico
new file mode 100644
index 0000000000..c5dfa8b538
--- /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 0000000000..1de4eeb783
--- /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 0000000000..48c454d27c
--- /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 0000000000..7df25f33bd
--- /dev/null
+++ b/image/test/gtest/green.png
Binary files differ
diff --git a/image/test/gtest/green.webp b/image/test/gtest/green.webp
new file mode 100644
index 0000000000..04b7f003b4
--- /dev/null
+++ b/image/test/gtest/green.webp
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 0000000000..228c5c9992
--- /dev/null
+++ b/image/test/gtest/invalid-truncated-metadata.bmp
Binary files differ
diff --git a/image/test/gtest/large.avif b/image/test/gtest/large.avif
new file mode 100644
index 0000000000..fbdf084148
--- /dev/null
+++ b/image/test/gtest/large.avif
Binary files differ
diff --git a/image/test/gtest/large.webp b/image/test/gtest/large.webp
new file mode 100644
index 0000000000..9bf0b64fa8
--- /dev/null
+++ b/image/test/gtest/large.webp
Binary files differ
diff --git a/image/test/gtest/moz.build b/image/test/gtest/moz.build
new file mode 100644
index 0000000000..6bb705124e
--- /dev/null
+++ b/image/test/gtest/moz.build
@@ -0,0 +1,124 @@
+# -*- 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",
+ "TestAnimationFrameBuffer.cpp",
+ "TestBlendAnimationFilter.cpp",
+ "TestContainers.cpp",
+ "TestCopyOnWrite.cpp",
+ "TestDeinterlacingFilter.cpp",
+ "TestFrameAnimator.cpp",
+ "TestLoader.cpp",
+ "TestRemoveFrameRectFilter.cpp",
+ "TestStreamingLexer.cpp",
+ "TestSurfaceSink.cpp",
+ "TestSwizzleFilter.cpp",
+]
+
+# skip the test on windows10-aarch64, aarch64 due to 1544961
+if not (CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CPU_ARCH"] == "aarch64"):
+ UNIFIED_SOURCES += [
+ "TestDecoders.cpp",
+ "TestDecodersPerf.cpp",
+ "TestDecodeToSurface.cpp",
+ "TestMetadata.cpp",
+ "TestSourceBuffer.cpp",
+ "TestSurfaceCache.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",
+ "blend.gif",
+ "blend.png",
+ "blend.webp",
+ "bug-1655846.avif",
+ "corrupt-with-bad-bmp-height.ico",
+ "corrupt-with-bad-bmp-width.ico",
+ "corrupt-with-bad-ico-bpp.ico",
+ "corrupt.jpg",
+ "downscaled.avif",
+ "downscaled.bmp",
+ "downscaled.gif",
+ "downscaled.ico",
+ "downscaled.icon",
+ "downscaled.jpg",
+ "downscaled.png",
+ "downscaled.webp",
+ "first-frame-green.gif",
+ "first-frame-green.png",
+ "first-frame-green.webp",
+ "first-frame-padding.gif",
+ "green-1x1-truncated.gif",
+ "green-large-bmp.ico",
+ "green-large-png.ico",
+ "green-multiple-sizes.ico",
+ "green.avif",
+ "green.bmp",
+ "green.gif",
+ "green.icc_srgb.webp",
+ "green.ico",
+ "green.icon",
+ "green.jpg",
+ "green.png",
+ "green.webp",
+ "invalid-truncated-metadata.bmp",
+ "large.avif",
+ "large.webp",
+ "multilayer.avif",
+ "no-frame-delay.gif",
+ "perf_cmyk.jpg",
+ "perf_gray.jpg",
+ "perf_gray.png",
+ "perf_gray_alpha.png",
+ "perf_srgb.gif",
+ "perf_srgb.png",
+ "perf_srgb_alpha.png",
+ "perf_srgb_alpha_lossless.webp",
+ "perf_srgb_alpha_lossy.webp",
+ "perf_srgb_lossless.webp",
+ "perf_srgb_lossy.webp",
+ "perf_ycbcr.jpg",
+ "rle4.bmp",
+ "rle8.bmp",
+ "stackcheck.avif",
+ "transparent-ico-with-and-mask.ico",
+ "transparent-if-within-ico.bmp",
+ "transparent-no-alpha-header.webp",
+ "transparent.avif",
+ "transparent.gif",
+ "transparent.png",
+ "transparent.webp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/gfx/2d",
+ "/image",
+]
+
+LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"]
+
+FINAL_LIBRARY = "xul-gtest"
+
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-error=shadow"]
diff --git a/image/test/gtest/multilayer.avif b/image/test/gtest/multilayer.avif
new file mode 100644
index 0000000000..91857fde54
--- /dev/null
+++ b/image/test/gtest/multilayer.avif
Binary files differ
diff --git a/image/test/gtest/no-frame-delay.gif b/image/test/gtest/no-frame-delay.gif
new file mode 100644
index 0000000000..1c50b67431
--- /dev/null
+++ b/image/test/gtest/no-frame-delay.gif
Binary files differ
diff --git a/image/test/gtest/perf_cmyk.jpg b/image/test/gtest/perf_cmyk.jpg
new file mode 100644
index 0000000000..e9d329f21e
--- /dev/null
+++ b/image/test/gtest/perf_cmyk.jpg
Binary files differ
diff --git a/image/test/gtest/perf_gray.jpg b/image/test/gtest/perf_gray.jpg
new file mode 100644
index 0000000000..ed75b91550
--- /dev/null
+++ b/image/test/gtest/perf_gray.jpg
Binary files differ
diff --git a/image/test/gtest/perf_gray.png b/image/test/gtest/perf_gray.png
new file mode 100644
index 0000000000..df16c72fb6
--- /dev/null
+++ b/image/test/gtest/perf_gray.png
Binary files differ
diff --git a/image/test/gtest/perf_gray_alpha.png b/image/test/gtest/perf_gray_alpha.png
new file mode 100644
index 0000000000..fc38ec549b
--- /dev/null
+++ b/image/test/gtest/perf_gray_alpha.png
Binary files differ
diff --git a/image/test/gtest/perf_srgb.gif b/image/test/gtest/perf_srgb.gif
new file mode 100644
index 0000000000..4dadf118b5
--- /dev/null
+++ b/image/test/gtest/perf_srgb.gif
Binary files differ
diff --git a/image/test/gtest/perf_srgb.png b/image/test/gtest/perf_srgb.png
new file mode 100644
index 0000000000..21f28081c2
--- /dev/null
+++ b/image/test/gtest/perf_srgb.png
Binary files differ
diff --git a/image/test/gtest/perf_srgb_alpha.png b/image/test/gtest/perf_srgb_alpha.png
new file mode 100644
index 0000000000..1fa7fed59b
--- /dev/null
+++ b/image/test/gtest/perf_srgb_alpha.png
Binary files differ
diff --git a/image/test/gtest/perf_srgb_alpha_lossless.webp b/image/test/gtest/perf_srgb_alpha_lossless.webp
new file mode 100644
index 0000000000..cce4c24ff4
--- /dev/null
+++ b/image/test/gtest/perf_srgb_alpha_lossless.webp
Binary files differ
diff --git a/image/test/gtest/perf_srgb_alpha_lossy.webp b/image/test/gtest/perf_srgb_alpha_lossy.webp
new file mode 100644
index 0000000000..1bc08edc7d
--- /dev/null
+++ b/image/test/gtest/perf_srgb_alpha_lossy.webp
Binary files differ
diff --git a/image/test/gtest/perf_srgb_lossless.webp b/image/test/gtest/perf_srgb_lossless.webp
new file mode 100644
index 0000000000..ae85a41237
--- /dev/null
+++ b/image/test/gtest/perf_srgb_lossless.webp
Binary files differ
diff --git a/image/test/gtest/perf_srgb_lossy.webp b/image/test/gtest/perf_srgb_lossy.webp
new file mode 100644
index 0000000000..3caad7ceca
--- /dev/null
+++ b/image/test/gtest/perf_srgb_lossy.webp
Binary files differ
diff --git a/image/test/gtest/perf_ycbcr.jpg b/image/test/gtest/perf_ycbcr.jpg
new file mode 100644
index 0000000000..d2ad4e2b20
--- /dev/null
+++ b/image/test/gtest/perf_ycbcr.jpg
Binary files differ
diff --git a/image/test/gtest/rle4.bmp b/image/test/gtest/rle4.bmp
new file mode 100644
index 0000000000..78a0927870
--- /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 0000000000..bd793b6b66
--- /dev/null
+++ b/image/test/gtest/rle8.bmp
Binary files differ
diff --git a/image/test/gtest/stackcheck.avif b/image/test/gtest/stackcheck.avif
new file mode 100644
index 0000000000..fbc9c34dee
--- /dev/null
+++ b/image/test/gtest/stackcheck.avif
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 0000000000..ab0dc4bce1
--- /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 0000000000..4dc04c181b
--- /dev/null
+++ b/image/test/gtest/transparent-if-within-ico.bmp
Binary files differ
diff --git a/image/test/gtest/transparent-no-alpha-header.webp b/image/test/gtest/transparent-no-alpha-header.webp
new file mode 100644
index 0000000000..8ddd73ac7a
--- /dev/null
+++ b/image/test/gtest/transparent-no-alpha-header.webp
Binary files differ
diff --git a/image/test/gtest/transparent.avif b/image/test/gtest/transparent.avif
new file mode 100644
index 0000000000..00ef35bf74
--- /dev/null
+++ b/image/test/gtest/transparent.avif
Binary files differ
diff --git a/image/test/gtest/transparent.gif b/image/test/gtest/transparent.gif
new file mode 100644
index 0000000000..48f5c7caf1
--- /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 0000000000..fc8002053a
--- /dev/null
+++ b/image/test/gtest/transparent.png
Binary files differ
diff --git a/image/test/gtest/transparent.webp b/image/test/gtest/transparent.webp
new file mode 100644
index 0000000000..87b9520521
--- /dev/null
+++ b/image/test/gtest/transparent.webp
Binary files differ