Bug 1194059 (Part 2) - Always detect IS_ANIMATED during the metadata decode. r=tn
authorSeth Fowler <mark.seth.fowler@gmail.com>
Fri, 14 Aug 2015 00:37:13 -0700
changeset 257777 167ceb9650797d3281a4940450604893dc6bad26
parent 257776 ddd36b0369e685d65bb380dfac623151a02d5654
child 257778 c263278045e2aa6214149359b18c9bd97df522bb
push id29226
push userryanvm@gmail.com
push dateFri, 14 Aug 2015 13:01:14 +0000
treeherdermozilla-central@1b2402247429 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstn
bugs1194059
milestone43.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 1194059 (Part 2) - Always detect IS_ANIMATED during the metadata decode. r=tn
image/Decoder.cpp
image/Decoder.h
image/DecoderFactory.cpp
image/DecoderFactory.h
image/FrameAnimator.cpp
image/FrameAnimator.h
image/ImageMetadata.cpp
image/ImageMetadata.h
image/RasterImage.cpp
image/RasterImage.h
image/decoders/nsGIFDecoder2.cpp
image/decoders/nsICODecoder.cpp
image/decoders/nsPNGDecoder.cpp
image/moz.build
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -32,20 +32,18 @@ Decoder::Decoder(RasterImage* aImage)
   , mFailCode(NS_OK)
   , mChunkCount(0)
   , mFlags(0)
   , mBytesDecoded(0)
   , mInitialized(false)
   , mMetadataDecode(false)
   , mSendPartialInvalidations(false)
   , mImageIsTransient(false)
-  , mImageIsLocked(false)
   , mFirstFrameDecode(false)
   , mInFrame(false)
-  , mIsAnimated(false)
   , mDataDone(false)
   , mDecodeDone(false)
   , mDataError(false)
   , mDecodeAborted(false)
   , mShouldReportError(false)
 { }
 
 Decoder::~Decoder()
@@ -232,17 +230,17 @@ Decoder::CompleteDecode()
   }
 
   if (mDecodeDone && !IsMetadataDecode()) {
     MOZ_ASSERT(HasError() || mCurrentFrame, "Should have an error or a frame");
 
     // If this image wasn't animated and isn't a transient image, mark its frame
     // as optimizable. We don't support optimizing animated images and
     // optimizing transient images isn't worth it.
-    if (!mIsAnimated && !mImageIsTransient && mCurrentFrame) {
+    if (!HasAnimation() && !mImageIsTransient && mCurrentFrame) {
       mCurrentFrame->SetOptimizable();
     }
   }
 }
 
 nsresult
 Decoder::AllocateFrame(uint32_t aFrameNum,
                        const nsIntSize& aTargetSize,
@@ -255,17 +253,22 @@ Decoder::AllocateFrame(uint32_t aFrameNu
                                         aPaletteDepth, mCurrentFrame.get());
 
   if (mCurrentFrame) {
     // Gather the raw pointers the decoders will use.
     mCurrentFrame->GetImageData(&mImageData, &mImageDataLength);
     mCurrentFrame->GetPaletteData(&mColormap, &mColormapSize);
 
     if (aFrameNum + 1 == mFrameCount) {
-      PostFrameStart();
+      // If we're past the first frame, PostIsAnimated() should've been called.
+      MOZ_ASSERT_IF(mFrameCount > 1, HasAnimation());
+
+      // Update our state to reflect the new frame
+      MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!");
+      mInFrame = true;
     }
   } else {
     PostDataError();
   }
 
   return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE;
 }
 
