diff options
Diffstat (limited to 'image/test/gtest')
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 Binary files differnew file mode 100644 index 0000000000..a145c814a6 --- /dev/null +++ b/image/test/gtest/animated-with-extra-image-sub-blocks.gif diff --git a/image/test/gtest/blend.gif b/image/test/gtest/blend.gif Binary files differnew file mode 100644 index 0000000000..2f7391454c --- /dev/null +++ b/image/test/gtest/blend.gif diff --git a/image/test/gtest/blend.png b/image/test/gtest/blend.png Binary files differnew file mode 100644 index 0000000000..c4e739f068 --- /dev/null +++ b/image/test/gtest/blend.png diff --git a/image/test/gtest/blend.webp b/image/test/gtest/blend.webp Binary files differnew file mode 100644 index 0000000000..1b95e6f377 --- /dev/null +++ b/image/test/gtest/blend.webp diff --git a/image/test/gtest/bug-1655846.avif b/image/test/gtest/bug-1655846.avif Binary files differnew file mode 100644 index 0000000000..31c7e42454 --- /dev/null +++ b/image/test/gtest/bug-1655846.avif diff --git a/image/test/gtest/corrupt-with-bad-bmp-height.ico b/image/test/gtest/corrupt-with-bad-bmp-height.ico Binary files differnew file mode 100644 index 0000000000..ee4a90fcd7 --- /dev/null +++ b/image/test/gtest/corrupt-with-bad-bmp-height.ico diff --git a/image/test/gtest/corrupt-with-bad-bmp-width.ico b/image/test/gtest/corrupt-with-bad-bmp-width.ico Binary files differnew file mode 100644 index 0000000000..aa4051cd07 --- /dev/null +++ b/image/test/gtest/corrupt-with-bad-bmp-width.ico diff --git a/image/test/gtest/corrupt-with-bad-ico-bpp.ico b/image/test/gtest/corrupt-with-bad-ico-bpp.ico Binary files differnew file mode 100644 index 0000000000..5db4922e34 --- /dev/null +++ b/image/test/gtest/corrupt-with-bad-ico-bpp.ico diff --git a/image/test/gtest/corrupt.jpg b/image/test/gtest/corrupt.jpg Binary files differnew file mode 100644 index 0000000000..555a416d7d --- /dev/null +++ b/image/test/gtest/corrupt.jpg diff --git a/image/test/gtest/downscaled.avif b/image/test/gtest/downscaled.avif Binary files differnew file mode 100644 index 0000000000..15aa14d240 --- /dev/null +++ b/image/test/gtest/downscaled.avif diff --git a/image/test/gtest/downscaled.bmp b/image/test/gtest/downscaled.bmp Binary files differnew file mode 100644 index 0000000000..9e6a29e62b --- /dev/null +++ b/image/test/gtest/downscaled.bmp diff --git a/image/test/gtest/downscaled.gif b/image/test/gtest/downscaled.gif Binary files differnew file mode 100644 index 0000000000..ff9a20bcdb --- /dev/null +++ b/image/test/gtest/downscaled.gif diff --git a/image/test/gtest/downscaled.ico b/image/test/gtest/downscaled.ico Binary files differnew file mode 100644 index 0000000000..ee112af0a9 --- /dev/null +++ b/image/test/gtest/downscaled.ico diff --git a/image/test/gtest/downscaled.icon b/image/test/gtest/downscaled.icon Binary files differnew file mode 100644 index 0000000000..0ec9139866 --- /dev/null +++ b/image/test/gtest/downscaled.icon diff --git a/image/test/gtest/downscaled.jpg b/image/test/gtest/downscaled.jpg Binary files differnew file mode 100644 index 0000000000..5a4b3cd036 --- /dev/null +++ b/image/test/gtest/downscaled.jpg diff --git a/image/test/gtest/downscaled.png b/image/test/gtest/downscaled.png Binary files differnew file mode 100644 index 0000000000..b71b4652d5 --- /dev/null +++ b/image/test/gtest/downscaled.png diff --git a/image/test/gtest/downscaled.webp b/image/test/gtest/downscaled.webp Binary files differnew file mode 100644 index 0000000000..c2db6d6446 --- /dev/null +++ b/image/test/gtest/downscaled.webp diff --git a/image/test/gtest/first-frame-green.gif b/image/test/gtest/first-frame-green.gif Binary files differnew file mode 100644 index 0000000000..cd3c7d3db8 --- /dev/null +++ b/image/test/gtest/first-frame-green.gif diff --git a/image/test/gtest/first-frame-green.png b/image/test/gtest/first-frame-green.png Binary files differnew file mode 100644 index 0000000000..115f035d89 --- /dev/null +++ b/image/test/gtest/first-frame-green.png diff --git a/image/test/gtest/first-frame-green.webp b/image/test/gtest/first-frame-green.webp Binary files differnew file mode 100644 index 0000000000..44db5c71c3 --- /dev/null +++ b/image/test/gtest/first-frame-green.webp diff --git a/image/test/gtest/first-frame-padding.gif b/image/test/gtest/first-frame-padding.gif Binary files differnew file mode 100644 index 0000000000..e6d7c49322 --- /dev/null +++ b/image/test/gtest/first-frame-padding.gif diff --git a/image/test/gtest/green-1x1-truncated.gif b/image/test/gtest/green-1x1-truncated.gif Binary files differnew file mode 100644 index 0000000000..0829f9694d --- /dev/null +++ b/image/test/gtest/green-1x1-truncated.gif diff --git a/image/test/gtest/green-large-bmp.ico b/image/test/gtest/green-large-bmp.ico Binary files differnew file mode 100644 index 0000000000..3962cea29d --- /dev/null +++ b/image/test/gtest/green-large-bmp.ico diff --git a/image/test/gtest/green-large-png.ico b/image/test/gtest/green-large-png.ico Binary files differnew file mode 100644 index 0000000000..27b9f43cdd --- /dev/null +++ b/image/test/gtest/green-large-png.ico diff --git a/image/test/gtest/green-multiple-sizes.ico b/image/test/gtest/green-multiple-sizes.ico Binary files differnew file mode 100644 index 0000000000..b9463d0c89 --- /dev/null +++ b/image/test/gtest/green-multiple-sizes.ico diff --git a/image/test/gtest/green.avif b/image/test/gtest/green.avif Binary files differnew file mode 100644 index 0000000000..226ed21180 --- /dev/null +++ b/image/test/gtest/green.avif diff --git a/image/test/gtest/green.bmp b/image/test/gtest/green.bmp Binary files differnew file mode 100644 index 0000000000..f79dd672ad --- /dev/null +++ b/image/test/gtest/green.bmp diff --git a/image/test/gtest/green.gif b/image/test/gtest/green.gif Binary files differnew file mode 100644 index 0000000000..ef215dfc94 --- /dev/null +++ b/image/test/gtest/green.gif diff --git a/image/test/gtest/green.icc_srgb.webp b/image/test/gtest/green.icc_srgb.webp Binary files differnew file mode 100644 index 0000000000..2a869b447b --- /dev/null +++ b/image/test/gtest/green.icc_srgb.webp diff --git a/image/test/gtest/green.ico b/image/test/gtest/green.ico Binary files differnew file mode 100644 index 0000000000..c5dfa8b538 --- /dev/null +++ b/image/test/gtest/green.ico diff --git a/image/test/gtest/green.icon b/image/test/gtest/green.icon Binary files differnew file mode 100644 index 0000000000..1de4eeb783 --- /dev/null +++ b/image/test/gtest/green.icon diff --git a/image/test/gtest/green.jpg b/image/test/gtest/green.jpg Binary files differnew file mode 100644 index 0000000000..48c454d27c --- /dev/null +++ b/image/test/gtest/green.jpg diff --git a/image/test/gtest/green.png b/image/test/gtest/green.png Binary files differnew file mode 100644 index 0000000000..7df25f33bd --- /dev/null +++ b/image/test/gtest/green.png diff --git a/image/test/gtest/green.webp b/image/test/gtest/green.webp Binary files differnew file mode 100644 index 0000000000..04b7f003b4 --- /dev/null +++ b/image/test/gtest/green.webp diff --git a/image/test/gtest/invalid-truncated-metadata.bmp b/image/test/gtest/invalid-truncated-metadata.bmp Binary files differnew file mode 100644 index 0000000000..228c5c9992 --- /dev/null +++ b/image/test/gtest/invalid-truncated-metadata.bmp diff --git a/image/test/gtest/large.avif b/image/test/gtest/large.avif Binary files differnew file mode 100644 index 0000000000..fbdf084148 --- /dev/null +++ b/image/test/gtest/large.avif diff --git a/image/test/gtest/large.webp b/image/test/gtest/large.webp Binary files differnew file mode 100644 index 0000000000..9bf0b64fa8 --- /dev/null +++ b/image/test/gtest/large.webp 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 Binary files differnew file mode 100644 index 0000000000..91857fde54 --- /dev/null +++ b/image/test/gtest/multilayer.avif diff --git a/image/test/gtest/no-frame-delay.gif b/image/test/gtest/no-frame-delay.gif Binary files differnew file mode 100644 index 0000000000..1c50b67431 --- /dev/null +++ b/image/test/gtest/no-frame-delay.gif diff --git a/image/test/gtest/perf_cmyk.jpg b/image/test/gtest/perf_cmyk.jpg Binary files differnew file mode 100644 index 0000000000..e9d329f21e --- /dev/null +++ b/image/test/gtest/perf_cmyk.jpg diff --git a/image/test/gtest/perf_gray.jpg b/image/test/gtest/perf_gray.jpg Binary files differnew file mode 100644 index 0000000000..ed75b91550 --- /dev/null +++ b/image/test/gtest/perf_gray.jpg diff --git a/image/test/gtest/perf_gray.png b/image/test/gtest/perf_gray.png Binary files differnew file mode 100644 index 0000000000..df16c72fb6 --- /dev/null +++ b/image/test/gtest/perf_gray.png diff --git a/image/test/gtest/perf_gray_alpha.png b/image/test/gtest/perf_gray_alpha.png Binary files differnew file mode 100644 index 0000000000..fc38ec549b --- /dev/null +++ b/image/test/gtest/perf_gray_alpha.png diff --git a/image/test/gtest/perf_srgb.gif b/image/test/gtest/perf_srgb.gif Binary files differnew file mode 100644 index 0000000000..4dadf118b5 --- /dev/null +++ b/image/test/gtest/perf_srgb.gif diff --git a/image/test/gtest/perf_srgb.png b/image/test/gtest/perf_srgb.png Binary files differnew file mode 100644 index 0000000000..21f28081c2 --- /dev/null +++ b/image/test/gtest/perf_srgb.png diff --git a/image/test/gtest/perf_srgb_alpha.png b/image/test/gtest/perf_srgb_alpha.png Binary files differnew file mode 100644 index 0000000000..1fa7fed59b --- /dev/null +++ b/image/test/gtest/perf_srgb_alpha.png diff --git a/image/test/gtest/perf_srgb_alpha_lossless.webp b/image/test/gtest/perf_srgb_alpha_lossless.webp Binary files differnew file mode 100644 index 0000000000..cce4c24ff4 --- /dev/null +++ b/image/test/gtest/perf_srgb_alpha_lossless.webp diff --git a/image/test/gtest/perf_srgb_alpha_lossy.webp b/image/test/gtest/perf_srgb_alpha_lossy.webp Binary files differnew file mode 100644 index 0000000000..1bc08edc7d --- /dev/null +++ b/image/test/gtest/perf_srgb_alpha_lossy.webp diff --git a/image/test/gtest/perf_srgb_lossless.webp b/image/test/gtest/perf_srgb_lossless.webp Binary files differnew file mode 100644 index 0000000000..ae85a41237 --- /dev/null +++ b/image/test/gtest/perf_srgb_lossless.webp diff --git a/image/test/gtest/perf_srgb_lossy.webp b/image/test/gtest/perf_srgb_lossy.webp Binary files differnew file mode 100644 index 0000000000..3caad7ceca --- /dev/null +++ b/image/test/gtest/perf_srgb_lossy.webp diff --git a/image/test/gtest/perf_ycbcr.jpg b/image/test/gtest/perf_ycbcr.jpg Binary files differnew file mode 100644 index 0000000000..d2ad4e2b20 --- /dev/null +++ b/image/test/gtest/perf_ycbcr.jpg diff --git a/image/test/gtest/rle4.bmp b/image/test/gtest/rle4.bmp Binary files differnew file mode 100644 index 0000000000..78a0927870 --- /dev/null +++ b/image/test/gtest/rle4.bmp diff --git a/image/test/gtest/rle8.bmp b/image/test/gtest/rle8.bmp Binary files differnew file mode 100644 index 0000000000..bd793b6b66 --- /dev/null +++ b/image/test/gtest/rle8.bmp diff --git a/image/test/gtest/stackcheck.avif b/image/test/gtest/stackcheck.avif Binary files differnew file mode 100644 index 0000000000..fbc9c34dee --- /dev/null +++ b/image/test/gtest/stackcheck.avif diff --git a/image/test/gtest/transparent-ico-with-and-mask.ico b/image/test/gtest/transparent-ico-with-and-mask.ico Binary files differnew file mode 100644 index 0000000000..ab0dc4bce1 --- /dev/null +++ b/image/test/gtest/transparent-ico-with-and-mask.ico diff --git a/image/test/gtest/transparent-if-within-ico.bmp b/image/test/gtest/transparent-if-within-ico.bmp Binary files differnew file mode 100644 index 0000000000..4dc04c181b --- /dev/null +++ b/image/test/gtest/transparent-if-within-ico.bmp diff --git a/image/test/gtest/transparent-no-alpha-header.webp b/image/test/gtest/transparent-no-alpha-header.webp Binary files differnew file mode 100644 index 0000000000..8ddd73ac7a --- /dev/null +++ b/image/test/gtest/transparent-no-alpha-header.webp diff --git a/image/test/gtest/transparent.avif b/image/test/gtest/transparent.avif Binary files differnew file mode 100644 index 0000000000..00ef35bf74 --- /dev/null +++ b/image/test/gtest/transparent.avif diff --git a/image/test/gtest/transparent.gif b/image/test/gtest/transparent.gif Binary files differnew file mode 100644 index 0000000000..48f5c7caf1 --- /dev/null +++ b/image/test/gtest/transparent.gif diff --git a/image/test/gtest/transparent.png b/image/test/gtest/transparent.png Binary files differnew file mode 100644 index 0000000000..fc8002053a --- /dev/null +++ b/image/test/gtest/transparent.png diff --git a/image/test/gtest/transparent.webp b/image/test/gtest/transparent.webp Binary files differnew file mode 100644 index 0000000000..87b9520521 --- /dev/null +++ b/image/test/gtest/transparent.webp |