Bug 1501794 - Implement img decode API. r=bzbarsky,tnikkel
authorAndrew Osmond <aosmond@mozilla.com>
Thu, 08 Nov 2018 12:45:50 -0500
changeset 526477 5a7f8303d682cc3ce7880c42dd0c74d75c0b5cd3
parent 526476 d363668ebdaa772feed5c3392fed60432789458f
child 526478 176c43e7780a93c0265165679808764f2107588d
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbzbarsky, tnikkel
bugs1501794
milestone68.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 1501794 - Implement img decode API. r=bzbarsky,tnikkel The img decode API allows a web author to request that an image be decoded at its intrinsic size and be notified when it has been completed. This is useful to ensure an image is ready to display before adding it to the DOM tree -- this will help reduce flickering. Differential Revision: https://phabricator.services.mozilla.com/D11362
dom/base/Document.cpp
dom/base/domerr.msg
dom/base/nsImageLoadingContent.cpp
dom/base/nsImageLoadingContent.h
dom/base/nsNodeUtils.cpp
dom/base/nsObjectLoadingContent.cpp
dom/html/HTMLImageElement.cpp
dom/html/HTMLImageElement.h
dom/svg/SVGImageElement.cpp
dom/svg/SVGImageElement.h
dom/webidl/HTMLImageElement.webidl
dom/webidl/SVGImageElement.webidl
image/DynamicImage.cpp
image/ImageWrapper.cpp
image/RasterImage.cpp
image/VectorImage.cpp
image/imgIContainer.idl
image/imgIRequest.idl
image/imgRequestProxy.cpp
testing/web-platform/meta/html/dom/interfaces.https.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-iframe.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes-svg.tentative.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-picture.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-svg.tentative.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach-svg.tentative.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode.html.ini
xpcom/base/ErrorList.py
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -4296,16 +4296,23 @@ static void NotifyActivityChanged(nsISup
     nsObjectLoadingContent* olc =
         static_cast<nsObjectLoadingContent*>(objectLoadingContent.get());
     olc->NotifyOwnerDocumentActivityChanged();
   }
   nsCOMPtr<nsIDocumentActivity> objectDocumentActivity(
       do_QueryInterface(aSupports));
   if (objectDocumentActivity) {
     objectDocumentActivity->NotifyOwnerDocumentActivityChanged();
+  } else {
+    nsCOMPtr<nsIImageLoadingContent> imageLoadingContent(
+        do_QueryInterface(aSupports));
+    if (imageLoadingContent) {
+      auto ilc = static_cast<nsImageLoadingContent*>(imageLoadingContent.get());
+      ilc->NotifyOwnerDocumentActivityChanged();
+    }
   }
 }
 
 bool Document::IsTopLevelWindowInactive() const {
   nsCOMPtr<nsIDocShellTreeItem> treeItem = GetDocShell();
   if (!treeItem) {
     return false;
   }
--- a/dom/base/domerr.msg
+++ b/dom/base/domerr.msg
@@ -159,8 +159,13 @@ DOM4_MSG_DEF(SyntaxError, "Invalid heade
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest has an invalid context.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest state must be OPENED.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_BE_OPENED)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest must not be sending.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest state must not be LOADING or DONE.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_LOADING_OR_DONE)
 DOM4_MSG_DEF(InvalidStateError, "responseXML is only available if responseType is '' or 'document'.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSEXML)
 DOM4_MSG_DEF(InvalidStateError, "responseText is only available if responseType is '' or 'text'.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSETEXT)
 DOM4_MSG_DEF(InvalidStateError, "synchronous XMLHttpRequests do not support 'moz-chunked-arraybuffer' responseType.", NS_ERROR_DOM_INVALID_STATE_XHR_CHUNKED_RESPONSETYPES_UNSUPPORTED_FOR_SYNC)
 DOM4_MSG_DEF(InvalidAccessError, "synchronous XMLHttpRequests do not support timeout and responseType.", NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC)
+
+/* Image decode errors. */
+DOM4_MSG_DEF(EncodingError, "Node bound to inactive document.", NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT)
+DOM4_MSG_DEF(EncodingError, "Invalid image request.", NS_ERROR_DOM_IMAGE_INVALID_REQUEST)
+DOM4_MSG_DEF(EncodingError, "Invalid encoded image data.", NS_ERROR_DOM_IMAGE_BROKEN)
--- a/dom/base/nsImageLoadingContent.cpp
+++ b/dom/base/nsImageLoadingContent.cpp
@@ -90,16 +90,18 @@ const nsAttrValue::EnumTable nsImageLoad
 
 const nsAttrValue::EnumTable* nsImageLoadingContent::kDecodingTableDefault =
     &nsImageLoadingContent::kDecodingTable[0];
 
 nsImageLoadingContent::nsImageLoadingContent()
     : mCurrentRequestFlags(0),
       mPendingRequestFlags(0),
       mObserverList(nullptr),
+      mOutstandingDecodePromises(0),
+      mRequestGeneration(0),
       mImageBlockingStatus(nsIContentPolicy::ACCEPT),
       mLoadingEnabled(true),
       mIsImageStateForced(false),
       mLoading(false),
       // mBroken starts out true, since an image without a URI is broken....
       mBroken(true),
       mUserDisabled(false),
       mSuppressed(false),
@@ -115,27 +117,31 @@ nsImageLoadingContent::nsImageLoadingCon
   }
 
   mMostRecentRequestChange = TimeStamp::ProcessCreation();
 }
 
 void nsImageLoadingContent::DestroyImageLoadingContent() {
   // Cancel our requests so they won't hold stale refs to us
   // NB: Don't ask to discard the images here.
+  RejectDecodePromises(NS_ERROR_DOM_IMAGE_INVALID_REQUEST);
   ClearCurrentRequest(NS_BINDING_ABORTED);
   ClearPendingRequest(NS_BINDING_ABORTED);
 }
 
 nsImageLoadingContent::~nsImageLoadingContent() {
-  NS_ASSERTION(!mCurrentRequest && !mPendingRequest,
-               "DestroyImageLoadingContent not called");
-  NS_ASSERTION(!mObserverList.mObserver && !mObserverList.mNext,
-               "Observers still registered?");
-  NS_ASSERTION(mScriptedObservers.IsEmpty(),
-               "Scripted observers still registered?");
+  MOZ_ASSERT(!mCurrentRequest && !mPendingRequest,
+             "DestroyImageLoadingContent not called");
+  MOZ_ASSERT(!mObserverList.mObserver && !mObserverList.mNext,
+             "Observers still registered?");
+  MOZ_ASSERT(mScriptedObservers.IsEmpty(),
+             "Scripted observers still registered?");
+  MOZ_ASSERT(mOutstandingDecodePromises == 0,
+             "Decode promises still unfulfilled?");
+  MOZ_ASSERT(mDecodePromises.IsEmpty(), "Decode promises still unfulfilled?");
 }
 
 /*
  * imgINotificationObserver impl
  */
 NS_IMETHODIMP
 nsImageLoadingContent::Notify(imgIRequest* aRequest, int32_t aType,
                               const nsIntRect* aData) {
@@ -199,16 +205,21 @@ nsImageLoadingContent::Notify(imgIReques
         doc->AddBlockedNodeByClassifier(thisNode);
       }
     }
     nsresult status =
         reqStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK;
     return OnLoadComplete(aRequest, status);
   }
 