@@ -401,29 +404,21 @@ Decoder::PostSize(int32_t aWidth,
 
 void
 Decoder::PostHasTransparency()
 {
   mProgress |= FLAG_HAS_TRANSPARENCY;
 }
 
 void
-Decoder::PostFrameStart()
+Decoder::PostIsAnimated(int32_t aFirstFrameTimeout)
 {
-  // We shouldn't already be mid-frame
-  MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!");
-
-  // Update our state to reflect the new frame
-  mInFrame = true;
-
-  // If we just became animated, record that fact.
-  if (mFrameCount > 1) {
-    mIsAnimated = true;
-    mProgress |= FLAG_IS_ANIMATED;
-  }
+  mProgress |= FLAG_IS_ANIMATED;
+  mImageMetadata.SetHasAnimation();
+  mImageMetadata.SetFirstFrameTimeout(aFirstFrameTimeout);
 }
 
 void
 Decoder::PostFrameStop(Opacity aFrameOpacity    /* = Opacity::TRANSPARENT */,
                        DisposalMethod aDisposalMethod
                                                 /* = DisposalMethod::KEEP */,
                        int32_t aTimeout         /* = 0 */,
                        BlendMethod aBlendMethod /* = BlendMethod::OVER */)
@@ -437,34 +432,34 @@ Decoder::PostFrameStop(Opacity aFrameOpa
   mInFrame = false;
 
   mCurrentFrame->Finish(aFrameOpacity, aDisposalMethod, aTimeout, aBlendMethod);
 
   mProgress |= FLAG_FRAME_COMPLETE;
 
   // If we're not sending partial invalidations, then we send an invalidation
   // here when the first frame is complete.
-  if (!mSendPartialInvalidations && !mIsAnimated) {
+  if (!mSendPartialInvalidations && !HasAnimation()) {
     mInvalidRect.UnionRect(mInvalidRect,
                            gfx::IntRect(gfx::IntPoint(0, 0), GetSize()));
   }
 }
 
 void
 Decoder::PostInvalidation(const nsIntRect& aRect,
                           const Maybe<nsIntRect>& aRectAtTargetSize
                             /* = Nothing() */)
 {
   // We should be mid-frame
   MOZ_ASSERT(mInFrame, "Can't invalidate when not mid-frame!");
   MOZ_ASSERT(mCurrentFrame, "Can't invalidate when not mid-frame!");
 
   // Record this invalidation, unless we're not sending partial invalidations
   // or we're past the first frame.
-  if (mSendPartialInvalidations && !mIsAnimated) {
+  if (mSendPartialInvalidations && !HasAnimation()) {
     mInvalidRect.UnionRect(mInvalidRect, aRect);
     mCurrentFrame->ImageUpdated(aRectAtTargetSize.valueOr(aRect));
   }
 }
 
 void
 Decoder::PostDecodeDone(int32_t aLoopCount /* = 0 */)
 {
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -177,30 +177,16 @@ public:
    */
   void SetImageIsTransient(bool aIsTransient)
   {
     MOZ_ASSERT(!mInitialized, "Shouldn't be initialized yet");
     mImageIsTransient = aIsTransient;
   }
 
   /**
-   * Set whether the image is locked for the lifetime of this decoder. We lock
-   * the image during our initial decode to ensure that we don't evict any
-   * surfaces before we realize that the image is animated.
-   */
-  void SetImageIsLocked()
-  {
-    MOZ_ASSERT(!mInitialized, "Shouldn't be initialized yet");
-    mImageIsLocked = true;
-  }
-
-  bool ImageIsLocked() const { return mImageIsLocked; }
-
-
-  /**
    * Set whether we should stop decoding after the first frame.
    */
   void SetIsFirstFrameDecode()
   {
     MOZ_ASSERT(!mInitialized, "Shouldn't be initialized yet");
     mFirstFrameDecode = true;
   }
 
@@ -220,17 +206,17 @@ public:
 
   // The number of complete frames we have (ie, not including anything
   // in-progress).
   uint32_t GetCompleteFrameCount() {
     return mInFrame ? mFrameCount - 1 : mFrameCount;
   }
 
   // Did we discover that the image we're decoding is animated?
-  bool HasAnimation() const { return mIsAnimated; }
+  bool HasAnimation() const { return mImageMetadata.HasAnimation(); }
 
   // Error tracking
   bool HasError() const { return HasDataError() || HasDecoderError(); }
   bool HasDataError() const { return mDataError; }
   bool HasDecoderError() const { return NS_FAILED(mFailCode); }
   bool ShouldReportError() const { return mShouldReportError; }
   nsresult GetDecoderError() const { return mFailCode; }
   void PostResizeError() { PostDataError(); }
@@ -339,19 +325,21 @@ protected:
   // possibility that the image has transparency, for example because its header
   // specifies that it has an alpha channel, we fire PostHasTransparency
   // immediately. PostFrameStop's aFrameOpacity argument, on the other hand, is
   // only used internally to ImageLib. Because PostFrameStop isn't delivered
   // until the entire frame has been decoded, decoders may take into account the
   // actual contents of the frame and give a more accurate result.
   void PostHasTransparency();
 
-  // Called by decoders when they begin a frame. Informs the image, sends
-  // notifications, and does internal book-keeping.
-  void PostFrameStart();
+  // Called by decoders if they determine that the image is animated.
+  //
+  // @param aTimeout The time for which the first frame should be shown before
+  //                 we advance to the next frame.
+  void PostIsAnimated(int32_t aFirstFrameTimeout);
 
   // Called by decoders when they end a frame. Informs the image, sends
   // notifications, and does internal book-keeping.
   // Specify whether this frame is opaque as an optimization.
   // For animated images, specify the disposal, blend method and timeout for
   // this frame.
   void PostFrameStop(Opacity aFrameOpacity = Opacity::SOME_TRANSPARENCY,
                      DisposalMethod aDisposalMethod = DisposalMethod::KEEP,
@@ -446,20 +434,18 @@ private:
 
   uint32_t mFlags;
   size_t mBytesDecoded;
 
   bool mInitialized : 1;
   bool mMetadataDecode : 1;
   bool mSendPartialInvalidations : 1;
   bool mImageIsTransient : 1;
-  bool mImageIsLocked : 1;
   bool mFirstFrameDecode : 1;
   bool mInFrame : 1;
-  bool mIsAnimated : 1;
   bool mDataDone : 1;
   bool mDecodeDone : 1;
   bool mDataError : 1;
   bool mDecodeAborted : 1;
   bool mShouldReportError : 1;
 };
 
 } // namespace image
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -108,38 +108,34 @@ DecoderFactory::GetDecoder(DecoderType a
 DecoderFactory::CreateDecoder(DecoderType aType,
                               RasterImage* aImage,
                               SourceBuffer* aSourceBuffer,
                               const Maybe<IntSize>& aTargetSize,
                               uint32_t aFlags,
                               int aSampleSize,
                               const IntSize& aResolution,
                               bool aIsRedecode,
-                              bool aImageIsTransient,
-                              bool aImageIsLocked)
+                              bool aImageIsTransient)
 {
   if (aType == DecoderType::UNKNOWN) {
     return nullptr;
   }
 
   nsRefPtr<Decoder> decoder = GetDecoder(aType, aImage, aIsRedecode);
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
   decoder->SetMetadataDecode(false);
   decoder->SetIterator(aSourceBuffer->Iterator());
   decoder->SetFlags(aFlags);
   decoder->SetSampleSize(aSampleSize);
   decoder->SetResolution(aResolution);
   decoder->SetSendPartialInvalidations(!aIsRedecode);
   decoder->SetImageIsTransient(aImageIsTransient);
-
-  if (aImageIsLocked) {
-    decoder->SetImageIsLocked();
-  }
+  decoder->SetIsFirstFrameDecode();
 
   // Set a target size for downscale-during-decode if applicable.
   if (aTargetSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize);
     MOZ_ASSERT(nsresult(rv) != NS_ERROR_NOT_AVAILABLE,
                "We're downscale-during-decode but decoder doesn't support it?");
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
   }
@@ -148,16 +144,49 @@ DecoderFactory::CreateDecoder(DecoderTyp
   if (NS_FAILED(decoder->GetDecoderError())) {
     return nullptr;
   }
 
   return decoder.forget();
 }
 
 /* static */ already_AddRefed<Decoder>
+DecoderFactory::CreateAnimationDecoder(DecoderType aType,
+                                       RasterImage* aImage,
+                                       SourceBuffer* aSourceBuffer,
+                                       uint32_t aFlags,
+                                       const IntSize& aResolution)
+{
+  if (aType == DecoderType::UNKNOWN) {
+    return nullptr;
+  }
+
+  MOZ_ASSERT(aType == DecoderType::GIF || aType == DecoderType::PNG,
+             "Calling CreateAnimationDecoder for non-animating DecoderType");
+
+  nsRefPtr<Decoder> decoder =
+    GetDecoder(aType, aImage, /* aIsRedecode = */ true);
+  MOZ_ASSERT(decoder, "Should have a decoder now");
+
+  // Initialize the decoder.
+  decoder->SetMetadataDecode(false);
+  decoder->SetIterator(aSourceBuffer->Iterator());
+  decoder->SetFlags(aFlags);
+  decoder->SetResolution(aResolution);
+  decoder->SetSendPartialInvalidations(false);
+
+  decoder->Init();
+  if (NS_FAILED(decoder->GetDecoderError())) {
+    return nullptr;
+  }
+
+  return decoder.forget();
+}
+
+/* static */ already_AddRefed<Decoder>
 DecoderFactory::CreateMetadataDecoder(DecoderType aType,
                                       RasterImage* aImage,
                                       SourceBuffer* aSourceBuffer,
                                       int aSampleSize,
                                       const IntSize& aResolution)
 {
   if (aType == DecoderType::UNKNOWN) {
     return nullptr;
--- a/image/DecoderFactory.h
+++ b/image/DecoderFactory.h
@@ -33,22 +33,23 @@ enum class DecoderType
 
 class DecoderFactory
 {
 public:
   /// @return the type of decoder which is appropriate for @aMimeType.
   static DecoderType GetDecoderType(const char* aMimeType);
 
   /**
-   * Creates and initializes a decoder of type @aType. The decoder will send
-   * notifications to @aImage.
+   * Creates and initializes a decoder for non-animated images of type @aType.
+   * (If the image *is* animated, only the first frame will be decoded.) The
+   * decoder will send notifications to @aImage.
    *
-   * XXX(seth): @aIsRedecode, @aImageIsTransient, and @aImageIsLocked should
-   * really be part of @aFlags. This requires changes to the way that decoder
-   * flags work, though. See bug 1185800.
+   * XXX(seth): @aIsRedecode and @aImageIsTransient should really be part of
+   * @aFlags. This requires changes to the way that decoder flags work, though.
+   * See bug 1185800.
    *
    * @param aType Which type of decoder to create - JPEG, PNG, etc.
    * @param aImage The image will own the decoder and which should receive
    *               notifications as decoding progresses.
    * @param aSourceBuffer The SourceBuffer which the decoder will read its data
    *                      from.
    * @param aTargetSize If not Nothing(), the target size which the image should
    *                    be scaled to during decoding. It's an error to specify
@@ -57,31 +58,48 @@ public:
    * @param aFlags Flags specifying what type of output the decoder should
    *               produce; see GetDecodeFlags() in RasterImage.h.
    * @param aSampleSize The sample size requested using #-moz-samplesize (or 0
    *                    if none).
    * @param aResolution The resolution requested using #-moz-resolution (or an
    *                    empty rect if none).
    * @param aIsRedecode Specify 'true' if this image has been decoded before.
    * @param aImageIsTransient Specify 'true' if this image is transient.
-   * @param aImageIsLocked Specify 'true' if this image is locked for the
-   *                       lifetime of this decoder, and should be unlocked
-   *                       when the decoder finishes.
    */
   static already_AddRefed<Decoder>
   CreateDecoder(DecoderType aType,
                 RasterImage* aImage,
                 SourceBuffer* aSourceBuffer,
                 const Maybe<gfx::IntSize>& aTargetSize,
                 uint32_t aFlags,
                 int aSampleSize,
                 const gfx::IntSize& aResolution,
                 bool aIsRedecode,
-                bool aImageIsTransient,
-                bool aImageIsLocked);
+                bool aImageIsTransient);
+
+  /**
+   * Creates and initializes a decoder for animated images of type @aType.
+   * The decoder will send notifications to @aImage.
+   *
+   * @param aType Which type of decoder to create - JPEG, PNG, etc.
+   * @param aImage The image will own the decoder and which should receive
+   *               notifications as decoding progresses.
+   * @param aSourceBuffer The SourceBuffer which the decoder will read its data
+   *                      from.
+   * @param aFlags Flags specifying what type of output the decoder should
+   *               produce; see GetDecodeFlags() in RasterImage.h.
+   * @param aResolution The resolution requested using #-moz-resolution (or an
+   *                    empty rect if none).
+   */
+  static already_AddRefed<Decoder>
+  CreateAnimationDecoder(DecoderType aType,
+                         RasterImage* aImage,
+                         SourceBuffer* aSourceBuffer,
+                         uint32_t aFlags,
+                         const gfx::IntSize& aResolution);
 
   /**
    * Creates and initializes a metadata decoder of type @aType. This decoder
    * will only decode the image's header, extracting metadata like the size of
    * the image. No actual image data will be decoded and no surfaces will be
    * allocated. The decoder will send notifications to @aImage.
    *
    * @param aType Which type of decoder to create - JPEG, PNG, etc.
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -286,42 +286,47 @@ FrameAnimator::GetCompositedFrame(uint32
   MOZ_ASSERT(!result || !result.DrawableRef()->GetIsPaletted(),
              "About to return a paletted frame");
   return result;
 }
 
 int32_t
 FrameAnimator::GetTimeoutForFrame(uint32_t aFrameNum) const
 {
+  int32_t rawTimeout = 0;
+
   RawAccessFrameRef frame = GetRawFrame(aFrameNum);
-  if (!frame) {
+  if (frame) {
+    AnimationData data = frame->GetAnimationData();
+    rawTimeout = data.mRawTimeout;
+  } else if (aFrameNum == 0) {
+    rawTimeout = mFirstFrameTimeout;
+  } else {
     NS_WARNING("No frame; called GetTimeoutForFrame too early?");
     return 100;
   }
 
-  AnimationData data = frame->GetAnimationData();
-
   // Ensure a minimal time between updates so we don't throttle the UI thread.
   // consider 0 == unspecified and make it fast but not too fast.  Unless we
   // have a single loop GIF. See bug 890743, bug 125137, bug 139677, and bug
   // 207059. The behavior of recent IE and Opera versions seems to be:
   // IE 6/Win:
   //   10 - 50ms go 100ms
   //   >50ms go correct speed
   // Opera 7 final/Win:
   //   10ms goes 100ms
   //   >10ms go correct speed
   // It seems that there are broken tools out there that set a 0ms or 10ms
   // timeout when they really want a "default" one.  So munge values in that
   // range.
-  if (data.mRawTimeout >= 0 && data.mRawTimeout <= 10) {
+  if (rawTimeout >= 0 && rawTimeout <= 10) {
     return 100;
   }
 
-  return data.mRawTimeout;
+  return rawTimeout;
 }
 
 static void
 DoCollectSizeOfCompositingSurfaces(const RawAccessFrameRef& aSurface,
                                    SurfaceMemoryCounterType aType,
                                    nsTArray<SurfaceMemoryCounter>& aCounters,
                                    MallocSizeOf aMallocSizeOf)
 {
--- a/image/FrameAnimator.h
+++ b/image/FrameAnimator.h
@@ -28,16 +28,17 @@ public:
                 gfx::IntSize aSize,
                 uint16_t aAnimationMode)
     : mImage(aImage)
     , mSize(aSize)
     , mCurrentAnimationFrameIndex(0)
     , mLoopRemainingCount(-1)
     , mLastCompositedFrameIndex(-1)
     , mLoopCount(-1)
+    , mFirstFrameTimeout(0)
     , mAnimationMode(aAnimationMode)
     , mDoneDecoding(false)
   { }
 
   /**
    * Return value from RequestRefresh. Tells callers what happened in that call
    * to RequestRefresh.
    */
@@ -143,16 +144,22 @@ public:
 
   /*
    * Set number of times to loop the image.
    * @note -1 means loop forever.
    */
   void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; }
   int32_t LoopCount() const { return mLoopCount; }
 
+  /*
+   * Set the timeout for the first frame. This is used to allow animation
+   * scheduling even before a full decode runs for this image.
+   */
+  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+
   /**
    * Collect an accounting of the memory occupied by the compositing surfaces we
    * use during animation playback. All of the actual animation frames are
    * stored in the SurfaceCache, so we don't need to report them here.
    */
   void CollectSizeOfCompositingSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                         MallocSizeOf aMallocSizeOf) const;
 
@@ -272,16 +279,19 @@ private: // data
   int32_t mLoopRemainingCount;
 
   //! Track the last composited frame for Optimizations (See DoComposite code)
   int32_t mLastCompositedFrameIndex;
 
   //! The total number of loops for the image.
   int32_t mLoopCount;
 
+  //! The timeout for the first frame of this image.
+  int32_t mFirstFrameTimeout;
+
   //! The animation mode of this image. Constants defined in imgIContainer.
   uint16_t mAnimationMode;
 
   //! Whether this image is done being decoded.
   bool mDoneDecoding;
 };
 
 } // namespace image
deleted file mode 100644
--- a/image/ImageMetadata.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-/* -*- 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 "ImageMetadata.h"
-
-#include "RasterImage.h"
-#include "nsComponentManagerUtils.h"
-#include "nsISupportsPrimitives.h"
-#include "nsXPCOMCID.h"
-
-namespace mozilla {
-namespace image {
-
-nsresult
-ImageMetadata::SetOnImage(RasterImage* aImage)
-{
-  nsresult rv = NS_OK;
-
-  if (mHotspotX != -1 && mHotspotY != -1) {
-    nsCOMPtr<nsISupportsPRUint32> intwrapx =
-      do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
-    nsCOMPtr<nsISupportsPRUint32> intwrapy =
-      do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
-    intwrapx->SetData(mHotspotX);
-    intwrapy->SetData(mHotspotY);
-    aImage->Set("hotspotX", intwrapx);
-    aImage->Set("hotspotY", intwrapy);
-  }
-
-  aImage->SetLoopCount(mLoopCount);
-
-  if (HasSize()) {
-    MOZ_ASSERT(HasOrientation(), "Should have orientation");
-    rv = aImage->SetSize(GetWidth(), GetHeight(), GetOrientation());
-  }
-
-  return rv;
-}
-
-} // namespace image
-} // namespace mozilla
--- a/image/ImageMetadata.h
+++ b/image/ImageMetadata.h
@@ -17,58 +17,64 @@ namespace image {
 
 class RasterImage;
 
 // The metadata about an image that decoders accumulate as they decode.
 class ImageMetadata
 {
 public:
   ImageMetadata()
-    : mHotspotX(-1)
-    , mHotspotY(-1)
-    , mLoopCount(-1)
+    : mLoopCount(-1)
+    , mFirstFrameTimeout(0)
+    , mHasAnimation(false)
   { }
 
-  // Set the metadata this object represents on an image.
-  nsresult SetOnImage(RasterImage* aImage);
+  void SetHotspot(uint16_t aHotspotX, uint16_t aHotspotY)
+  {
+    mHotspot = Some(gfx::IntPoint(aHotspotX, aHotspotY));
+  }
+  gfx::IntPoint GetHotspot() const { return *mHotspot; }
+  bool HasHotspot() const { return mHotspot.isSome(); }
 
-  void SetHotspot(uint16_t hotspotx, uint16_t hotspoty)
-  {
-    mHotspotX = hotspotx;
-    mHotspotY = hotspoty;
-  }
   void SetLoopCount(int32_t loopcount)
   {
     mLoopCount = loopcount;
   }
+  int32_t GetLoopCount() const { return mLoopCount; }
+
+  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+  int32_t GetFirstFrameTimeout() const { return mFirstFrameTimeout; }
 
   void SetSize(int32_t width, int32_t height, Orientation orientation)
   {
     if (!HasSize()) {
       mSize.emplace(nsIntSize(width, height));
       mOrientation.emplace(orientation);
     }
   }
-
+  nsIntSize GetSize() const { return *mSize; }
+  Orientation GetOrientation() const { return *mOrientation; }
   bool HasSize() const { return mSize.isSome(); }
   bool HasOrientation() const { return mOrientation.isSome(); }
 
-  int32_t GetWidth() const { return mSize->width; }
-  int32_t GetHeight() const { return mSize->height; }
-  nsIntSize GetSize() const { return *mSize; }
-  Orientation GetOrientation() const { return *mOrientation; }
+  void SetHasAnimation() { mHasAnimation = true; }
+  bool HasAnimation() const { return mHasAnimation; }
 
 private:
-  // The hotspot found on cursors, or -1 if none was found.
-  int32_t mHotspotX;
-  int32_t mHotspotY;
+  /// The hotspot found on cursors, if present.
+  Maybe<gfx::IntPoint> mHotspot;
 
-  // The loop count for animated images, or -1 for infinite loop.
+  /// The loop count for animated images, or -1 for infinite loop.
   int32_t mLoopCount;
 
+  /// The timeout of an animated image's first frame.
+  int32_t mFirstFrameTimeout;
+
   Maybe<nsIntSize> mSize;
   Maybe<Orientation> mOrientation;
+
+  bool mHasAnimation : 1;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_ImageMetadata_h
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -19,16 +19,17 @@
 #include "prsystem.h"
 #include "ImageContainer.h"
 #include "ImageRegion.h"
 #include "Layers.h"
 #include "LookupResult.h"
 #include "nsIConsoleService.h"
 #include "nsIInputStream.h"
 #include "nsIScriptError.h"
+#include "nsISupportsPrimitives.h"
 #include "nsPresContext.h"
 #include "SourceBuffer.h"
 #include "SurfaceCache.h"
 #include "FrameAnimator.h"
 
 #include "gfxContext.h"
 
 #include "mozilla/gfx/2D.h"
@@ -473,17 +474,17 @@ RasterImage::LookupFrame(uint32_t aFrame
   }
 
   if (result.Type() == MatchType::NOT_FOUND ||
       result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
       ((aFlags & FLAG_SYNC_DECODE) && !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(!mAnim, "Animated frames should be locked");
+    MOZ_ASSERT(!mAnim || GetNumFrames() < 1, "Animated frames should be locked");
 
     Decode(requestedSize, aFlags);
 
     // If we can sync decode, we should already have the frame.
     if (aFlags & FLAG_SYNC_DECODE) {
       result = LookupFrameInternal(aFrameNum, requestedSize, aFlags);
     }
   }
@@ -524,17 +525,17 @@ uint32_t
 RasterImage::GetRequestedFrameIndex(uint32_t aWhichFrame) const
 {
   return aWhichFrame == FRAME_FIRST ? 0 : GetCurrentFrameIndex();
 }
 
 IntRect
 RasterImage::GetFirstFrameRect()
 {
-  if (mAnim) {
+  if (mAnim && mHasBeenDecoded) {
     return mAnim->GetFirstFrameRefreshArea();
   }
 
   // Fall back to our size. This is implicitly zero-size if !mHasSize.
   return IntRect(IntPoint(0,0), mSize);
 }
 
 NS_IMETHODIMP_(bool)
@@ -577,17 +578,20 @@ RasterImage::GetAnimated(bool* aAnimated
 
   // If we have mAnim, we can know for sure
   if (mAnim) {
     *aAnimated = true;
     return NS_OK;
   }
 
   // Otherwise, we need to have been decoded to know for sure, since if we were
-  // decoded at least once mAnim would have been created for animated images
+  // decoded at least once mAnim would have been created for animated images.
+  // This is true even though we check for animation during the metadata decode,
+  // because we may still discover animation only during the full decode for
+  // corrupt images.
   if (!mHasBeenDecoded) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // We know for sure
   *aAnimated = false;
 
   return NS_OK;
@@ -916,70 +920,97 @@ RasterImage::OnAddedFrame(uint32_t aNewF
   if (mError) {
     return;  // We're in an error state, possibly due to OOM. Bail.
   }
 
   if (aNewFrameCount > mFrameCount) {
     mFrameCount = aNewFrameCount;
 
     if (aNewFrameCount == 2) {
-      // We're becoming animated, so initialize animation stuff.
-      MOZ_ASSERT(!mAnim, "Already have animation state?");
-      mAnim = MakeUnique<FrameAnimator>(this, mSize, mAnimationMode);
+      MOZ_ASSERT(mAnim, "Should already have animation state");
 
-      // We don't support discarding animated images (See bug 414259).
-      // Lock the image and throw away the key.
-      //
-      // Note that this is inefficient, since we could get rid of the source
-      // data too. However, doing this is actually hard, because we're probably
-      // mid-decode, and thus we're decoding out of the source buffer. Since
-      // we're going to fix this anyway later, and since we didn't kill the
-      // source data in the old world either, locking is acceptable for the
-      // moment.
-      LockImage();
-
+      // We may be able to start animating.
       if (mPendingAnimation && ShouldAnimate()) {
         StartAnimation();
       }
     }
     if (aNewFrameCount > 1) {
       mAnim->UnionFirstFrameRefreshArea(aNewRefreshArea);
     }
   }
 }
 
 nsresult
-RasterImage::SetSize(int32_t aWidth, int32_t aHeight, Orientation aOrientation)
+RasterImage::SetMetadata(const ImageMetadata& aMetadata,
+                         bool aFromMetadataDecode)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
-  // Ensure that we have positive values
-  // XXX - Why isn't the size unsigned? Should this be changed?
-  if ((aWidth < 0) || (aHeight < 0)) {
-    return NS_ERROR_INVALID_ARG;
+  if (aMetadata.HasSize()) {
+    IntSize size = aMetadata.GetSize();
+    if (size.width < 0 || size.height < 0) {
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    MOZ_ASSERT(aMetadata.HasOrientation());
+    Orientation orientation = aMetadata.GetOrientation();
+
+    // If we already have a size, check the new size against the old one.
+    if (mHasSize && (size != mSize || orientation != mOrientation)) {
+      NS_WARNING("Image changed size or orientation on redecode! "
+                 "This should not happen!");
+      DoError();
+      return NS_ERROR_UNEXPECTED;
+    }
+
+    // Set the size and flag that we have it.
+    mSize = size;
+    mOrientation = orientation;
+    mHasSize = true;
   }
 
-  // if we already have a size, check the new size against the old one
-  if (mHasSize &&
-      ((aWidth != mSize.width) ||
-       (aHeight != mSize.height) ||
-       (aOrientation != mOrientation))) {
-    NS_WARNING("Image changed size on redecode! This should not happen!");
-    DoError();
-    return NS_ERROR_UNEXPECTED;
+  if (mHasSize && aMetadata.HasAnimation() && !mAnim) {
+    // We're becoming animated, so initialize animation stuff.
+    mAnim = MakeUnique<FrameAnimator>(this, mSize, mAnimationMode);
+
+    // We don't support discarding animated images (See bug 414259).
+    // Lock the image and throw away the key.
+    LockImage();
+
+    if (!aFromMetadataDecode) {
+      // The metadata decode reported that this image isn't animated, but we
+      // discovered that it actually was during the full decode. This is a
+      // rare failure that only occurs for corrupt images. To recover, we need
+      // to discard all existing surfaces and redecode.
+      RecoverFromLossOfFrames(mSize, DECODE_FLAGS_DEFAULT);
+    }
   }
 
-  // Set the size and flag that we have it
-  mSize.SizeTo(aWidth, aHeight);
-  mOrientation = aOrientation;
-  mHasSize = true;
+  if (mAnim) {
+    mAnim->SetLoopCount(aMetadata.GetLoopCount());
+    mAnim->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout());
+  }
+
+  if (aMetadata.HasHotspot()) {
+    IntPoint hotspot = aMetadata.GetHotspot();
+
+    nsCOMPtr<nsISupportsPRUint32> intwrapx =
+      do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
+    nsCOMPtr<nsISupportsPRUint32> intwrapy =
+      do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
+    intwrapx->SetData(hotspot.x);
+    intwrapy->SetData(hotspot.y);
+
+    Set("hotspotX", intwrapx);
+    Set("hotspotY", intwrapy);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 RasterImage::SetAnimationMode(uint16_t aAnimationMode)
 {
   if (mAnim) {
@@ -994,20 +1025,19 @@ nsresult
 RasterImage::StartAnimation()
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   MOZ_ASSERT(ShouldAnimate(), "Should not animate!");
 
-  // If we don't have mAnim yet, then we're not ready to animate.  Setting
-  // mPendingAnimation will cause us to start animating as soon as we have a
-  // second frame, which causes mAnim to be constructed.
-  mPendingAnimation = !mAnim;
+  // If we're not ready to animate, then set mPendingAnimation, which will cause
+  // us to start animating if and when we do become ready.
+  mPendingAnimation = !mAnim || GetNumFrames() < 2;
   if (mPendingAnimation) {
     return NS_OK;
   }
 
   // A timeout of -1 means we should display this frame forever.
   if (mAnim->GetTimeoutForFrame(GetCurrentFrameIndex()) < 0) {
     mAnimationFinished = true;
     return NS_ERROR_ABORT;
@@ -1086,29 +1116,16 @@ NS_IMETHODIMP_(float)
 RasterImage::GetFrameIndex(uint32_t aWhichFrame)
 {
   MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument");
   return (aWhichFrame == FRAME_FIRST || !mAnim)
          ? 0.0f
          : mAnim->GetCurrentAnimationFrameIndex();
 }
 
-void
-RasterImage::SetLoopCount(int32_t aLoopCount)
-{
-  if (mError) {
-    return;
-  }
-
-  // No need to set this if we're not an animation.
-  if (mAnim) {
-    mAnim->SetLoopCount(aLoopCount);
-  }
-}
-
 NS_IMETHODIMP_(IntRect)
 RasterImage::GetImageSpaceInvalidationRect(const IntRect& aRect)
 {
   return aRect;
 }
 
 nsresult
 RasterImage::OnImageDataComplete(nsIRequest*, nsISupports*, nsresult aStatus,
@@ -1386,31 +1403,30 @@ RasterImage::Decode(const IntSize& aSize
   }
 
   MOZ_ASSERT(mDownscaleDuringDecode || aSize == mSize,
              "Can only decode to our intrinsic size if we're not allowed to "
              "downscale-during-decode");
 
   Maybe<IntSize> targetSize = mSize != aSize ? Some(aSize) : Nothing();
 
-  bool imageIsLocked = false;
-  if (!mHasBeenDecoded) {
-    // Lock the image while we're decoding, so that it doesn't get evicted from
-    // the SurfaceCache before we have a chance to realize that it's animated.
-    // The corresponding unlock happens in FinalizeDecoder.
-    LockImage();
-    imageIsLocked = true;
+  // Create a decoder.
+  nsRefPtr<Decoder> decoder;
+  if (mAnim) {
+    decoder = DecoderFactory::CreateAnimationDecoder(mDecoderType, this,
+                                                     mSourceBuffer, aFlags,
+                                                     mRequestedResolution);
+  } else {
+    decoder = DecoderFactory::CreateDecoder(mDecoderType, this, mSourceBuffer,
+                                            targetSize, aFlags,
+                                            mRequestedSampleSize,
+                                            mRequestedResolution,
+                                            mHasBeenDecoded, mTransient);
   }
 
-  // Create a decoder.
-  nsRefPtr<Decoder> decoder =
-    DecoderFactory::CreateDecoder(mDecoderType, this, mSourceBuffer, targetSize,
-                                  aFlags, mRequestedSampleSize, mRequestedResolution,
-                                  mHasBeenDecoded, mTransient, imageIsLocked);
-
   // Make sure DecoderFactory was able to create a decoder successfully.
   if (!decoder) {
     return NS_ERROR_FAILURE;
   }
 
   // Add a placeholder for the first frame to the SurfaceCache so we won't
   // trigger any more decoders with the same parameters.
   InsertOutcome outcome =
@@ -1948,38 +1964,30 @@ RasterImage::FinalizeDecoder(Decoder* aD
              "Finalizing a decoder in the middle of a frame");
 
   // If the decoder detected an error, log it to the error console.
   if (aDecoder->ShouldReportError() && !aDecoder->WasAborted()) {
     ReportDecoderError(aDecoder);
   }
 
   // Record all the metadata the decoder gathered about this image.
-  nsresult rv = aDecoder->GetImageMetadata().SetOnImage(this);
+  nsresult rv = SetMetadata(aDecoder->GetImageMetadata(),
+                            aDecoder->IsMetadataDecode());
   if (NS_FAILED(rv)) {
     aDecoder->PostResizeError();
   }
 
   MOZ_ASSERT(mError || mHasSize || !aDecoder->HasSize(),
              "Should have handed off size by now");
 
   if (aDecoder->GetDecodeTotallyDone() && !mError) {
     // Flag that we've been decoded before.
     mHasBeenDecoded = true;
-
-    if (aDecoder->HasAnimation()) {
-      if (mAnim) {
-        mAnim->SetDoneDecoding(true);
-      } else {
-        // The OnAddedFrame event that will create mAnim is still in the event
-        // queue. Wait for it.
-        nsCOMPtr<nsIRunnable> runnable =
-          NS_NewRunnableMethod(this, &RasterImage::MarkAnimationDecoded);
-        NS_DispatchToMainThread(runnable);
-      }
+    if (mAnim) {
+      mAnim->SetDoneDecoding(true);
     }
   }
 
   // Send out any final notifications.
   NotifyProgress(aDecoder->TakeProgress(),
                  aDecoder->TakeInvalidRect(),
                  aDecoder->GetDecodeFlags());
 
@@ -2017,40 +2025,24 @@ RasterImage::FinalizeDecoder(Decoder* aD
     // If we were waiting to fire the load event, go ahead and fire it now.
     if (mLoadProgress && wasMetadata) {
       NotifyForLoadEvent(*mLoadProgress);
       mLoadProgress = Nothing();
       NotifyProgress(FLAG_ONLOAD_UNBLOCKED);
     }
   }
 
-  if (aDecoder->ImageIsLocked()) {
-    // Unlock the image, balancing the LockImage call we made in CreateDecoder.
-    UnlockImage();
-  }
-
   // If we were a metadata decode and a full decode was requested, do it.
   if (done && wasMetadata && mWantFullDecode) {
     mWantFullDecode = false;
     RequestDecode();
   }
 }
 
 void
-RasterImage::MarkAnimationDecoded()
-{
-  MOZ_ASSERT(mAnim, "Should have an animation now");
-  if (!mAnim) {
-    return;
-  }
-
-  mAnim->SetDoneDecoding(true);
-}
-
-void
 RasterImage::ReportDecoderError(Decoder* aDecoder)
 {
   nsCOMPtr<nsIConsoleService> consoleService =
     do_GetService(NS_CONSOLESERVICE_CONTRACTID);
   nsCOMPtr<nsIScriptError> errorObject =
     do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
 
   if (consoleService && errorObject && !aDecoder->HasDecoderError()) {
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -127,16 +127,17 @@ namespace layers {
 class ImageContainer;
 class Image;
 } // namespace layers
 
 namespace image {
 
 class Decoder;
 class FrameAnimator;
+class ImageMetadata;
 class SourceBuffer;
 
 /**
  * Given a set of imgIContainer FLAG_* flags, returns those flags that can
  * affect the output of decoders.
  */
 inline MOZ_CONSTEXPR uint32_t
 DecodeFlags(uint32_t aFlags)
@@ -183,28 +184,16 @@ public:
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Decoder callbacks.
   //////////////////////////////////////////////////////////////////////////////
 
   void OnAddedFrame(uint32_t aNewFrameCount, const nsIntRect& aNewRefreshArea);
 
-  /** Sets the size and inherent orientation of the container. This should only
-   * be called by the decoder. This function may be called multiple times, but
-   * will throw an error if subsequent calls do not match the first.
-   */
-  nsresult SetSize(int32_t aWidth, int32_t aHeight, Orientation aOrientation);
-
-  /**
-   * Number of times to loop the image.
-   * @note -1 means forever.
-   */
-  void     SetLoopCount(int32_t aLoopCount);
-
   /**
    * Sends the provided progress notifications to ProgressTracker.
    *
    * Main-thread only.
    *
    * @param aProgress    The progress notifications to send.
    * @param aInvalidRect An invalidation rect to send.
    * @param aFlags       The decode flags used by the decoder that generated
@@ -217,18 +206,17 @@ public:
 
   /**
    * Records telemetry and does final teardown of the provided decoder.
    *
    * Main-thread only.
    */
   void FinalizeDecoder(Decoder* aDecoder);
 
-  // Helper methods for FinalizeDecoder.
-  void MarkAnimationDecoded();
+  // Helper method for FinalizeDecoder.
   void ReportDecoderError(Decoder* aDecoder);
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Network callbacks.
   //////////////////////////////////////////////////////////////////////////////
 
   virtual nsresult OnImageDataAvailable(nsIRequest* aRequest,
@@ -335,16 +323,29 @@ private:
 
   /**
    * Creates and runs a metadata decoder, either synchronously or
    * asynchronously according to @aFlags.
    */
   NS_IMETHOD DecodeMetadata(uint32_t aFlags);
 
   /**
+   * Sets the size, inherent orientation, animation metadata, and other
+   * information about the image gathered during decoding.
+   *
+   * This function may be called multiple times, but will throw an error if
+   * subsequent calls do not match the first.
+   *
+   * @param aMetadata The metadata to set on this image.
+   * @param aFromMetadataDecode True if this metadata came from a metadata
+   *                            decode; false if it came from a full decode.
+   */
+  nsresult SetMetadata(const ImageMetadata& aMetadata, bool aFromMetadataDecode);
+
+  /**
    * In catastrophic circumstances like a GPU driver crash, we may lose our
    * frames even if they're locked. RecoverFromLossOfFrames discards all
    * existing frames and redecodes using the provided @aSize and @aFlags.
    */
   void RecoverFromLossOfFrames(const nsIntSize& aSize, uint32_t aFlags);
 
 private: // data
   nsIntSize                  mSize;
--- a/image/decoders/nsGIFDecoder2.cpp
+++ b/image/decoders/nsGIFDecoder2.cpp
@@ -852,16 +852,21 @@ nsGIFDecoder2::WriteInternal(const char*
             method == DisposalMethod::CLEAR) {
           // We may have to display the background under this image during
           // animation playback, so we regard it as transparent.
           PostHasTransparency();
         }
       }
 
       mGIFStruct.delay_time = GETINT16(q + 1) * 10;
+
+      if (mGIFStruct.delay_time > 0) {
+        PostIsAnimated(mGIFStruct.delay_time);
+      }
+
       GETN(1, gif_consume_block);
       break;
 
     case gif_comment_extension:
       if (*q) {
         GETN(*q, gif_consume_comment);
       } else {
         GETN(1, gif_image_start);
@@ -916,21 +921,30 @@ nsGIFDecoder2::WriteInternal(const char*
 
         default:
           // 0,3-7 are yet to be defined netscape extension codes
           mGIFStruct.state = gif_error;
       }
       break;
 
     case gif_image_header: {
-      if (mGIFStruct.images_decoded > 0 && IsFirstFrameDecode()) {
-        // We're about to get a second frame, but we only want the first. Stop
-        // decoding now.
-        mGIFStruct.state = gif_done;
-        break;
+      if (mGIFStruct.images_decoded == 1) {
+        if (!HasAnimation()) {
+          // We should've already called PostIsAnimated(); this must be a
+          // corrupt animated image with a first frame timeout of zero. Signal
+          // that we're animated now, before the first-frame decode early exit
+          // below, so that RasterImage can detect that this happened.
+          PostIsAnimated(/* aFirstFrameTimeout = */ 0);
+        }
+        if (IsFirstFrameDecode()) {
+          // We're about to get a second frame, but we only want the first. Stop
+          // decoding now.
+          mGIFStruct.state = gif_done;
+          break;
+        }
       }
 
       // Get image offsets, with respect to the screen origin
       mGIFStruct.x_offset = GETINT16(q);
       mGIFStruct.y_offset = GETINT16(q + 2);
 
       // Get image width and height.
       mGIFStruct.width  = GETINT16(q + 4);
--- a/image/decoders/nsICODecoder.cpp
+++ b/image/decoders/nsICODecoder.cpp
@@ -373,18 +373,18 @@ nsICODecoder::WriteInternal(const char* 
 
   // If we have a PNG, let the PNG decoder do all of the rest of the work
   if (mIsPNG && mContainedDecoder && mPos >= mImageOffset + PNGSIGNATURESIZE) {
     if (!WriteToContainedDecoder(aBuffer, aCount)) {
       return;
     }
 
     if (!HasSize() && mContainedDecoder->HasSize()) {
-      PostSize(mContainedDecoder->GetImageMetadata().GetWidth(),
-               mContainedDecoder->GetImageMetadata().GetHeight());
+      nsIntSize size = mContainedDecoder->GetSize();
+      PostSize(size.width, size.height);
     }
 
     mPos += aCount;
     aBuffer += aCount;
     aCount = 0;
 
     // Raymond Chen says that 32bpp only are valid PNG ICOs
     // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx
@@ -474,18 +474,18 @@ nsICODecoder::WriteInternal(const char* 
       return;
     }
 
     // Write out the BMP's bitmap info header
     if (!WriteToContainedDecoder(mBIHraw, sizeof(mBIHraw))) {
       return;
     }
 
-    PostSize(mContainedDecoder->GetImageMetadata().GetWidth(),
-             mContainedDecoder->GetImageMetadata().GetHeight());
+    nsIntSize size = mContainedDecoder->GetSize();
+    PostSize(size.width, size.height);
 
     // We have the size. If we're doing a metadata decode, we're done.
     if (IsMetadataDecode()) {
       return;
     }
 
     // Sometimes the ICO BPP header field is not filled out
     // so we should trust the contained resource over our own
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -51,56 +51,59 @@ GetPNGDecoderAccountingLog()
 
 nsPNGDecoder::AnimFrameInfo::AnimFrameInfo()
  : mDispose(DisposalMethod::KEEP)
  , mBlend(BlendMethod::OVER)
  , mTimeout(0)
 { }
 
 #ifdef PNG_APNG_SUPPORTED
+
+int32_t GetNextFrameDelay(png_structp aPNG, png_infop aInfo)
+{
+  // Delay, in seconds, is delayNum / delayDen.
+  png_uint_16 delayNum = png_get_next_frame_delay_num(aPNG, aInfo);
+  png_uint_16 delayDen = png_get_next_frame_delay_den(aPNG, aInfo);
+
+  if (delayNum == 0) {
+    return 0; // SetFrameTimeout() will set to a minimum.
+  }
+
+  if (delayDen == 0) {
+    delayDen = 100; // So says the APNG spec.
+  }
+
+  // Need to cast delay_num to float to have a proper division and
+  // the result to int to avoid a compiler warning.
+  return static_cast<int32_t>(static_cast<double>(delayNum) * 1000 / delayDen);
+}
+
 nsPNGDecoder::AnimFrameInfo::AnimFrameInfo(png_structp aPNG, png_infop aInfo)
  : mDispose(DisposalMethod::KEEP)
  , mBlend(BlendMethod::OVER)
  , mTimeout(0)
 {
-  png_uint_16 delay_num, delay_den;
-  // delay, in seconds is delay_num/delay_den
-  png_byte dispose_op;
-  png_byte blend_op;
-  delay_num = png_get_next_frame_delay_num(aPNG, aInfo);
-  delay_den = png_get_next_frame_delay_den(aPNG, aInfo);
-  dispose_op = png_get_next_frame_dispose_op(aPNG, aInfo);
-  blend_op = png_get_next_frame_blend_op(aPNG, aInfo);
-
-  if (delay_num == 0) {
-    mTimeout = 0; // SetFrameTimeout() will set to a minimum
-  } else {
-    if (delay_den == 0) {
-      delay_den = 100; // so says the APNG spec
-    }
-
-    // Need to cast delay_num to float to have a proper division and
-    // the result to int to avoid compiler warning
-    mTimeout = static_cast<int32_t>(static_cast<double>(delay_num) *
-                                    1000 / delay_den);
-  }
+  png_byte dispose_op = png_get_next_frame_dispose_op(aPNG, aInfo);
+  png_byte blend_op = png_get_next_frame_blend_op(aPNG, aInfo);
 
   if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) {
     mDispose = DisposalMethod::RESTORE_PREVIOUS;
   } else if (dispose_op == PNG_DISPOSE_OP_BACKGROUND) {
     mDispose = DisposalMethod::CLEAR;
   } else {
     mDispose = DisposalMethod::KEEP;
   }
 
   if (blend_op == PNG_BLEND_OP_SOURCE) {
     mBlend = BlendMethod::SOURCE;
   } else {
     mBlend = BlendMethod::OVER;
   }
+
+  mTimeout = GetNextFrameDelay(aPNG, aInfo);
 }
 #endif
 
 // First 8 bytes of a PNG file
 const uint8_t
 nsPNGDecoder::pngSignatureBytes[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
 
 nsPNGDecoder::nsPNGDecoder(RasterImage* aImage)
@@ -590,28 +593,35 @@ nsPNGDecoder::info_callback(png_structp 
   if (channels == 1 || channels == 3) {
     decoder->format = gfx::SurfaceFormat::B8G8R8X8;
   } else if (channels == 2 || channels == 4) {
     decoder->format = gfx::SurfaceFormat::B8G8R8A8;
   } else {
     png_longjmp(decoder->mPNG, 1); // invalid number of channels
   }
 
+#ifdef PNG_APNG_SUPPORTED
+  bool isAnimated = png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL);
+  if (isAnimated) {
+    decoder->PostIsAnimated(GetNextFrameDelay(png_ptr, info_ptr));
+  }
+#endif
+
   if (decoder->IsMetadataDecode()) {
     decoder->CheckForTransparency(decoder->format,
                                   IntRect(0, 0, width, height));
 
-    // We have the size and transparency information we're looking for, so we
-    // don't need to decode any further.
+    // We have the metadata we're looking for, so we don't need to decode any
+    // further.
     decoder->mSuccessfulEarlyFinish = true;
     png_longjmp(decoder->mPNG, 1);
   }
 
 #ifdef PNG_APNG_SUPPORTED
-  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL)) {
+  if (isAnimated) {
     png_set_progressive_frame_fn(png_ptr, nsPNGDecoder::frame_info_callback,
                                  nullptr);
   }
 
   if (png_get_first_frame_is_hidden(png_ptr, info_ptr)) {
     decoder->mFrameIsHidden = true;
   } else {
 #endif
--- a/image/moz.build
+++ b/image/moz.build
@@ -54,17 +54,16 @@ UNIFIED_SOURCES += [
     'Decoder.cpp',
     'DecoderFactory.cpp',
     'DynamicImage.cpp',
     'FrameAnimator.cpp',
     'FrozenImage.cpp',
     'Image.cpp',
     'ImageCacheKey.cpp',
     'ImageFactory.cpp',
-    'ImageMetadata.cpp',
     'ImageOps.cpp',
     'ImageWrapper.cpp',
     'imgFrame.cpp',
     'imgTools.cpp',
     'MultipartImage.cpp',
     'OrientedImage.cpp',
     'ScriptedNotificationObserver.cpp',
     'ShutdownTracker.cpp',