image/decoders/nsWebPDecoder.cpp
author Andrew Osmond <aosmond@mozilla.com>
Fri, 02 Nov 2018 15:28:17 -0400
changeset 500768 5fb87d2312ea4881026ecaac6488fbdc2e4a7725
parent 500481 5223058ac4fffd00aad34eeb29998c3264b250e8
child 501284 1262664295ff6c471a0515b64a57b56d3c57b6ee
permissions -rw-r--r--
Bug 1504237 - Ensure partial animated frames always use BGRA instead of BGRX. r=tnikkel For decoders which produce unpaletted partial frames (APNG, WebP), the surface format should always be BGRA. These frames while partial, are the same size as the output size of the animated image. When FrameAnimator performs the blend with the compositing frame, it expects all pixels we don't care about to be set to fully transparent. If it is BGRX, they will be set to solid white instead. Differential Revision: https://phabricator.services.mozilla.com/D10753

/* -*- 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 "ImageLogging.h" // Must appear first
#include "nsWebPDecoder.h"

#include "RasterImage.h"
#include "SurfacePipeFactory.h"

using namespace mozilla::gfx;

namespace mozilla {
namespace image {

static LazyLogModule sWebPLog("WebPDecoder");

nsWebPDecoder::nsWebPDecoder(RasterImage* aImage)
  : Decoder(aImage)
  , mDecoder(nullptr)
  , mBlend(BlendMethod::OVER)
  , mDisposal(DisposalMethod::KEEP)
  , mTimeout(FrameTimeout::Forever())
  , mFormat(SurfaceFormat::B8G8R8X8)
  , mLastRow(0)
  , mCurrentFrame(0)
  , mData(nullptr)
  , mLength(0)
  , mIteratorComplete(false)
  , mNeedDemuxer(true)
  , mGotColorProfile(false)
  , mInProfile(nullptr)
  , mTransform(nullptr)
{
  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::nsWebPDecoder", this));
}

nsWebPDecoder::~nsWebPDecoder()
{
  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::~nsWebPDecoder", this));
  if (mDecoder) {
    WebPIDelete(mDecoder);
    WebPFreeDecBuffer(&mBuffer);
  }
  if (mInProfile) {
    // mTransform belongs to us only if mInProfile is non-null
    if (mTransform) {
      qcms_transform_release(mTransform);
    }
    qcms_profile_release(mInProfile);
  }
}

LexerResult
nsWebPDecoder::ReadData()
{
  MOZ_ASSERT(mData);
  MOZ_ASSERT(mLength > 0);

  WebPDemuxer* demuxer = nullptr;
  bool complete = mIteratorComplete;

  if (mNeedDemuxer) {
    WebPDemuxState state;
    WebPData fragment;
    fragment.bytes = mData;
    fragment.size = mLength;

    demuxer = WebPDemuxPartial(&fragment, &state);
    if (state == WEBP_DEMUX_PARSE_ERROR) {
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::ReadData -- demux parse error\n", this));
      WebPDemuxDelete(demuxer);
      return LexerResult(TerminalState::FAILURE);
    }

    if (state == WEBP_DEMUX_PARSING_HEADER) {
      WebPDemuxDelete(demuxer);
      return LexerResult(Yield::NEED_MORE_DATA);
    }

    if (!demuxer) {
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::ReadData -- no demuxer\n", this));
      return LexerResult(TerminalState::FAILURE);
    }

    complete = complete || state == WEBP_DEMUX_DONE;
  }

  LexerResult rv(TerminalState::FAILURE);
  if (!HasSize()) {
    rv = ReadHeader(demuxer, complete);
  } else {
    rv = ReadPayload(demuxer, complete);
  }

  WebPDemuxDelete(demuxer);
  return rv;
}

LexerResult
nsWebPDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
{
  MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");

  SourceBufferIterator::State state = SourceBufferIterator::COMPLETE;
  if (!mIteratorComplete) {
    state = aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume);

    // We need to remember since we can't advance a complete iterator.
    mIteratorComplete = state == SourceBufferIterator::COMPLETE;
  }

  switch (state) {
    case SourceBufferIterator::READY:
      if (!aIterator.IsContiguous()) {
        // We need to buffer. This should be rare, but expensive.
        break;
      }
      if (!mData) {
        // For as long as we hold onto an iterator, we know the data pointers
        // to the chunks cannot change underneath us, so save the pointer to
        // the first block.
        MOZ_ASSERT(mLength == 0);
        mData = reinterpret_cast<const uint8_t*>(aIterator.Data());
      }
      mLength += aIterator.Length();
      return ReadData();
    case SourceBufferIterator::COMPLETE:
      return ReadData();
    case SourceBufferIterator::WAITING:
      return LexerResult(Yield::NEED_MORE_DATA);
    default:
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::DoDecode -- bad state\n", this));
      return LexerResult(TerminalState::FAILURE);
  }

  // We need to buffer. If we have no data buffered, we need to get everything
  // from the first chunk of the source buffer before appending the new data.
  if (mBufferedData.empty()) {
    MOZ_ASSERT(mData);
    MOZ_ASSERT(mLength > 0);

    if (!mBufferedData.append(mData, mLength)) {
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::DoDecode -- oom, initialize %zu\n",
           this, mLength));
      return LexerResult(TerminalState::FAILURE);
    }

    MOZ_LOG(sWebPLog, LogLevel::Debug,
        ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu bytes\n",
         this, mLength));
  }

  // Append the incremental data from the iterator.
  if (!mBufferedData.append(aIterator.Data(), aIterator.Length())) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
        ("[this=%p] nsWebPDecoder::DoDecode -- oom, append %zu on %zu\n",
         this, aIterator.Length(), mBufferedData.length()));
    return LexerResult(TerminalState::FAILURE);
  }

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu -> %zu bytes\n",
       this, aIterator.Length(), mBufferedData.length()));
  mData = mBufferedData.begin();
  mLength = mBufferedData.length();
  return ReadData();
}

nsresult
nsWebPDecoder::CreateFrame(const nsIntRect& aFrameRect)
{
  MOZ_ASSERT(HasSize());
  MOZ_ASSERT(!mDecoder);

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::CreateFrame -- frame %u, (%d, %d) %d x %d\n",
       this, mCurrentFrame, aFrameRect.x, aFrameRect.y,
       aFrameRect.width, aFrameRect.height));

  if (aFrameRect.width <= 0 || aFrameRect.height <= 0) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
        ("[this=%p] nsWebPDecoder::CreateFrame -- bad frame rect\n",
         this));
    return NS_ERROR_FAILURE;
  }

  // If this is our first frame in an animation and it doesn't cover the
  // full frame, then we are transparent even if there is no alpha
  if (mCurrentFrame == 0 && !aFrameRect.IsEqualEdges(FullFrame())) {
    MOZ_ASSERT(HasAnimation());
    mFormat = SurfaceFormat::B8G8R8A8;
    PostHasTransparency();
  }

  WebPInitDecBuffer(&mBuffer);
  mBuffer.colorspace = MODE_RGBA;

  mDecoder = WebPINewDecoder(&mBuffer);
  if (!mDecoder) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
        ("[this=%p] nsWebPDecoder::CreateFrame -- create decoder error\n",
         this));
    return NS_ERROR_FAILURE;
  }

  SurfacePipeFlags pipeFlags = SurfacePipeFlags();

  if (ShouldBlendAnimation()) {
    pipeFlags |= SurfacePipeFlags::BLEND_ANIMATION;
  }

  AnimationParams animParams {
    aFrameRect, mTimeout, mCurrentFrame, mBlend, mDisposal
  };

  Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(this,
      Size(), OutputSize(), aFrameRect, mFormat, Some(animParams), pipeFlags);
  if (!pipe) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
        ("[this=%p] nsWebPDecoder::CreateFrame -- no pipe\n", this));
    return NS_ERROR_FAILURE;
  }

  mFrameRect = aFrameRect;
  mPipe = std::move(*pipe);
  return NS_OK;
}

void
nsWebPDecoder::EndFrame()
{
  MOZ_ASSERT(HasSize());
  MOZ_ASSERT(mDecoder);

  auto opacity = mFormat == SurfaceFormat::B8G8R8A8
                 ? Opacity::SOME_TRANSPARENCY : Opacity::FULLY_OPAQUE;

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::EndFrame -- frame %u, opacity %d, "
       "disposal %d, timeout %d, blend %d\n",
       this, mCurrentFrame, (int)opacity, (int)mDisposal,
       mTimeout.AsEncodedValueDeprecated(), (int)mBlend));

  PostFrameStop(opacity);
  WebPIDelete(mDecoder);
  WebPFreeDecBuffer(&mBuffer);
  mDecoder = nullptr;
  mLastRow = 0;
  ++mCurrentFrame;
}

void
nsWebPDecoder::ApplyColorProfile(const char* aProfile, size_t aLength)
{
  MOZ_ASSERT(!mGotColorProfile);
  mGotColorProfile = true;

  if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
    return;
  }

  auto mode = gfxPlatform::GetCMSMode();
  if (mode == eCMSMode_Off || (mode == eCMSMode_TaggedOnly && !aProfile)) {
    return;
  }

  if (!aProfile || !gfxPlatform::GetCMSOutputProfile()) {
    MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::ApplyColorProfile -- not tagged or no output "
       "profile , use sRGB transform\n", this));
    mTransform = gfxPlatform::GetCMSRGBATransform();
    return;
  }

  mInProfile = qcms_profile_from_memory(aProfile, aLength);
  if (!mInProfile) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
      ("[this=%p] nsWebPDecoder::ApplyColorProfile -- bad color profile\n",
       this));
    return;
  }

  // Calculate rendering intent.
  int intent = gfxPlatform::GetRenderingIntent();
  if (intent == -1) {
    intent = qcms_profile_get_rendering_intent(mInProfile);
  }

  // Create the color management transform.
  mTransform = qcms_transform_create(mInProfile,
                                     QCMS_DATA_RGBA_8,
                                     gfxPlatform::GetCMSOutputProfile(),
                                     QCMS_DATA_RGBA_8,
                                     (qcms_intent)intent);
  MOZ_LOG(sWebPLog, LogLevel::Debug,
    ("[this=%p] nsWebPDecoder::ApplyColorProfile -- use tagged "
     "transform\n", this));
}

LexerResult
nsWebPDecoder::ReadHeader(WebPDemuxer* aDemuxer,
                          bool aIsComplete)
{
  MOZ_ASSERT(aDemuxer);

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::ReadHeader -- %zu bytes\n", this, mLength));

  uint32_t flags = WebPDemuxGetI(aDemuxer, WEBP_FF_FORMAT_FLAGS);

  if (!IsMetadataDecode() && !mGotColorProfile) {
    if (flags & WebPFeatureFlags::ICCP_FLAG) {
      WebPChunkIterator iter;
      if (!WebPDemuxGetChunk(aDemuxer, "ICCP", 1, &iter)) {
        return aIsComplete ? LexerResult(TerminalState::FAILURE)
                           : LexerResult(Yield::NEED_MORE_DATA);
      }

      ApplyColorProfile(reinterpret_cast<const char*>(iter.chunk.bytes),
                        iter.chunk.size);
      WebPDemuxReleaseChunkIterator(&iter);
    } else {
      ApplyColorProfile(nullptr, 0);
    }
  }

  if (flags & WebPFeatureFlags::ANIMATION_FLAG) {
    // A metadata decode expects to get the correct first frame timeout which
    // sadly is not provided by the normal WebP header parsing.
    WebPIterator iter;
    if (!WebPDemuxGetFrame(aDemuxer, 1, &iter)) {
      return aIsComplete ? LexerResult(TerminalState::FAILURE)
                         : LexerResult(Yield::NEED_MORE_DATA);
    }

    PostIsAnimated(FrameTimeout::FromRawMilliseconds(iter.duration));
    WebPDemuxReleaseIterator(&iter);
  } else {
    // Single frames don't need a demuxer to be created.
    mNeedDemuxer = false;
  }

  uint32_t width = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_WIDTH);
  uint32_t height = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_HEIGHT);
  if (width > INT32_MAX || height > INT32_MAX) {
    return LexerResult(TerminalState::FAILURE);
  }

  PostSize(width, height);

  bool alpha = flags & WebPFeatureFlags::ALPHA_FLAG;
  if (alpha) {
    mFormat = SurfaceFormat::B8G8R8A8;
    PostHasTransparency();
  }

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::ReadHeader -- %u x %u, alpha %d, "
       "animation %d, metadata decode %d, first frame decode %d\n",
       this, width, height, alpha, HasAnimation(),
       IsMetadataDecode(), IsFirstFrameDecode()));

  if (IsMetadataDecode()) {
    return LexerResult(TerminalState::SUCCESS);
  }

  return ReadPayload(aDemuxer, aIsComplete);
}

LexerResult
nsWebPDecoder::ReadPayload(WebPDemuxer* aDemuxer,
                           bool aIsComplete)
{
  if (!HasAnimation()) {
    auto rv = ReadSingle(mData, mLength, FullFrame());
    if (rv.is<TerminalState>() &&
        rv.as<TerminalState>() == TerminalState::SUCCESS) {
      PostDecodeDone();
    }
    return rv;
  }
  return ReadMultiple(aDemuxer, aIsComplete);
}

LexerResult
nsWebPDecoder::ReadSingle(const uint8_t* aData, size_t aLength, const IntRect& aFrameRect)
{
  MOZ_ASSERT(!IsMetadataDecode());
  MOZ_ASSERT(aData);
  MOZ_ASSERT(aLength > 0);

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::ReadSingle -- %zu bytes\n", this, aLength));

  if (!mDecoder && NS_FAILED(CreateFrame(aFrameRect))) {
    return LexerResult(TerminalState::FAILURE);
  }

  bool complete;
  VP8StatusCode status = WebPIUpdate(mDecoder, aData, aLength);
  switch (status) {
    case VP8_STATUS_OK:
      complete = true;
      break;
    case VP8_STATUS_SUSPENDED:
      complete = false;
      break;
    default:
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::ReadSingle -- append error %d\n",
           this, status));
      return LexerResult(TerminalState::FAILURE);
  }

  int lastRow = -1;
  int width = 0;
  int height = 0;
  int stride = 0;
  uint8_t* rowStart = WebPIDecGetRGB(mDecoder, &lastRow, &width, &height, &stride);
  if (!rowStart || lastRow == -1) {
    return LexerResult(Yield::NEED_MORE_DATA);
  }

  if (width != mFrameRect.width || height != mFrameRect.height ||
      stride < mFrameRect.width * 4 ||
      lastRow > mFrameRect.height) {
    MOZ_LOG(sWebPLog, LogLevel::Error,
        ("[this=%p] nsWebPDecoder::ReadSingle -- bad (w,h,s) = (%d, %d, %d)\n",
         this, width, height, stride));
    return LexerResult(TerminalState::FAILURE);
  }

  const bool noPremultiply =
    bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA);

  for (int row = mLastRow; row < lastRow; row++) {
    uint8_t* src = rowStart + row * stride;
    if (mTransform) {
      qcms_transform_data(mTransform, src, src, width);
    }

    WriteState result;
    if (noPremultiply) {
      result = mPipe.WritePixelsToRow<uint32_t>([&]() -> NextPixel<uint32_t> {
        MOZ_ASSERT(mFormat == SurfaceFormat::B8G8R8A8 || src[3] == 0xFF);
        const uint32_t pixel =
          gfxPackedPixelNoPreMultiply(src[3], src[0], src[1], src[2]);
        src += 4;
        return AsVariant(pixel);
      });
    } else {
      result = mPipe.WritePixelsToRow<uint32_t>([&]() -> NextPixel<uint32_t> {
        MOZ_ASSERT(mFormat == SurfaceFormat::B8G8R8A8 || src[3] == 0xFF);
        const uint32_t pixel = gfxPackedPixel(src[3], src[0], src[1], src[2]);
        src += 4;
        return AsVariant(pixel);
      });
    }

    MOZ_ASSERT(result != WriteState::FAILURE);
    MOZ_ASSERT_IF(result == WriteState::FINISHED, complete && row == lastRow - 1);

    if (result == WriteState::FAILURE) {
      MOZ_LOG(sWebPLog, LogLevel::Error,
          ("[this=%p] nsWebPDecoder::ReadSingle -- write pixels error\n",
           this));
      return LexerResult(TerminalState::FAILURE);
    }
  }

  if (mLastRow != lastRow) {
    mLastRow = lastRow;

    Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect();
    if (invalidRect) {
      PostInvalidation(invalidRect->mInputSpaceRect,
          Some(invalidRect->mOutputSpaceRect));
    }
  }

  if (!complete) {
    return LexerResult(Yield::NEED_MORE_DATA);
  }

  EndFrame();
  return LexerResult(TerminalState::SUCCESS);
}

LexerResult
nsWebPDecoder::ReadMultiple(WebPDemuxer* aDemuxer, bool aIsComplete)
{
  MOZ_ASSERT(!IsMetadataDecode());
  MOZ_ASSERT(aDemuxer);

  MOZ_LOG(sWebPLog, LogLevel::Debug,
      ("[this=%p] nsWebPDecoder::ReadMultiple\n", this));

  bool complete = aIsComplete;
  WebPIterator iter;
  auto rv = LexerResult(Yield::NEED_MORE_DATA);
  if (WebPDemuxGetFrame(aDemuxer, mCurrentFrame + 1, &iter)) {
    switch (iter.blend_method) {
      case WEBP_MUX_BLEND:
        mBlend = BlendMethod::OVER;
        break;
      case WEBP_MUX_NO_BLEND:
        mBlend = BlendMethod::SOURCE;
        break;
      default:
        MOZ_ASSERT_UNREACHABLE("Unhandled blend method");
        break;
    }

    switch (iter.dispose_method) {
      case WEBP_MUX_DISPOSE_NONE:
        mDisposal = DisposalMethod::KEEP;
        break;
      case WEBP_MUX_DISPOSE_BACKGROUND:
        mDisposal = DisposalMethod::CLEAR;
        break;
      default:
        MOZ_ASSERT_UNREACHABLE("Unhandled dispose method");
        break;
    }

    mFormat = iter.has_alpha || mCurrentFrame > 0 ? SurfaceFormat::B8G8R8A8
                                                  : SurfaceFormat::B8G8R8X8;
    mTimeout = FrameTimeout::FromRawMilliseconds(iter.duration);
    nsIntRect frameRect(iter.x_offset, iter.y_offset, iter.width, iter.height);

    rv = ReadSingle(iter.fragment.bytes, iter.fragment.size, frameRect);
    complete = complete && !WebPDemuxNextFrame(&iter);
    WebPDemuxReleaseIterator(&iter);
  }

  if (rv.is<TerminalState>() &&
      rv.as<TerminalState>() == TerminalState::SUCCESS) {
    // If we extracted one frame, and it is not the last, we need to yield to
    // the lexer to allow the upper layers to acknowledge the frame.
    if (!complete && !IsFirstFrameDecode()) {
      rv = LexerResult(Yield::OUTPUT_AVAILABLE);
    } else {
      uint32_t loopCount = WebPDemuxGetI(aDemuxer, WEBP_FF_LOOP_COUNT);

      MOZ_LOG(sWebPLog, LogLevel::Debug,
        ("[this=%p] nsWebPDecoder::ReadMultiple -- loop count %u\n",
         this, loopCount));
      PostDecodeDone(loopCount - 1);
    }
  }

  return rv;
}

Maybe<Telemetry::HistogramID>
nsWebPDecoder::SpeedHistogram() const
{
  return Some(Telemetry::IMAGE_DECODE_SPEED_WEBP);
}

} // namespace image
} // namespace mozilla