+  if (aType == imgINotificationObserver::FRAME_COMPLETE &&
+      mCurrentRequest == aRequest) {
+    MaybeResolveDecodePromises();
+  }
+
   if (aType == imgINotificationObserver::DECODE_COMPLETE) {
     nsCOMPtr<imgIContainer> container;
     aRequest->GetImage(getter_AddRefs(container));
     if (container) {
       container->PropagateUseCounters(GetOurOwnerDoc());
     }
 
     UpdateImageState(true);
@@ -255,16 +266,17 @@ nsresult nsImageLoadingContent::OnLoadCo
   } else {
     FireEvent(NS_LITERAL_STRING("error"));
     FireEvent(NS_LITERAL_STRING("loadend"));
   }
 
   nsCOMPtr<nsINode> thisNode =
       do_QueryInterface(static_cast<nsIImageLoadingContent*>(this));
   SVGObserverUtils::InvalidateDirectRenderingObservers(thisNode->AsElement());
+  MaybeResolveDecodePromises();
 
   return NS_OK;
 }
 
 static bool ImageIsAnimated(imgIRequest* aRequest) {
   if (!aRequest) {
     return false;
   }
@@ -329,16 +341,176 @@ nsresult nsImageLoadingContent::OnImageI
  */
 
 void nsImageLoadingContent::SetLoadingEnabled(bool aLoadingEnabled) {
   if (nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) {
     mLoadingEnabled = aLoadingEnabled;
   }
 }
 
+already_AddRefed<Promise> nsImageLoadingContent::QueueDecodeAsync(
+    ErrorResult& aRv) {
+  Document* doc = GetOurOwnerDoc();
+  RefPtr<Promise> promise = Promise::Create(doc->GetScopeObject(), aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  class QueueDecodeTask final : public Runnable {
+   public:
+    QueueDecodeTask(nsImageLoadingContent* aOwner, Promise* aPromise,
+                    uint32_t aRequestGeneration)
+        : Runnable("nsImageLoadingContent::QueueDecodeTask"),
+          mOwner(aOwner),
+          mPromise(aPromise),
+          mRequestGeneration(aRequestGeneration) {}
+
+    NS_IMETHOD Run() override {
+      mOwner->DecodeAsync(std::move(mPromise), mRequestGeneration);
+      return NS_OK;
+    }
+
+   private:
+    RefPtr<nsImageLoadingContent> mOwner;
+    RefPtr<Promise> mPromise;
+    uint32_t mRequestGeneration;
+  };
+
+  if (++mOutstandingDecodePromises == 1) {
+    MOZ_ASSERT(mDecodePromises.IsEmpty());
+    doc->RegisterActivityObserver(this);
+  }
+
+  auto task = MakeRefPtr<QueueDecodeTask>(this, promise, mRequestGeneration);
+  nsContentUtils::RunInStableState(task.forget());
+  return promise.forget();
+}
+
+void nsImageLoadingContent::DecodeAsync(RefPtr<Promise>&& aPromise,
+                                        uint32_t aRequestGeneration) {
+  MOZ_ASSERT(nsContentUtils::IsInStableOrMetaStableState());
+  MOZ_ASSERT(aPromise);
+  MOZ_ASSERT(mOutstandingDecodePromises > mDecodePromises.Length());
+
+  // The request may have gotten updated since the decode call was issued.
+  if (aRequestGeneration != mRequestGeneration) {
+    aPromise->MaybeReject(NS_ERROR_DOM_IMAGE_INVALID_REQUEST);
+    // We never got placed in mDecodePromises, so we must ensure we decrement
+    // the counter explicitly.
+    --mOutstandingDecodePromises;
+    MaybeDeregisterActivityObserver();
+    return;
+  }
+
+  bool wasEmpty = mDecodePromises.IsEmpty();
+  mDecodePromises.AppendElement(std::move(aPromise));
+  if (wasEmpty) {
+    MaybeResolveDecodePromises();
+  }
+}
+
+void nsImageLoadingContent::MaybeResolveDecodePromises() {
+  if (mDecodePromises.IsEmpty()) {
+    return;
+  }
+
+  if (!mCurrentRequest) {
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_INVALID_REQUEST);
+    return;
+  }
+
+  // Only can resolve if our document is the active document. If not we are
+  // supposed to reject the promise, even if it was fulfilled successfully.
+  if (!GetOurOwnerDoc()->IsCurrentActiveDocument()) {
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT);
+    return;
+  }
+
+  // If any error occurred while decoding, we need to reject first.
+  uint32_t status = imgIRequest::STATUS_NONE;
+  mCurrentRequest->GetImageStatus(&status);
+  if (status & imgIRequest::STATUS_ERROR) {
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_BROKEN);
+    return;
+  }
+
+  // We need the size to bother with requesting a decode, as we are either
+  // blocked on validation or metadata decoding.
+  if (!(status & imgIRequest::STATUS_SIZE_AVAILABLE)) {
+    return;
+  }
+
+  // Check the surface cache status and/or request decoding begin. We do this
+  // before LOAD_COMPLETE because we want to start as soon as possible.
+  uint32_t flags = imgIContainer::FLAG_HIGH_QUALITY_SCALING |
+                   imgIContainer::FLAG_AVOID_REDECODE_FOR_SIZE;
+  if (!mCurrentRequest->RequestDecodeWithResult(flags)) {
+    return;
+  }
+
+  // We can only fulfill the promises once we have all the data.
+  if (!(status & imgIRequest::STATUS_LOAD_COMPLETE)) {
+    return;
+  }
+
+  for (auto& promise : mDecodePromises) {
+    promise->MaybeResolveWithUndefined();
+  }
+
+  MOZ_ASSERT(mOutstandingDecodePromises >= mDecodePromises.Length());
+  mOutstandingDecodePromises -= mDecodePromises.Length();
+  mDecodePromises.Clear();
+  MaybeDeregisterActivityObserver();
+}
+
+void nsImageLoadingContent::RejectDecodePromises(nsresult aStatus) {
+  if (mDecodePromises.IsEmpty()) {
+    return;
+  }
+
+  for (auto& promise : mDecodePromises) {
+    promise->MaybeReject(aStatus);
+  }
+
+  MOZ_ASSERT(mOutstandingDecodePromises >= mDecodePromises.Length());
+  mOutstandingDecodePromises -= mDecodePromises.Length();
+  mDecodePromises.Clear();
+  MaybeDeregisterActivityObserver();
+}
+
+void nsImageLoadingContent::MaybeAgeRequestGeneration(nsIURI* aNewURI) {
+  MOZ_ASSERT(mCurrentRequest);
+
+  // If the current request is about to change, we need to verify if the new
+  // URI matches the existing current request's URI. If it doesn't, we need to
+  // reject any outstanding promises due to the current request mutating as per
+  // step 2.2 of the decode API requirements.
+  //
+  // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decode
+  if (aNewURI) {
+    nsCOMPtr<nsIURI> currentURI;
+    mCurrentRequest->GetURI(getter_AddRefs(currentURI));
+
+    bool equal = false;
+    if (NS_SUCCEEDED(aNewURI->Equals(currentURI, &equal)) && equal) {
+      return;
+    }
+  }
+
+  ++mRequestGeneration;
+  RejectDecodePromises(NS_ERROR_DOM_IMAGE_INVALID_REQUEST);
+}
+
+void nsImageLoadingContent::MaybeDeregisterActivityObserver() {
+  if (mOutstandingDecodePromises == 0) {
+    MOZ_ASSERT(mDecodePromises.IsEmpty());
+    GetOurOwnerDoc()->UnregisterActivityObserver(this);
+  }
+}
+
 void nsImageLoadingContent::SetSyncDecodingHint(bool aHint) {
   if (mSyncDecodingHint == aHint) {
     return;
   }
 
   mSyncDecodingHint = aHint;
   MaybeForceSyncDecoding(/* aPrepareNextRequest */ false);
 }
