Bug 1294490 - Part 3. Implement WebP decoder. r=tnikkel
authorAndrew Osmond <aosmond@mozilla.com>
Wed, 03 Oct 2018 17:40:35 -0400
changeset 443876 8d76871100bea172d77b2132daec9accc0bdb6a5
parent 443875 ceb0fe9822473904ca40b5772242459b5eba4f8a
child 443877 77efeae42385f9fd3d574813349625cbb6dc2123
push id34973
push userebalazs@mozilla.com
push dateThu, 01 Nov 2018 09:13:27 +0000
treeherdermozilla-central@dcc2391cc0dd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1294490
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1294490 - Part 3. Implement WebP decoder. r=tnikkel Differential Revision: https://phabricator.services.mozilla.com/D8116
gfx/thebes/gfxPrefs.h
image/DecoderFactory.cpp
image/DecoderFactory.h
image/SourceBuffer.h
image/build/nsImageModule.cpp
image/decoders/moz.build
image/decoders/nsWebPDecoder.cpp
image/decoders/nsWebPDecoder.h
modules/libpref/init/all.js
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -565,16 +565,17 @@ private:
   DECL_GFX_PREF(Live, "image.mem.shared",                      ImageMemShared, bool, true);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.discard_factor", ImageMemSurfaceCacheDiscardFactor, uint32_t, 1);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.max_size_kb",    ImageMemSurfaceCacheMaxSizeKB, uint32_t, 100 * 1024);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.min_expiration_ms", ImageMemSurfaceCacheMinExpirationMS, uint32_t, 60*1000);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.size_factor",    ImageMemSurfaceCacheSizeFactor, uint32_t, 64);
   DECL_GFX_PREF(Live, "image.mem.volatile.min_threshold_kb",   ImageMemVolatileMinThresholdKB, int32_t, -1);
   DECL_GFX_PREF(Once, "image.multithreaded_decoding.limit",    ImageMTDecodingLimit, int32_t, -1);
   DECL_GFX_PREF(Once, "image.multithreaded_decoding.idle_timeout", ImageMTDecodingIdleTimeout, int32_t, -1);
+  DECL_GFX_PREF(Live, "image.webp.enabled",                    ImageWebPEnabled, bool, false);
 
   DECL_GFX_PREF(Once, "layers.acceleration.disabled",          LayersAccelerationDisabledDoNotUseDirectly, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps",          LayersDrawFPS, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps.print-histogram",  FPSPrintHistogram, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps.write-to-file", WriteFPSToFile, bool, false);
   DECL_GFX_PREF(Once, "layers.acceleration.force-enabled",     LayersAccelerationForceEnabledDoNotUseDirectly, bool, false);
   DECL_GFX_PREF(Live, "layers.advanced.basic-layer.enabled",          LayersAdvancedBasicLayerEnabled, bool, false);
   DECL_GFX_PREF(Once, "layers.amd-switchable-gfx.enabled",     LayersAMDSwitchableGfxEnabled, bool, false);
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -1,29 +1,31 @@
 /* -*- 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 "DecoderFactory.h"
 
+#include "gfxPrefs.h"
 #include "nsMimeTypes.h"
 #include "mozilla/RefPtr.h"
 
 #include "AnimationSurfaceProvider.h"
 #include "Decoder.h"
 #include "DecodedSurfaceProvider.h"
 #include "IDecodingTask.h"
 #include "ImageOps.h"
 #include "nsPNGDecoder.h"
 #include "nsGIFDecoder2.h"
 #include "nsJPEGDecoder.h"
 #include "nsBMPDecoder.h"
 #include "nsICODecoder.h"
 #include "nsIconDecoder.h"
+#include "nsWebPDecoder.h"
 
 namespace mozilla {
 
 using namespace gfx;
 
 namespace image {
 
 /* static */ DecoderType
@@ -62,16 +64,21 @@ DecoderFactory::GetDecoderType(const cha
   } else if (!strcmp(aMimeType, IMAGE_ICO)) {
     type = DecoderType::ICO;
   } else if (!strcmp(aMimeType, IMAGE_ICO_MS)) {
     type = DecoderType::ICO;
 
   // Icon
   } else if (!strcmp(aMimeType, IMAGE_ICON_MS)) {
     type = DecoderType::ICON;
+
+  // WebP
+  } else if (!strcmp(aMimeType, IMAGE_WEBP) &&
+             gfxPrefs::ImageWebPEnabled()) {
+    type = DecoderType::WEBP;
   }
 
   return type;
 }
 
 /* static */ already_AddRefed<Decoder>
 DecoderFactory::GetDecoder(DecoderType aType,
                            RasterImage* aImage,
@@ -97,16 +104,19 @@ DecoderFactory::GetDecoder(DecoderType a
       decoder = new nsBMPDecoder(aImage);
       break;
     case DecoderType::ICO:
       decoder = new nsICODecoder(aImage);
       break;
     case DecoderType::ICON:
       decoder = new nsIconDecoder(aImage);
       break;
+    case DecoderType::WEBP:
+      decoder = new nsWebPDecoder(aImage);
+      break;
     default:
       MOZ_ASSERT_UNREACHABLE("Unknown decoder type");
   }
 
   return decoder.forget();
 }
 
 /* static */ nsresult
