/* -*- 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 "Decoder.h" #include "mozilla/gfx/2D.h" #include "DecodePool.h" #include "GeckoProfiler.h" #include "imgIContainer.h" #include "nsIConsoleService.h" #include "nsIScriptError.h" #include "nsProxyRelease.h" #include "nsServiceManagerUtils.h" #include "nsComponentManagerUtils.h" #include "mozilla/Telemetry.h" using mozilla::gfx::IntSize; using mozilla::gfx::SurfaceFormat; namespace mozilla { namespace image { Decoder::Decoder(RasterImage* aImage) : mImage(aImage) , mProgress(NoProgress) , mImageData(nullptr) , mColormap(nullptr) , mChunkCount(0) , mFlags(0) , mBytesDecoded(0) , mSendPartialInvalidations(false) , mDataDone(false) , mDecodeDone(false) , mDataError(false) , mDecodeAborted(false) , mShouldReportError(false) , mImageIsTransient(false) , mImageIsLocked(false) , mFrameCount(0) , mFailCode(NS_OK) , mInitialized(false) , mMetadataDecode(false) , mInFrame(false) , mIsAnimated(false) { } Decoder::~Decoder() { MOZ_ASSERT(mProgress == NoProgress, "Destroying Decoder without taking all its progress changes"); MOZ_ASSERT(mInvalidRect.IsEmpty(), "Destroying Decoder without taking all its invalidations"); mInitialized = false; if (!NS_IsMainThread()) { // Dispatch mImage to main thread to prevent it from being destructed by the // decode thread. nsCOMPtr mainThread = do_GetMainThread(); NS_WARN_IF_FALSE(mainThread, "Couldn't get the main thread!"); if (mainThread) { // Handle ambiguous nsISupports inheritance. RasterImage* rawImg = nullptr; mImage.swap(rawImg); DebugOnly rv = NS_ProxyRelease(mainThread, NS_ISUPPORTS_CAST(ImageResource*, rawImg)); MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to proxy release to main thread"); } } } /* * Common implementation of the decoder interface. */ void Decoder::Init() { // No re-initializing MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!"); // Implementation-specific initialization InitInternal(); mInitialized = true; } nsresult Decoder::Decode() { MOZ_ASSERT(mInitialized, "Should be initialized here"); MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); // We keep decoding chunks until the decode completes or there are no more // chunks available. while (!GetDecodeDone() && !HasError()) { auto newState = mIterator->AdvanceOrScheduleResume(this); if (newState == SourceBufferIterator::WAITING) { // We can't continue because the rest of the data hasn't arrived from the // network yet. We don't have to do anything special; the // SourceBufferIterator will ensure that Decode() gets called again on a // DecodePool thread when more data is available. return NS_OK; } if (newState == SourceBufferIterator::COMPLETE) { mDataDone = true; nsresult finalStatus = mIterator->CompletionStatus(); if (NS_FAILED(finalStatus)) { PostDataError(); } CompleteDecode(); return finalStatus; } MOZ_ASSERT(newState == SourceBufferIterator::READY); Write(mIterator->Data(), mIterator->Length()); } CompleteDecode(); return HasError() ? NS_ERROR_FAILURE : NS_OK; } void Decoder::Resume() { DecodePool* decodePool = DecodePool::Singleton(); MOZ_ASSERT(decodePool); decodePool->AsyncDecode(this); } bool Decoder::ShouldSyncDecode(size_t aByteLimit) { MOZ_ASSERT(aByteLimit > 0); MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); return mIterator->RemainingBytesIsNoMoreThan(aByteLimit); } void Decoder::Write(const char* aBuffer, uint32_t aCount) { PROFILER_LABEL("ImageDecoder", "Write", js::ProfileEntry::Category::GRAPHICS); MOZ_ASSERT(aBuffer); MOZ_ASSERT(aCount > 0); // We're strict about decoder errors MOZ_ASSERT(!HasDecoderError(), "Not allowed to make more decoder calls after error!"); // Begin recording telemetry data. TimeStamp start = TimeStamp::Now(); mChunkCount++; // Keep track of the total number of bytes written. mBytesDecoded += aCount; // If a data error occured, just ignore future data. if (HasDataError()) { return; } if (IsMetadataDecode() && HasSize()) { // More data came in since we found the size. We have nothing to do here. return; } // Pass the data along to the implementation. WriteInternal(aBuffer, aCount); // Finish telemetry. mDecodeTime += (TimeStamp::Now() - start); } void Decoder::CompleteDecode() { // Implementation-specific finalization if (!HasError()) { FinishInternal(); } else { FinishWithErrorInternal(); } // If the implementation left us mid-frame, finish that up. if (mInFrame && !HasError()) { PostFrameStop(); } // If PostDecodeDone() has not been called, and this decoder wasn't aborted // early because of low-memory conditions or losing a race with another // decoder, we need to send teardown notifications (and report an error to the // console later). if (!IsMetadataDecode() && !mDecodeDone && !WasAborted()) { mShouldReportError = true; // If we only have a data error, we're usable if we have at least one // complete frame. if (!HasDecoderError() && GetCompleteFrameCount() > 0) { // We're usable, so do exactly what we should have when the decoder // completed. // Not writing to the entire frame may have left us transparent. PostHasTransparency(); if (mInFrame) { PostFrameStop(); } PostDecodeDone(); } else { // We're not usable. Record some final progress indicating the error. if (!IsMetadataDecode()) { mProgress |= FLAG_DECODE_COMPLETE; } mProgress |= FLAG_HAS_ERROR; } } } void Decoder::Finish() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(HasError() || !mInFrame, "Finishing while we're still in a frame"); // If we detected an error in CompleteDecode(), log it to the error console. if (mShouldReportError && !WasAborted()) { nsCOMPtr consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID); nsCOMPtr errorObject = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); if (consoleService && errorObject && !HasDecoderError()) { nsAutoString msg(NS_LITERAL_STRING("Image corrupt or truncated.")); nsAutoString src; if (mImage->GetURI()) { nsCString uri; if (mImage->GetURI()->GetSpecTruncatedTo1k(uri) == ImageURL::TruncatedTo1k) { msg += NS_LITERAL_STRING(" URI in this note truncated due to length."); } src = NS_ConvertUTF8toUTF16(uri); } if (NS_SUCCEEDED(errorObject->InitWithWindowID( msg, src, EmptyString(), 0, 0, nsIScriptError::errorFlag, "Image", mImage->InnerWindowID() ))) { consoleService->LogMessage(errorObject); } } } // Set image metadata before calling DecodingComplete, because // DecodingComplete calls Optimize(). mImageMetadata.SetOnImage(mImage); if (HasSize()) { SetSizeOnImage(); } if (mDecodeDone && !IsMetadataDecode()) { MOZ_ASSERT(HasError() || mCurrentFrame, "Should have an error or a frame"); // If this image wasn't animated and isn't a transient image, mark its frame // as optimizable. We don't support optimizing animated images and // optimizing transient images isn't worth it. if (!mIsAnimated && !mImageIsTransient && mCurrentFrame) { mCurrentFrame->SetOptimizable(); } mImage->OnDecodingComplete(mIsAnimated); } } nsresult Decoder::AllocateFrame(uint32_t aFrameNum, const nsIntSize& aTargetSize, const nsIntRect& aFrameRect, gfx::SurfaceFormat aFormat, uint8_t aPaletteDepth) { mCurrentFrame = AllocateFrameInternal(aFrameNum, aTargetSize, aFrameRect, GetDecodeFlags(), aFormat, aPaletteDepth, mCurrentFrame.get()); if (mCurrentFrame) { // Gather the raw pointers the decoders will use. mCurrentFrame->GetImageData(&mImageData, &mImageDataLength); mCurrentFrame->GetPaletteData(&mColormap, &mColormapSize); if (aFrameNum + 1 == mFrameCount) { PostFrameStart(); } } else { PostDataError(); } return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE; } RawAccessFrameRef Decoder::AllocateFrameInternal(uint32_t aFrameNum, const nsIntSize& aTargetSize, const nsIntRect& aFrameRect, uint32_t aDecodeFlags, SurfaceFormat aFormat, uint8_t aPaletteDepth, imgFrame* aPreviousFrame) { if (mDataError || NS_FAILED(mFailCode)) { return RawAccessFrameRef(); } if (aFrameNum != mFrameCount) { MOZ_ASSERT_UNREACHABLE("Allocating frames out of order"); return RawAccessFrameRef(); } if (aTargetSize.width <= 0 || aTargetSize.height <= 0 || aFrameRect.width <= 0 || aFrameRect.height <= 0) { NS_WARNING("Trying to add frame with zero or negative size"); return RawAccessFrameRef(); } const uint32_t bytesPerPixel = aPaletteDepth == 0 ? 4 : 1; if (!SurfaceCache::CanHold(aFrameRect.Size(), bytesPerPixel)) { NS_WARNING("Trying to add frame that's too large for the SurfaceCache"); return RawAccessFrameRef(); } nsRefPtr frame = new imgFrame(); bool nonPremult = aDecodeFlags & imgIContainer::FLAG_DECODE_NO_PREMULTIPLY_ALPHA; if (NS_FAILED(frame->InitForDecoder(aTargetSize, aFrameRect, aFormat, aPaletteDepth, nonPremult))) { NS_WARNING("imgFrame::Init should succeed"); return RawAccessFrameRef(); } RawAccessFrameRef ref = frame->RawAccessRef(); if (!ref) { frame->Abort(); return RawAccessFrameRef(); } InsertOutcome outcome = SurfaceCache::Insert(frame, ImageKey(mImage.get()), RasterSurfaceKey(aTargetSize, aDecodeFlags, aFrameNum), Lifetime::Persistent); if (outcome == InsertOutcome::FAILURE) { // We couldn't insert the surface, almost certainly due to low memory. We // treat this as a permanent error to help the system recover; otherwise, we // might just end up attempting to decode this image again immediately. ref->Abort(); return RawAccessFrameRef(); } else if (outcome == InsertOutcome::FAILURE_ALREADY_PRESENT) { // Another decoder beat us to decoding this frame. We abort this decoder // rather than treat this as a real error. mDecodeAborted = true; ref->Abort(); return RawAccessFrameRef(); } nsIntRect refreshArea; if (aFrameNum == 1) { MOZ_ASSERT(aPreviousFrame, "Must provide a previous frame when animated"); aPreviousFrame->SetRawAccessOnly(); // If we dispose of the first frame by clearing it, then the first frame's // refresh area is all of itself. // RESTORE_PREVIOUS is invalid (assumed to be DISPOSE_CLEAR). AnimationData previousFrameData = aPreviousFrame->GetAnimationData(); if (previousFrameData.mDisposalMethod == DisposalMethod::CLEAR || previousFrameData.mDisposalMethod == DisposalMethod::CLEAR_ALL || previousFrameData.mDisposalMethod == DisposalMethod::RESTORE_PREVIOUS) { refreshArea = previousFrameData.mRect; } } if (aFrameNum > 0) { ref->SetRawAccessOnly(); // Some GIFs are huge but only have a small area that they animate. We only // need to refresh that small area when frame 0 comes around again. refreshArea.UnionRect(refreshArea, frame->GetRect()); } mFrameCount++; mImage->OnAddedFrame(mFrameCount, refreshArea); return ref; } void Decoder::SetSizeOnImage() { MOZ_ASSERT(mImageMetadata.HasSize(), "Should have size"); MOZ_ASSERT(mImageMetadata.HasOrientation(), "Should have orientation"); nsresult rv = mImage->SetSize(mImageMetadata.GetWidth(), mImageMetadata.GetHeight(), mImageMetadata.GetOrientation()); if (NS_FAILED(rv)) { PostResizeError(); } } /* * Hook stubs. Override these as necessary in decoder implementations. */ void Decoder::InitInternal() { } void Decoder::FinishInternal() { } void Decoder::FinishWithErrorInternal() { } /* * Progress Notifications */ void Decoder::PostSize(int32_t aWidth, int32_t aHeight, Orientation aOrientation /* = Orientation()*/) { // Validate MOZ_ASSERT(aWidth >= 0, "Width can't be negative!"); MOZ_ASSERT(aHeight >= 0, "Height can't be negative!"); // Tell the image mImageMetadata.SetSize(aWidth, aHeight, aOrientation); // Record this notification. mProgress |= FLAG_SIZE_AVAILABLE; } void Decoder::PostHasTransparency() { mProgress |= FLAG_HAS_TRANSPARENCY; } void Decoder::PostFrameStart() { // We shouldn't already be mid-frame MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!"); // Update our state to reflect the new frame mInFrame = true; // If we just became animated, record that fact. if (mFrameCount > 1) { mIsAnimated = true; mProgress |= FLAG_IS_ANIMATED; } } void Decoder::PostFrameStop(Opacity aFrameOpacity /* = Opacity::TRANSPARENT */, DisposalMethod aDisposalMethod /* = DisposalMethod::KEEP */, int32_t aTimeout /* = 0 */, BlendMethod aBlendMethod /* = BlendMethod::OVER */) { // We should be mid-frame MOZ_ASSERT(!IsMetadataDecode(), "Stopping frame during metadata decode"); MOZ_ASSERT(mInFrame, "Stopping frame when we didn't start one"); MOZ_ASSERT(mCurrentFrame, "Stopping frame when we don't have one"); // Update our state mInFrame = false; mCurrentFrame->Finish(aFrameOpacity, aDisposalMethod, aTimeout, aBlendMethod); mProgress |= FLAG_FRAME_COMPLETE; // If we're not sending partial invalidations, then we send an invalidation // here when the first frame is complete. if (!mSendPartialInvalidations && !mIsAnimated) { mInvalidRect.UnionRect(mInvalidRect, gfx::IntRect(gfx::IntPoint(0, 0), GetSize())); } } void Decoder::PostInvalidation(const nsIntRect& aRect, const Maybe& aRectAtTargetSize /* = Nothing() */) { // We should be mid-frame MOZ_ASSERT(mInFrame, "Can't invalidate when not mid-frame!"); MOZ_ASSERT(mCurrentFrame, "Can't invalidate when not mid-frame!"); // Record this invalidation, unless we're not sending partial invalidations // or we're past the first frame. if (mSendPartialInvalidations && !mIsAnimated) { mInvalidRect.UnionRect(mInvalidRect, aRect); mCurrentFrame->ImageUpdated(aRectAtTargetSize.valueOr(aRect)); } } void Decoder::PostDecodeDone(int32_t aLoopCount /* = 0 */) { MOZ_ASSERT(!IsMetadataDecode(), "Done with decoding in metadata decode"); MOZ_ASSERT(!mInFrame, "Can't be done decoding if we're mid-frame!"); MOZ_ASSERT(!mDecodeDone, "Decode already done!"); mDecodeDone = true; mImageMetadata.SetLoopCount(aLoopCount); mProgress |= FLAG_DECODE_COMPLETE; } void Decoder::PostDataError() { mDataError = true; if (mInFrame && mCurrentFrame) { mCurrentFrame->Abort(); } } void Decoder::PostDecoderError(nsresult aFailureCode) { MOZ_ASSERT(NS_FAILED(aFailureCode), "Not a failure code!"); mFailCode = aFailureCode; // XXXbholley - we should report the image URI here, but imgContainer // needs to know its URI first NS_WARNING("Image decoding error - This is probably a bug!"); if (mInFrame && mCurrentFrame) { mCurrentFrame->Abort(); } } Telemetry::ID Decoder::SpeedHistogram() { // Use HistogramCount as an invalid Histogram ID. return Telemetry::HistogramCount; } } // namespace image } // namespace mozilla