@@ -781,16 +953,25 @@ nsImageLoadingContent::LoadImageWithChan
     *aListener = nullptr;
     return NS_OK;
   }
 
   // XXX what should we do with content policies here, if anything?
   // Shouldn't that be done before the start of the load?
   // XXX what about shouldProcess?
 
+  // If we have a current request without a size, we know we will replace it
+  // with the PrepareNextRequest below. If the new current request is for a
+  // different URI, then we need to reject any outstanding promises.
+  if (mCurrentRequest && !HaveSize(mCurrentRequest)) {
+    nsCOMPtr<nsIURI> uri;
+    aChannel->GetOriginalURI(getter_AddRefs(uri));
+    MaybeAgeRequestGeneration(uri);
+  }
+
   // Our state might change. Watch it.
   AutoStateChanger changer(this, true);
 
   // Do the load.
   RefPtr<imgRequestProxy>& req = PrepareNextRequest(eImageLoadType_Normal);
   nsresult rv = loader->LoadImageWithChannel(aChannel, this, ToSupports(doc),
                                              aListener, getter_AddRefs(req));
   if (NS_SUCCEEDED(rv)) {
@@ -939,16 +1120,23 @@ nsresult nsImageLoadingContent::LoadImag
     bool equal;
     if (currentURI && NS_SUCCEEDED(currentURI->Equals(aNewURI, &equal)) &&
         equal) {
       // Nothing to do here.
       return NS_OK;
     }
   }
 
+  // If we have a current request without a size, we know we will replace it
+  // with the PrepareNextRequest below. If the new current request is for a
+  // different URI, then we need to reject any outstanding promises.
+  if (mCurrentRequest && !HaveSize(mCurrentRequest)) {
+    MaybeAgeRequestGeneration(aNewURI);
+  }
+
   // From this point on, our image state could change. Watch it.
   AutoStateChanger changer(this, aNotify);
 
   // Sanity check.
   //
   // We use the principal of aDocument to avoid having to QI |this| an extra
   // time. It should always be the same as the principal of this node.
 #ifdef DEBUG
@@ -1115,31 +1303,34 @@ void nsImageLoadingContent::UpdateImageS
   // be user-disabled.  Otherwise, claim to be broken.
   if (mImageBlockingStatus == nsIContentPolicy::REJECT_SERVER) {
     mSuppressed = true;
   } else if (mImageBlockingStatus == nsIContentPolicy::REJECT_TYPE) {
     mUserDisabled = true;
   } else if (!mCurrentRequest) {
     // No current request means error, since we weren't disabled or suppressed
     mBroken = true;
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_BROKEN);
   } else {
     uint32_t currentLoadStatus;
     nsresult rv = mCurrentRequest->GetImageStatus(&currentLoadStatus);
     if (NS_FAILED(rv) || (currentLoadStatus & imgIRequest::STATUS_ERROR)) {
       mBroken = true;
+      RejectDecodePromises(NS_ERROR_DOM_IMAGE_BROKEN);
     } else if (!(currentLoadStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) {
       mLoading = true;
     }
   }
 
   NS_ASSERTION(thisContent->IsElement(), "Not an element?");
   thisContent->AsElement()->UpdateState(aNotify);
 }
 
 void nsImageLoadingContent::CancelImageRequests(bool aNotify) {
+  RejectDecodePromises(NS_ERROR_DOM_IMAGE_INVALID_REQUEST);
   AutoStateChanger changer(this, aNotify);
   ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
   ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
 }
 
 Document* nsImageLoadingContent::GetOurOwnerDoc() {
   return AsContent()->OwnerDoc();
 }
@@ -1178,16 +1369,17 @@ nsresult nsImageLoadingContent::StringTo
   return NS_NewURI(aURI, aSpec, encoding, baseURL,
                    nsContentUtils::GetIOService());
 }
 
 nsresult nsImageLoadingContent::FireEvent(const nsAString& aEventType,
                                           bool aIsCancelable) {
   if (nsContentUtils::DocumentInactiveForImageLoads(GetOurOwnerDoc())) {
     // Don't bother to fire any events, especially error events.
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT);
     return NS_OK;
   }
 
   // We have to fire the event asynchronously so that we won't go into infinite
   // loops in cases when onLoad handlers reset the src and the new src is in
   // cache.
 
   nsCOMPtr<nsINode> thisNode =