@@ -177,17 +187,18 @@ DecoderFactory::CreateAnimationDecoder(D
                                        SurfaceFlags aSurfaceFlags,
                                        size_t aCurrentFrame,
                                        IDecodingTask** aOutTask)
 {
   if (aType == DecoderType::UNKNOWN) {
     return NS_ERROR_INVALID_ARG;
   }
 
-  MOZ_ASSERT(aType == DecoderType::GIF || aType == DecoderType::PNG,
+  MOZ_ASSERT(aType == DecoderType::GIF || aType == DecoderType::PNG ||
+             aType == DecoderType::WEBP,
              "Calling CreateAnimationDecoder for non-animating DecoderType");
 
   // Create an anonymous decoder. Interaction with the SurfaceCache and the
   // owning RasterImage will be mediated by AnimationSurfaceProvider.
   RefPtr<Decoder> decoder = GetDecoder(aType, nullptr, /* aIsRedecode = */ true);
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
--- a/image/DecoderFactory.h
+++ b/image/DecoderFactory.h
@@ -32,16 +32,17 @@ class SourceBufferIterator;
 enum class DecoderType
 {
   PNG,
   GIF,
   JPEG,
   BMP,
   ICO,
   ICON,
+  WEBP,
   UNKNOWN
 };
 
 class DecoderFactory
 {
 public:
   /// @return the type of decoder which is appropriate for @aMimeType.
   static DecoderType GetDecoderType(const char* aMimeType);
--- a/image/SourceBuffer.h
+++ b/image/SourceBuffer.h
@@ -170,16 +170,24 @@ public:
 
   /// If we're ready to read, returns the length of the new data.
   size_t Length() const
   {
     MOZ_ASSERT(mState == READY, "Calling Length() in the wrong state");
     return mState == READY ? mData.mIterating.mNextReadLength : 0;
   }
 
+  /// If we're ready to read, returns whether or not everything available thus
+  /// far has been in the same contiguous buffer.
+  bool IsContiguous() const
+  {
+    MOZ_ASSERT(mState == READY, "Calling IsContiguous() in the wrong state");
+    return mState == READY ? mData.mIterating.mChunk == 0 : false;
+  }
+
   /// @return a count of the chunks we've advanced through.
   uint32_t ChunkCount() const { return mChunkCount; }
 
   /// @return a count of the bytes in all chunks we've advanced through.
   size_t ByteCount() const { return mByteCount; }
 
   /// @return the source buffer which owns the iterator.
   SourceBuffer* Owner() const
--- a/image/build/nsImageModule.cpp
+++ b/image/build/nsImageModule.cpp
@@ -77,16 +77,17 @@ static const mozilla::Module::CategoryEn
   { "Gecko-Content-Viewers", IMAGE_ICO, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_ICO_MS, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_BMP, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_BMP_MS, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_ICON_MS, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_PNG, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_APNG, "@mozilla.org/content/document-loader-factory;1" },
   { "Gecko-Content-Viewers", IMAGE_X_PNG, "@mozilla.org/content/document-loader-factory;1" },
+  { "Gecko-Content-Viewers", IMAGE_WEBP, "@mozilla.org/content/document-loader-factory;1" },
   { "content-sniffing-services", "@mozilla.org/image/loader;1", "@mozilla.org/image/loader;1" },
   { nullptr }
 };
 
 static bool sInitialized = false;
 nsresult
 mozilla::image::EnsureModuleInitialized()
 {
--- a/image/decoders/moz.build
+++ b/image/decoders/moz.build
@@ -23,16 +23,17 @@ UNIFIED_SOURCES += [
     'EXIF.cpp',
     'iccjpeg.c',
     'nsBMPDecoder.cpp',
     'nsGIFDecoder2.cpp',
     'nsICODecoder.cpp',
     'nsIconDecoder.cpp',
     'nsJPEGDecoder.cpp',
     'nsPNGDecoder.cpp',
+    'nsWebPDecoder.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 LOCAL_INCLUDES += [
     # Access to Skia headers for Downscaler.
     '/gfx/2d',
     # Decoders need ImageLib headers.
new file mode 100644
--- /dev/null
+++ b/image/decoders/nsWebPDecoder.cpp
@@ -0,0 +1,551 @@
+/* -*- 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.Advance(SIZE_MAX);
+
+    // 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();
+    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 x %d\n",
+       this, mCurrentFrame, aFrameRect.width, aFrameRect.height));
+
+  // 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;
+  }
+
+  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 <= 0 || height <= 0 || stride <= 0) {
+    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 ? 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;
+}
+
+} // namespace image
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/image/decoders/nsWebPDecoder.h
@@ -0,0 +1,110 @@
+/* -*- 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_decoders_nsWebPDecoder_h
+#define mozilla_image_decoders_nsWebPDecoder_h
+
+#include "Decoder.h"
+#include "webp/demux.h"
+#include "StreamingLexer.h"
+#include "SurfacePipe.h"
+
+namespace mozilla {
+namespace image {
+class RasterImage;
+
+class nsWebPDecoder final : public Decoder
+{
+public:
+  virtual ~nsWebPDecoder();
+
+protected:
+  LexerResult DoDecode(SourceBufferIterator& aIterator,
+                       IResumable* aOnResume) override;
+
+private:
+  friend class DecoderFactory;
+
+  // Decoders should only be instantiated via DecoderFactory.
+  explicit nsWebPDecoder(RasterImage* aImage);
+
+  enum class State
+  {
+    WEBP_DATA,
+    FINISHED_WEBP_DATA
+  };
+
+  void ApplyColorProfile(const char* aProfile, size_t aLength);
+
+  LexerResult ReadData();
+  LexerResult ReadHeader(WebPDemuxer* aDemuxer, bool aIsComplete);
+  LexerResult ReadPayload(WebPDemuxer* aDemuxer, bool aIsComplete);
+
+  nsresult CreateFrame(const nsIntRect& aFrameRect);
+  void EndFrame();
+
+  LexerResult ReadSingle(const uint8_t* aData, size_t aLength,
+                         const IntRect& aFrameRect);
+
+  LexerResult ReadMultiple(WebPDemuxer* aDemuxer, bool aIsComplete);
+
+  /// The SurfacePipe used to write to the output surface.
+  SurfacePipe mPipe;
+
+  /// The buffer used to accumulate data until the complete WebP header is
+  /// received, if and only if the iterator is discontiguous.
+  Vector<uint8_t> mBufferedData;
+
+  /// The libwebp output buffer descriptor pointing to the decoded data.
+  WebPDecBuffer mBuffer;
+
+  /// The libwebp incremental decoder descriptor, wraps mBuffer.
+  WebPIDecoder* mDecoder;
+
+  /// Blend method for the current frame.
+  BlendMethod mBlend;
+
+  /// Disposal method for the current frame.
+  DisposalMethod mDisposal;
+
+  /// Frame timeout for the current frame;
+  FrameTimeout mTimeout;
+
+  /// Surface format for the current frame.
+  gfx::SurfaceFormat mFormat;
+
+  /// The last row of decoded pixels written to mPipe.
+  int mLastRow;
+
+  /// Number of decoded frames.
+  uint32_t mCurrentFrame;
+
+  /// Pointer to the start of the contiguous encoded image data.
+  const uint8_t* mData;
+
+  /// Length of data pointed to by mData.
+  size_t mLength;
+
+  /// True if the iterator has reached its end.
+  bool mIteratorComplete;
+
+  /// True if this decoding pass requires a WebPDemuxer.
+  bool mNeedDemuxer;
+
+  /// True if we have setup the color profile for the image.
+  bool mGotColorProfile;
+
+  /// Color management profile from the ICCP chunk in the image.
+  qcms_profile* mInProfile;
+
+  /// Color management transform to apply to image data.
+  qcms_transform* mTransform;
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_decoders_nsWebPDecoder_h
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4766,16 +4766,19 @@ pref("image.mem.volatile.min_threshold_k
 
 // How many threads we'll use for multithreaded decoding. If < 0, will be
 // automatically determined based on the system's number of cores.
 pref("image.multithreaded_decoding.limit", -1);
 
 // How long in ms before we should start shutting down idle decoder threads.
 pref("image.multithreaded_decoding.idle_timeout", 600000);
 
+// Whether we attempt to decode WebP images or not.
+pref("image.webp.enabled", false);
+
 // Limit for the canvas image cache. 0 means we don't limit the size of the
 // cache.
 pref("canvas.image.cache.limit", 0);
 
 // WebGL prefs
 #ifdef ANDROID
 // Disable MSAA on mobile.
 pref("gl.msaa-level", 0);