diff --git a/image/Decoder.cpp b/image/Decoder.cpp index de24fac35e87..bde3800ea4da 100644 --- a/image/Decoder.cpp +++ b/image/Decoder.cpp @@ -90,15 +90,18 @@ Decoder::Init() } nsresult -Decoder::Decode() +Decoder::Decode(IResumable* aOnResume) { MOZ_ASSERT(mInitialized, "Should be initialized here"); MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); + // If no IResumable was provided, default to |this|. + IResumable* onResume = aOnResume ? aOnResume : this; + // We keep decoding chunks until the decode completes or there are no more // chunks available. while (!GetDecodeDone() && !HasError()) { - auto newState = mIterator->AdvanceOrScheduleResume(this); + auto newState = mIterator->AdvanceOrScheduleResume(onResume); if (newState == SourceBufferIterator::WAITING) { // We can't continue because the rest of the data hasn't arrived from the diff --git a/image/Decoder.h b/image/Decoder.h index f6969d5e86c1..1d39605e49d6 100644 --- a/image/Decoder.h +++ b/image/Decoder.h @@ -36,13 +36,16 @@ public: void Init(); /** - * Decodes, reading all data currently available in the SourceBuffer. If more - * data is needed, Decode() automatically ensures that it will be called again - * on a DecodePool thread when the data becomes available. + * Decodes, reading all data currently available in the SourceBuffer. + * + * If more data is needed, Decode() will schedule @aOnResume to be called when + * more data is available. If @aOnResume is null or unspecified, the default + * implementation resumes decoding on a DecodePool thread. Most callers should + * use the default implementation. * * Any errors are reported by setting the appropriate state on the decoder. */ - nsresult Decode(); + nsresult Decode(IResumable* aOnResume = nullptr); /** * Given a maximum number of bytes we're willing to decode, @aByteLimit, diff --git a/image/SourceBuffer.cpp b/image/SourceBuffer.cpp index f283c7f79f15..ce008967b4f5 100644 --- a/image/SourceBuffer.cpp +++ b/image/SourceBuffer.cpp @@ -213,10 +213,6 @@ SourceBuffer::AddWaitingConsumer(IResumable* aConsumer) MOZ_ASSERT(!mStatus, "Waiting when we're complete?"); - if (MOZ_UNLIKELY(NS_IsMainThread())) { - NS_WARNING("SourceBuffer consumer on the main thread needed to wait"); - } - mWaitingConsumers.AppendElement(aConsumer); } diff --git a/image/test/gtest/TestDecoders.cpp b/image/test/gtest/TestDecoders.cpp new file mode 100644 index 000000000000..4fa7b906a6e0 --- /dev/null +++ b/image/test/gtest/TestDecoders.cpp @@ -0,0 +1,249 @@ +/* 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 "imgIContainer.h" +#include "imgITools.h" +#include "ImageFactory.h" +#include "mozilla/gfx/2D.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "mozilla/nsRefPtr.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; + +TEST(ImageDecoders, ImageModuleAvailable) +{ + // We can run into problems if XPCOM modules get initialized in the wrong + // order. It's important that this test run first, both as a sanity check and + // to ensure we get the module initialization order we want. + nsCOMPtr imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + EXPECT_TRUE(imgTools != nullptr); +} + +static void +CheckDecoderResults(const ImageTestCase& aTestCase, Decoder* aDecoder) +{ + EXPECT_TRUE(aDecoder->GetDecodeDone()); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), + aDecoder->HasError()); + EXPECT_TRUE(!aDecoder->WasAborted()); + + // 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; // 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->GetSize(); + 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(); + nsRefPtr surface = currentFrame->GetSurface(); + + // Verify that the resulting surfaces matches our expectations. + EXPECT_EQ(SurfaceType::DATA, surface->GetType()); + EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::B8G8R8X8 || + surface->GetFormat() == SurfaceFormat::B8G8R8A8); + EXPECT_EQ(aTestCase.mSize, surface->GetSize()); + EXPECT_TRUE(IsSolidColor(surface, BGRAColor::Green(), + aTestCase.mFlags & TEST_CASE_IS_FUZZY)); +} + +static void +CheckDecoderSingleChunk(const ImageTestCase& aTestCase) +{ + nsCOMPtr 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. + nsRefPtr sourceBuffer = new SourceBuffer(); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + sourceBuffer->Complete(NS_OK); + + // Create a decoder. + DecoderType decoderType = + DecoderFactory::GetDecoderType(aTestCase.mMimeType); + nsRefPtr decoder = + DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, + DefaultSurfaceFlags()); + ASSERT_TRUE(decoder != nullptr); + + // Run the full decoder synchronously. + decoder->Decode(); + + CheckDecoderResults(aTestCase, decoder); +} + +class NoResume : public IResumable +{ +public: + NS_INLINE_DECL_REFCOUNTING(NoResume, override) + virtual void Resume() override { } + +private: + ~NoResume() { } +}; + +static void +CheckDecoderMultiChunk(const ImageTestCase& aTestCase) +{ + nsCOMPtr 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. + nsRefPtr sourceBuffer = new SourceBuffer(); + sourceBuffer->ExpectLength(length); + DecoderType decoderType = + DecoderFactory::GetDecoderType(aTestCase.mMimeType); + nsRefPtr decoder = + DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, + DefaultSurfaceFlags()); + ASSERT_TRUE(decoder != nullptr); + + // Decode synchronously, using a |NoResume| IResumable so the Decoder doesn't + // attempt to schedule itself on a nonexistent DecodePool when we write more + // data into the SourceBuffer. + nsRefPtr noResume = new NoResume(); + for (uint64_t read = 0; read < length ; ++read) { + uint64_t available = 0; + rv = inputStream->Available(&available); + ASSERT_TRUE(available > 0); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + rv = sourceBuffer->AppendFromInputStream(inputStream, 1); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + decoder->Decode(noResume); + } + + sourceBuffer->Complete(NS_OK); + decoder->Decode(noResume); + + CheckDecoderResults(aTestCase, decoder); +} + +TEST(ImageDecoders, PNGSingleChunk) +{ + CheckDecoderSingleChunk(GreenPNGTestCase()); +} + +TEST(ImageDecoders, PNGMultiChunk) +{ + CheckDecoderMultiChunk(GreenPNGTestCase()); +} + +TEST(ImageDecoders, GIFSingleChunk) +{ + CheckDecoderSingleChunk(GreenGIFTestCase()); +} + +TEST(ImageDecoders, GIFMultiChunk) +{ + CheckDecoderMultiChunk(GreenGIFTestCase()); +} + +TEST(ImageDecoders, JPGSingleChunk) +{ + CheckDecoderSingleChunk(GreenJPGTestCase()); +} + +TEST(ImageDecoders, JPGMultiChunk) +{ + CheckDecoderMultiChunk(GreenJPGTestCase()); +} + +TEST(ImageDecoders, BMPSingleChunk) +{ + CheckDecoderSingleChunk(GreenBMPTestCase()); +} + +TEST(ImageDecoders, BMPMultiChunk) +{ + CheckDecoderMultiChunk(GreenBMPTestCase()); +} + +TEST(ImageDecoders, ICOSingleChunk) +{ + CheckDecoderSingleChunk(GreenICOTestCase()); +} + +// XXX(seth): Disabled. We'll fix this in bug 1196066. +TEST(ImageDecoders, DISABLED_ICOMultiChunk) +{ + CheckDecoderMultiChunk(GreenICOTestCase()); +} + +TEST(ImageDecoders, AnimatedGIFSingleChunk) +{ + CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST(ImageDecoders, AnimatedGIFMultiChunk) +{ + CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + +TEST(ImageDecoders, AnimatedPNGSingleChunk) +{ + CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST(ImageDecoders, AnimatedPNGMultiChunk) +{ + CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + +TEST(ImageDecoders, CorruptSingleChunk) +{ + CheckDecoderSingleChunk(CorruptTestCase()); +} + +TEST(ImageDecoders, CorruptMultiChunk) +{ + CheckDecoderMultiChunk(CorruptTestCase()); +} diff --git a/image/test/gtest/moz.build b/image/test/gtest/moz.build index 45f9f96197fb..dd309945d377 100644 --- a/image/test/gtest/moz.build +++ b/image/test/gtest/moz.build @@ -10,6 +10,7 @@ FAIL_ON_WARNINGS = True UNIFIED_SOURCES = [ 'Common.cpp', + 'TestDecoders.cpp', 'TestDecodeToSurface.cpp', 'TestMetadata.cpp', ]