@@ -1310,16 +1502,23 @@ class ImageRequestAutoLock {
   nsCOMPtr<imgIRequest> mRequest;
 };
 
 }  // namespace
 
 void nsImageLoadingContent::MakePendingRequestCurrent() {
   MOZ_ASSERT(mPendingRequest);
 
+  // If we have a pending request, we know that there is an existing current
+  // request with size information. If the pending request is for a different
+  // URI, then we need to reject any outstanding promises.
+  nsCOMPtr<nsIURI> uri;
+  mPendingRequest->GetURI(getter_AddRefs(uri));
+  MaybeAgeRequestGeneration(uri);
+
   // Lock mCurrentRequest for the duration of this method.  We do this because
   // PrepareCurrentRequest() might unlock mCurrentRequest.  If mCurrentRequest
   // and mPendingRequest are both requests for the same image, unlocking
   // mCurrentRequest before we lock mPendingRequest can cause the lock count
   // to go to 0 and the image to be discarded!
   ImageRequestAutoLock autoLock(mCurrentRequest);
 
   ImageLoadType loadType = (mPendingRequestFlags & REQUEST_IS_IMAGESET)
@@ -1403,16 +1602,22 @@ bool nsImageLoadingContent::HaveSize(img
   if (!aImage) return false;
 
   // Query the image
   uint32_t status;
   nsresult rv = aImage->GetImageStatus(&status);
   return (NS_SUCCEEDED(rv) && (status & imgIRequest::STATUS_SIZE_AVAILABLE));
 }
 
+void nsImageLoadingContent::NotifyOwnerDocumentActivityChanged() {
+  if (!GetOurOwnerDoc()->IsCurrentActiveDocument()) {
+    RejectDecodePromises(NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT);
+  }
+}
+
 void nsImageLoadingContent::BindToTree(Document* aDocument, nsIContent* aParent,
                                        nsIContent* aBindingParent) {
   // We may be getting connected, if so our image should be tracked,
   if (GetOurCurrentDoc()) {
     TrackImage(mCurrentRequest);
     TrackImage(mPendingRequest);
   }
 }
--- a/dom/base/nsImageLoadingContent.h
+++ b/dom/base/nsImageLoadingContent.h
@@ -18,16 +18,17 @@
 #include "mozilla/EventStates.h"
 #include "mozilla/TimeStamp.h"
 #include "nsCOMPtr.h"
 #include "nsIImageLoadingContent.h"
 #include "nsIRequest.h"
 #include "mozilla/ErrorResult.h"
 #include "nsIContentPolicy.h"
 #include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Promise.h"
 #include "mozilla/net/ReferrerPolicy.h"
 #include "nsAttrValue.h"
 
 class nsIURI;
 class nsPresContext;
 class nsIContent;
 class imgRequestProxy;
 
@@ -77,16 +78,22 @@ class nsImageLoadingContent : public nsI
 
   mozilla::dom::Element* FindImageMap();
 
   /**
    * Toggle whether or not to synchronously decode an image on draw.
    */
   void SetSyncDecodingHint(bool aHint);
 
+  /**
+   * Notify us that the document state has changed. Called by nsDocument so that
+   * we may reject any promises which require the document to be active.
+   */
+  void NotifyOwnerDocumentActivityChanged();
+
  protected:
   enum ImageLoadType {
     // Most normal image loads
     eImageLoadType_Normal,
     // From a <img srcset> or <picture> context. Affects type given to content
     // policy.
     eImageLoadType_Imageset
   };
@@ -225,27 +232,71 @@ class nsImageLoadingContent : public nsI
   // Get ourselves as an nsIContent*.  Not const because some of the callers
   // want a non-const nsIContent.
   virtual nsIContent* AsContent() = 0;
 
   // Hooks for subclasses to call to get the intrinsic width and height.
   uint32_t NaturalWidth();
   uint32_t NaturalHeight();
 
+  /**
+   * Create a promise and queue a microtask which will ensure the current
+   * request (after any pending loads are applied) has requested a full decode.
+   * The promise is fulfilled once the request has a fully decoded surface that
+   * is available for drawing, or an error condition occurrs (e.g. broken image,
+   * current request is updated, etc).
+   *
+   * https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decode
+   */
+  already_AddRefed<mozilla::dom::Promise> QueueDecodeAsync(
+      mozilla::ErrorResult& aRv);
+
   enum class ImageDecodingType : uint8_t {
     Auto,
     Async,
     Sync,
   };
 
   static const nsAttrValue::EnumTable kDecodingTable[];
   static const nsAttrValue::EnumTable* kDecodingTableDefault;
 
  private:
   /**
+   * Enqueue and/or fulfill a promise created by QueueDecodeAsync.
+   */
+  void DecodeAsync(RefPtr<mozilla::dom::Promise>&& aPromise,
+                   uint32_t aRequestGeneration);
+
+  /**
+   * Attempt to resolve all queued promises based on the state of the current
+   * request. If the current request does not yet have all of the encoded data,
+   * or the decoding has not yet completed, it will return without changing the
+   * promise states.
+   */
+  void MaybeResolveDecodePromises();
+
+  /**
+   * Reject all queued promises with the given status.
+   */
+  void RejectDecodePromises(nsresult aStatus);
+
+  /**
+   * Age the generation counter if we have a new current request with a
+   * different URI. If the generation counter is aged, then all queued promises
+   * will also be rejected.
+   */
+  void MaybeAgeRequestGeneration(nsIURI* aNewURI);
+
+  /**
+   * Deregister as an observer for the owner document's activity notifications
+   * if we have no outstanding decode promises.
+   */
+  void MaybeDeregisterActivityObserver();
+
+  /**
    * Struct used to manage the native image observers.
    */
   struct ImageObserver {
     explicit ImageObserver(imgINotificationObserver* aObserver);
     ~ImageObserver();
 
     nsCOMPtr<imgINotificationObserver> mObserver;
     ImageObserver* mNext;
@@ -479,23 +530,46 @@ class nsImageLoadingContent : public nsI
   /**
    * Typically we will have no scripted observers, as this is only used by
    * chrome, legacy extensions, and some mochitests. An empty array reserves
    * minimal memory.
    */
   nsTArray<RefPtr<ScriptedImageObserver>> mScriptedObservers;
 
   /**
+   * Promises created by QueueDecodeAsync that are still waiting to be
+   * fulfilled by the image being fully decoded.
+   */
+  nsTArray<RefPtr<mozilla::dom::Promise>> mDecodePromises;
+
+  /**
    * When mIsImageStateForced is true, this holds the ImageState that we'll
    * return in ImageState().
    */
   mozilla::EventStates mForcedImageState;
 
   mozilla::TimeStamp mMostRecentRequestChange;
 
+  /**
+   * Total number of outstanding decode promises, including those stored in
+   * mDecodePromises and those embedded in runnables waiting to be enqueued.
+   * This is used to determine whether we need to register as an observer for
+   * document activity notifications.
+   */
+  size_t mOutstandingDecodePromises;
+
+  /**
+   * An incrementing counter representing the current request generation;
+   * Each time mCurrentRequest is modified with a different URI, this will
+   * be incremented. Each QueueDecodeAsync call will cache the generation
+   * of the current request so that when it is processed, it knows if it
+   * should have rejected because the request changed.
+   */
+  uint32_t mRequestGeneration;
+
   int16_t mImageBlockingStatus;
   bool mLoadingEnabled : 1;
 
   /**
    * When true, we return mForcedImageState from ImageState().
    */
   bool mIsImageStateForced : 1;
 
--- a/dom/base/nsNodeUtils.cpp
+++ b/dom/base/nsNodeUtils.cpp
@@ -494,16 +494,26 @@ already_AddRefed<nsINode> nsNodeUtils::C
         mediaElem->NotifyOwnerDocumentActivityChanged();
       }
       nsCOMPtr<nsIObjectLoadingContent> objectLoadingContent(
           do_QueryInterface(aNode));
       if (objectLoadingContent) {
         nsObjectLoadingContent* olc =
             static_cast<nsObjectLoadingContent*>(objectLoadingContent.get());
         olc->NotifyOwnerDocumentActivityChanged();
+      } else {
+        // HTMLImageElement::FromNode is insufficient since we need this for
+        // <svg:image> as well.
+        nsCOMPtr<nsIImageLoadingContent> imageLoadingContent(
+            do_QueryInterface(aNode));
+        if (imageLoadingContent) {
+          auto ilc =
+              static_cast<nsImageLoadingContent*>(imageLoadingContent.get());
+          ilc->NotifyOwnerDocumentActivityChanged();
+        }
       }
     }
 
     if (oldDoc != newDoc && oldDoc->MayHaveDOMMutationObservers()) {
       newDoc->SetMayHaveDOMMutationObservers();
     }
 
     if (oldDoc != newDoc && oldDoc->MayHaveAnimationObservers()) {
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -922,16 +922,17 @@ void nsObjectLoadingContent::NotifyOwner
   // XXX(johns): We cannot touch plugins or run arbitrary script from this call,
   //             as Document is in a non-reentrant state.
 
   // If we have a plugin we want to queue an event to stop it unless we are
   // moved into an active document before returning to the event loop.
   if (mInstanceOwner || mInstantiating) {
     QueueCheckPluginStopEvent();
   }
+  nsImageLoadingContent::NotifyOwnerDocumentActivityChanged();
 }
 
 // nsIRequestObserver
 NS_IMETHODIMP
 nsObjectLoadingContent::OnStartRequest(nsIRequest* aRequest) {
   AUTO_PROFILER_LABEL("nsObjectLoadingContent::OnStartRequest", NETWORK);
 
   LOG(("OBJLC [%p]: Channel OnStartRequest", this));
--- a/dom/html/HTMLImageElement.cpp
+++ b/dom/html/HTMLImageElement.cpp
@@ -186,16 +186,20 @@ CSSIntPoint HTMLImageElement::GetXY() {
 int32_t HTMLImageElement::X() { return GetXY().x; }
 
 int32_t HTMLImageElement::Y() { return GetXY().y; }
 
 void HTMLImageElement::GetDecoding(nsAString& aValue) {
   GetEnumAttr(nsGkAtoms::decoding, kDecodingTableDefault->tag, aValue);
 }
 
+already_AddRefed<Promise> HTMLImageElement::Decode(ErrorResult& aRv) {
+  return nsImageLoadingContent::QueueDecodeAsync(aRv);
+}
+
 bool HTMLImageElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
                                       const nsAString& aValue,
                                       nsIPrincipal* aMaybeScriptedPrincipal,
                                       nsAttrValue& aResult) {
   if (aNamespaceID == kNameSpaceID_None) {
     if (aAttribute == nsGkAtoms::align) {
       return ParseAlignValue(aValue, aResult);
     }
--- a/dom/html/HTMLImageElement.h
+++ b/dom/html/HTMLImageElement.h
@@ -175,16 +175,18 @@ class HTMLImageElement final : public ns
   void GetReferrerPolicy(nsAString& aReferrer) {
     GetEnumAttr(nsGkAtoms::referrerpolicy, EmptyCString().get(), aReferrer);
   }
   void SetDecoding(const nsAString& aDecoding, ErrorResult& aError) {
     SetHTMLAttr(nsGkAtoms::decoding, aDecoding, aError);
   }
   void GetDecoding(nsAString& aValue);
 
+  already_AddRefed<Promise> Decode(ErrorResult& aRv);
+
   net::ReferrerPolicy GetImageReferrerPolicy() override {
     return GetReferrerPolicyAsEnum();
   }
 
   MOZ_CAN_RUN_SCRIPT int32_t X();
   MOZ_CAN_RUN_SCRIPT int32_t Y();
   void GetLowsrc(nsAString& aLowsrc) {
     GetURIAttr(nsGkAtoms::lowsrc, nullptr, aLowsrc);
--- a/dom/svg/SVGImageElement.cpp
+++ b/dom/svg/SVGImageElement.cpp
@@ -96,16 +96,20 @@ already_AddRefed<DOMSVGAnimatedString> S
              ? mStringAttributes[HREF].ToDOMAnimatedString(this)
              : mStringAttributes[XLINK_HREF].ToDOMAnimatedString(this);
 }
 
 void SVGImageElement::GetDecoding(nsAString& aValue) {
   GetEnumAttr(nsGkAtoms::decoding, kDecodingTableDefault->tag, aValue);
 }
 
+already_AddRefed<Promise> SVGImageElement::Decode(ErrorResult& aRv) {
+  return nsImageLoadingContent::QueueDecodeAsync(aRv);
+}
+
 //----------------------------------------------------------------------
 
 nsresult SVGImageElement::LoadSVGImage(bool aForce, bool aNotify) {
   // resolve href attribute
   nsCOMPtr<nsIURI> baseURI = GetBaseURI();
 
   nsAutoString href;
   if (mStringAttributes[HREF].IsExplicitlySet()) {
--- a/dom/svg/SVGImageElement.h
+++ b/dom/svg/SVGImageElement.h
@@ -88,16 +88,18 @@ class SVGImageElement : public SVGImageE
   already_AddRefed<DOMSVGAnimatedPreserveAspectRatio> PreserveAspectRatio();
   already_AddRefed<DOMSVGAnimatedString> Href();
 
   void SetDecoding(const nsAString& aDecoding, ErrorResult& aError) {
     SetAttr(nsGkAtoms::decoding, aDecoding, aError);
   }
   void GetDecoding(nsAString& aValue);
 
+  already_AddRefed<Promise> Decode(ErrorResult& aRv);
+
  protected:
   nsresult LoadSVGImage(bool aForce, bool aNotify);
 
   virtual LengthAttributesInfo GetLengthInfo() override;
   virtual SVGAnimatedPreserveAspectRatio* GetPreserveAspectRatio() override;
   virtual StringAttributesInfo GetStringInfo() override;
 
   // Override for nsImageLoadingContent.
--- a/dom/webidl/HTMLImageElement.webidl
+++ b/dom/webidl/HTMLImageElement.webidl
@@ -37,16 +37,18 @@ interface HTMLImageElement : HTMLElement
            attribute unsigned long width;
            [CEReactions, SetterThrows]
            attribute unsigned long height;
            [CEReactions, SetterThrows]
            attribute DOMString decoding;
   readonly attribute unsigned long naturalWidth;
   readonly attribute unsigned long naturalHeight;
   readonly attribute boolean complete;
+           [NewObject]
+           Promise<void> decode();
 };
 
 // http://www.whatwg.org/specs/web-apps/current-work/#other-elements,-attributes-and-apis
 partial interface HTMLImageElement {
            [CEReactions, SetterThrows]
            attribute DOMString name;
            [CEReactions, SetterThrows]
            attribute DOMString align;
--- a/dom/webidl/SVGImageElement.webidl
+++ b/dom/webidl/SVGImageElement.webidl
@@ -18,13 +18,15 @@ interface SVGImageElement : SVGGraphicsE
   [Constant]
   readonly attribute SVGAnimatedLength width;
   [Constant]
   readonly attribute SVGAnimatedLength height;
   [Constant]
   readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio;
   [CEReactions, SetterThrows]
   attribute DOMString decoding;
+  [NewObject]
+  Promise<void> decode();
 };
 
 SVGImageElement implements MozImageLoadingContent;
 SVGImageElement implements SVGURIReference;
 
--- a/image/DynamicImage.cpp
+++ b/image/DynamicImage.cpp
@@ -220,16 +220,18 @@ DynamicImage::Draw(gfxContext* aContext,
   return ImgDrawResult::SUCCESS;
 }
 
 NS_IMETHODIMP
 DynamicImage::StartDecoding(uint32_t aFlags) { return NS_OK; }
 
 bool DynamicImage::StartDecodingWithResult(uint32_t aFlags) { return true; }
 
+bool DynamicImage::RequestDecodeWithResult(uint32_t aFlags) { return true; }
+
 NS_IMETHODIMP
 DynamicImage::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags) {
   return NS_OK;
 }
 
 NS_IMETHODIMP
 DynamicImage::LockImage() { return NS_OK; }
 
--- a/image/ImageWrapper.cpp
+++ b/image/ImageWrapper.cpp
@@ -197,16 +197,20 @@ NS_IMETHODIMP
 ImageWrapper::StartDecoding(uint32_t aFlags) {
   return mInnerImage->StartDecoding(aFlags);
 }
 
 bool ImageWrapper::StartDecodingWithResult(uint32_t aFlags) {
   return mInnerImage->StartDecodingWithResult(aFlags);
 }
 
+bool ImageWrapper::RequestDecodeWithResult(uint32_t aFlags) {
+  return mInnerImage->RequestDecodeWithResult(aFlags);
+}
+
 NS_IMETHODIMP
 ImageWrapper::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags) {
   return mInnerImage->RequestDecodeForSize(aSize, aFlags);
 }
 
 NS_IMETHODIMP
 ImageWrapper::LockImage() {
   MOZ_ASSERT(NS_IsMainThread(),
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -334,53 +334,55 @@ LookupResult RasterImage::LookupFrame(co
   LookupResult result =
       LookupFrameInternal(requestedSize, aFlags, aPlaybackType, aMarkUsed);
 
   if (!result && !mHasSize) {
     // We can't request a decode without knowing our intrinsic size. Give up.
     return LookupResult(MatchType::NOT_FOUND);
   }
 
+  const bool syncDecode = aFlags & FLAG_SYNC_DECODE;
+  const bool avoidRedecode = aFlags & FLAG_AVOID_REDECODE_FOR_SIZE;
   if (result.Type() == MatchType::NOT_FOUND ||
-      result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
-      ((aFlags & FLAG_SYNC_DECODE) && !result)) {
+      (result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND &&
+       !avoidRedecode) ||
+      (syncDecode && !avoidRedecode && !result)) {
     // We don't have a copy of this frame, and there's no decoder working on
     // one. (Or we're sync decoding and the existing decoder hasn't even started
     // yet.) Trigger decoding so it'll be available next time.
     MOZ_ASSERT(aPlaybackType != PlaybackType::eAnimated ||
                    gfxPrefs::ImageMemAnimatedDiscardable() ||
                    !mAnimationState || mAnimationState->KnownFrameCount() < 1,
                "Animated frames should be locked");
 
     // The surface cache may suggest the preferred size we are supposed to
     // decode at. This should only happen if we accept substitutions.
     if (!result.SuggestedSize().IsEmpty()) {
-      MOZ_ASSERT(!(aFlags & FLAG_SYNC_DECODE) &&
-                 (aFlags & FLAG_HIGH_QUALITY_SCALING));
+      MOZ_ASSERT(!syncDecode && (aFlags & FLAG_HIGH_QUALITY_SCALING));
       requestedSize = result.SuggestedSize();
     }
 
     bool ranSync = Decode(requestedSize, aFlags, aPlaybackType);
 
     // If we can or did sync decode, we should already have the frame.
-    if (ranSync || (aFlags & FLAG_SYNC_DECODE)) {
+    if (ranSync || syncDecode) {
       result =
           LookupFrameInternal(requestedSize, aFlags, aPlaybackType, aMarkUsed);
     }
   }
 
   if (!result) {
     // We still weren't able to get a frame. Give up.
     return result;
   }
 
   // Sync decoding guarantees that we got the frame, but if it's owned by an
   // async decoder that's currently running, the contents of the frame may not
   // be available yet. Make sure we get everything.
-  if (mAllSourceData && (aFlags & FLAG_SYNC_DECODE)) {
+  if (mAllSourceData && syncDecode) {
     result.Surface()->WaitUntilFinished();
   }
 
   // If we could have done some decoding in this function we need to check if
   // that decoding encountered an error and hence aborted the surface. We want
   // to avoid calling IsAborted if we weren't passed any sync decode flag
   // because IsAborted acquires the monitor for the imgFrame.
   if (aFlags & (FLAG_SYNC_DECODE | FLAG_SYNC_DECODE_IF_FAST) &&
@@ -1073,16 +1075,28 @@ bool RasterImage::StartDecodingWithResul
   }
 
   uint32_t flags = (aFlags & FLAG_ASYNC_NOTIFY) | FLAG_SYNC_DECODE_IF_FAST |
                    FLAG_HIGH_QUALITY_SCALING;
   DrawableSurface surface = RequestDecodeForSizeInternal(mSize, flags);
   return surface && surface->IsFinished();
 }
 
+bool RasterImage::RequestDecodeWithResult(uint32_t aFlags) {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mError) {
+    return false;
+  }
+
+  uint32_t flags = aFlags | FLAG_ASYNC_NOTIFY;
+  DrawableSurface surface = RequestDecodeForSizeInternal(mSize, flags);
+  return surface && surface->IsFinished();
+}
+
 NS_IMETHODIMP
 RasterImage::RequestDecodeForSize(const IntSize& aSize, uint32_t aFlags) {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -1231,16 +1231,21 @@ VectorImage::StartDecoding(uint32_t aFla
   return NS_OK;
 }
 
 bool VectorImage::StartDecodingWithResult(uint32_t aFlags) {
   // SVG images are ready to draw when they are loaded
   return mIsFullyLoaded;
 }
 
+bool VectorImage::RequestDecodeWithResult(uint32_t aFlags) {
+  // SVG images are ready to draw when they are loaded
+  return mIsFullyLoaded;
+}
+
 NS_IMETHODIMP
 VectorImage::RequestDecodeForSize(const nsIntSize& aSize, uint32_t aFlags) {
   // Nothing to do for SVG images, though in theory we could rasterize to the
   // provided size ahead of time if we supported off-main-thread SVG
   // rasterization...
   return NS_OK;
 }
 
--- a/image/imgIContainer.idl
+++ b/image/imgIContainer.idl
@@ -204,29 +204,34 @@ interface imgIContainer : nsISupports
    * FLAG_FORCE_UNIFORM_SCALING: Signal to ClippedImage::OptimalSizeForDest that
    * its returned size can only scale the image's size *uniformly* (by the same
    * factor in each dimension). We need this flag when painting border-image
    * section with SVG image source-data, if the SVG image has no viewBox and no
    * intrinsic size. In such a case, we synthesize a viewport for the SVG image
    * (a "window into SVG space") based on the border image area, and we need to
    * be sure we don't subsequently scale that viewport in a way that distorts
    * its contents by stretching them more in one dimension than the other.
+   *
+   * FLAG_AVOID_REDECODE_FOR_SIZE: If there is already a raster surface
+   * available for this image, but it is not the same size as requested, skip
+   * starting a new decode for said size.
    */
   const unsigned long FLAG_NONE                            = 0x0;
   const unsigned long FLAG_SYNC_DECODE                     = 0x1;
   const unsigned long FLAG_SYNC_DECODE_IF_FAST             = 0x2;
   const unsigned long FLAG_ASYNC_NOTIFY                    = 0x4;
   const unsigned long FLAG_DECODE_NO_PREMULTIPLY_ALPHA     = 0x8;
   const unsigned long FLAG_DECODE_NO_COLORSPACE_CONVERSION = 0x10;
   const unsigned long FLAG_CLAMP                           = 0x20;
   const unsigned long FLAG_HIGH_QUALITY_SCALING            = 0x40;
   const unsigned long FLAG_WANT_DATA_SURFACE               = 0x80;
   const unsigned long FLAG_BYPASS_SURFACE_CACHE            = 0x100;
   const unsigned long FLAG_FORCE_PRESERVEASPECTRATIO_NONE  = 0x200;
   const unsigned long FLAG_FORCE_UNIFORM_SCALING           = 0x400;
+  const unsigned long FLAG_AVOID_REDECODE_FOR_SIZE         = 0x800;
 
   /**
    * A constant specifying the default set of decode flags (i.e., the default
    * values for FLAG_DECODE_*).
    */
   const unsigned long DECODE_FLAGS_DEFAULT = 0;
 
   /**
@@ -472,16 +477,27 @@ interface imgIContainer : nsISupports
    */
   [noscript, notxpcom] boolean startDecodingWithResult(in uint32_t aFlags);
 
   /*
    * This method triggers decoding for an image, but unlike startDecoding() it
    * enables the caller to provide more detailed information about the decode
    * request.
    *
+   * @param aFlags Flags of the FLAG_* variety.
+   * @return True there is a surface that satisfies the request and it is
+   *         fully decoded, else false.
+   */
+  [noscript, notxpcom] boolean requestDecodeWithResult(in uint32_t aFlags);
+
+  /*
+   * This method triggers decoding for an image, but unlike startDecoding() it
+   * enables the caller to provide more detailed information about the decode
+   * request.
+   *
    * @param aSize The size to which the image should be scaled while decoding,
    *              if possible. If the image cannot be scaled to this size while
    *              being decoded, it will be decoded at its intrinsic size.
    * @param aFlags Flags of the FLAG_* variety.
    */
   [noscript] void requestDecodeForSize([const] in nsIntSize aSize,
                                        in uint32_t aFlags);
 
--- a/image/imgIRequest.idl
+++ b/image/imgIRequest.idl
@@ -158,20 +158,34 @@ interface imgIRequest : nsIRequest
    * container already exists, or calls it once the container becomes available
    * if it does not yet exist.
    */
   void startDecoding(in uint32_t aFlags);
 
   /**
    * Exactly like startDecoding above except returns whether the current frame
    * of the image is complete or not.
+   *
+   * @param aFlags Flags of the FLAG_* variety. Only FLAG_ASYNC_NOTIFY
+   *               is accepted; all others are ignored.
    */
   [noscript, notxpcom] boolean startDecodingWithResult(in uint32_t aFlags);
 
   /**
+   * This method triggers decoding for an image, but unlike startDecoding() it
+   * enables the caller to provide more detailed information about the decode
+   * request.
+   *
+   * @param aFlags Flags of the FLAG_* variety.
+   * @return True there is a surface that satisfies the request and it is
+   *         fully decoded, else false.
+   */
+  [noscript, notxpcom] boolean requestDecodeWithResult(in uint32_t aFlags);
+
+  /**
    * Locks an image. If the image does not exist yet, locks it once it becomes
    * available. The lock persists for the lifetime of the imgIRequest (until
    * unlockImage is called) even if the underlying image changes.
    *
    * If you don't call unlockImage() by the time this imgIRequest goes away, it
    * will be called for you automatically.
    *
    * @see imgIContainer::lockImage for documentation of the underlying call.
--- a/image/imgRequestProxy.cpp
+++ b/image/imgRequestProxy.cpp
@@ -538,16 +538,34 @@ bool imgRequestProxy::StartDecodingWithR
 
   if (GetOwner()) {
     GetOwner()->StartDecoding();
   }
 
   return false;
 }
 
+bool imgRequestProxy::RequestDecodeWithResult(uint32_t aFlags) {
+  if (IsValidating()) {
+    mDecodeRequested = true;
+    return false;
+  }
+
+  RefPtr<Image> image = GetImage();
+  if (image) {
+    return image->RequestDecodeWithResult(aFlags);
+  }
+
+  if (GetOwner()) {
+    GetOwner()->StartDecoding();
+  }
+
+  return false;
+}
+
 NS_IMETHODIMP
 imgRequestProxy::LockImage() {
   mLockCount++;
   RefPtr<Image> image = GetImage();
   if (image) {
     return image->LockImage();
   }
   return NS_OK;
--- a/testing/web-platform/meta/html/dom/interfaces.https.html.ini
+++ b/testing/web-platform/meta/html/dom/interfaces.https.html.ini
@@ -472,25 +472,16 @@ prefs: [dom.security.featurePolicy.enabl
     expected: FAIL
 
   [HTMLBodyElement interface: document.createElement("body") must inherit property "onrejectionhandled" with the proper type]
     expected: FAIL
 
   [HTMLBodyElement interface: document.createElement("body") must inherit property "onunhandledrejection" with the proper type]
     expected: FAIL
 
-  [HTMLImageElement interface: operation decode()]
-    expected: FAIL
-
-  [HTMLImageElement interface: document.createElement("img") must inherit property "decode()" with the proper type]
-    expected: FAIL
-
-  [HTMLImageElement interface: new Image() must inherit property "decode()" with the proper type]
-    expected: FAIL
-
   [HTMLIFrameElement interface: attribute allowUserMedia]
     expected: FAIL
 
   [HTMLIFrameElement interface: attribute delegateStickyUserActivation]
     expected: FAIL
 
   [HTMLVideoElement interface: attribute playsInline]
     expected: FAIL
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-iframe.html.ini
+++ b/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-iframe.html.ini
@@ -1,21 +1,9 @@
 [image-decode-iframe.html]
-  expected:
-    ERROR
-  [(misc) Decode from removed iframe fails (loaded img)]
-    expected: TIMEOUT
-
-  [(misc) Decode from removed iframe fails (img not loaded)]
-    expected: NOTRUN
-
-  [(misc) Decode from iframe, later removed, fails (img not loaded)]
-    expected: NOTRUN
-
-  [HTMLImageElement.prototype.decode(), iframe tests. Decode from removed iframe fails (loaded img)]
-    expected: TIMEOUT
+  expected: TIMEOUT
 
   [HTMLImageElement.prototype.decode(), iframe tests. Decode from removed iframe fails (img not loaded)]
-    expected: NOTRUN
+    expected: TIMEOUT
 
   [HTMLImageElement.prototype.decode(), iframe tests. Decode from iframe, later removed, fails (img not loaded)]
     expected: NOTRUN
 
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes-svg.tentative.html.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[image-decode-path-changes-svg.tentative.html]
-  [SVGImageElement.prototype.decode(), href mutation tests. xlink:href changes fail decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), href mutation tests. href changes fail decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), href mutation tests. xlink:href changes fail decode; following good decode succeeds.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), href mutation tests. href changes fail decode; following good decode succeeds.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), href mutation tests. xlink:href changes fail decode; following bad decode fails.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), href mutation tests. href changes fail decode; following bad decode fails.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini
+++ /dev/null
@@ -1,49 +0,0 @@
-[image-decode-path-changes.html]
-  [(src) Path changes fail decode.]
-    expected: FAIL
-
-  [(src) Path changes fail decode; following good decode succeeds.]
-    expected: FAIL
-
-  [(src) Path changes fail decode; following bad decode fails.]
-    expected: FAIL
-
-  [(src) Path changes to the same path succeed.]
-    expected: FAIL
-
-  [(srcset) Path changes fail decode.]
-    expected: FAIL
-
-  [(srcset) Path changes fail decode; following good decode succeeds.]
-    expected: FAIL
-
-  [(srcset) Path changes fail decode; following bad decode fails.]
-    expected: FAIL
-
-  [(srcset) Path changes to the same path succeed.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good png decode succeeds.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good svg decode succeeds.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following bad decode fails.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes to the same path succeed.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode; following good decode succeeds.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode; following bad decode fails.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-picture.html.ini
+++ /dev/null
@@ -1,25 +0,0 @@
-[image-decode-picture.html]
-  [HTMLImageElement.prototype.decode(), picture tests. Image with PNG source decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Image with multiple sources decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Image with PNG data URL source decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Image with SVG source decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Non-existent source fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Corrupt image in src fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Image without srcset fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), picture tests. Multiple decodes for images with src succeed.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-svg.tentative.html.ini
+++ /dev/null
@@ -1,40 +0,0 @@
-[image-decode-svg.tentative.html]
-  [SVGImageElement.prototype.decode(), basic tests. Image with PNG xlink:href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image with PNG href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image with PNG data URL xlink:href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image with PNG data URL href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image with SVG xlink:href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image with SVG href decodes with undefined.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Non-existent xlink:href fails decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Non-existent href fails decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Corrupt image in xlink:href fails decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Corrupt image in href fails decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Image without xlink:href or href fails decode.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Multiple decodes with a xlink:href succeed.]
-    expected: FAIL
-
-  [SVGImageElement.prototype.decode(), basic tests. Multiple decodes with a href succeed.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach-svg.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[image-decode-with-quick-attach-svg.tentative.html]
-  [SVGImageElement.prototype.decode(), attach to DOM before promise resolves.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[image-decode-with-quick-attach.html]
-  [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-img-element/decode/image-decode.html.ini
+++ /dev/null
@@ -1,76 +0,0 @@
-[image-decode.html]
-  [(src) PNG image decodes with undefined.]
-    expected: FAIL
-
-  [(src) PNG url image decodes with undefined.]
-    expected: FAIL
-
-  [(src) SVG image decodes with undefined.]
-    expected: FAIL
-
-  [(src) Non-existent path fails decode.]
-    expected: FAIL
-
-  [(src) Corrupt image fails decode.]
-    expected: FAIL
-
-  [(src) Path-less image fails decode.]
-    expected: FAIL
-
-  [(src) Multiple decodes succeed.]
-    expected: FAIL
-
-  [(srcset) PNG image decodes with undefined.]
-    expected: FAIL
-
-  [(srcset) SVG image decodes with undefined.]
-    expected: FAIL
-
-  [(srcset) Non-existent path fails decode.]
-    expected: FAIL
-
-  [(srcset) Multiple decodes succeed.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image with PNG src decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image with PNG data URL src decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image with SVG src decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Non-existent src fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Inactive document fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Adopted active image into inactive document fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Adopted inactive image into active document succeeds.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Corrupt image in src fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image without src/srcset fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Multiple decodes for images with src succeed.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image with PNG srcset decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Image with SVG srcset decodes with undefined.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Non-existent srcset fails decode.]
-    expected: FAIL
-
-  [HTMLImageElement.prototype.decode(), basic tests. Multiple decodes for images with srcset succeed.]
-    expected: FAIL
-
--- a/xpcom/base/ErrorList.py
+++ b/xpcom/base/ErrorList.py
@@ -637,16 +637,21 @@ with modules["DOM"]:
     errors["NS_ERROR_DOM_INVALID_STATE_XHR_CHUNKED_RESPONSETYPES_UNSUPPORTED_FOR_SYNC"] = FAILURE(1024)  # NOQA: E501
     errors["NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC"] = FAILURE(1025)  # NOQA: E501
 
     # When manipulating the bytecode cache with the JS API, some transcoding
     # errors, such as a different bytecode format can cause failures of the
     # decoding process.
     errors["NS_ERROR_DOM_JS_DECODING_ERROR"] = FAILURE(1026)
 
+    # Image decode errors.
+    errors["NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT"] = FAILURE(1027)
+    errors["NS_ERROR_DOM_IMAGE_INVALID_REQUEST"] = FAILURE(1028)
+    errors["NS_ERROR_DOM_IMAGE_BROKEN"] = FAILURE(1029)
+
     # May be used to indicate when e.g. setting a property value didn't
     # actually change the value, like for obj.foo = "bar"; obj.foo = "bar";
     # the second assignment throws NS_SUCCESS_DOM_NO_OPERATION.
     errors["NS_SUCCESS_DOM_NO_OPERATION"] = SUCCESS(1)
 
     # A success code that indicates that evaluating a string of JS went
     # just fine except it threw an exception. Only for legacy use by
     # nsJSUtils.