Bug 1343499 - Expose native image sizes to imagelib users. r=tnikkel
☠☠ backed out by d9f0bdf14469 ☠ ☠
authorAndrew Osmond <aosmond@mozilla.com>
Wed, 22 Mar 2017 09:05:36 -0400
changeset 503283 0b797601dc36ce8b3758aa9b39eaa2f20356d239
parent 502876 1f295c110fe7b561027126410391a85b84c1b84e
child 503284 d9f0bdf14469ffdb47987c7eab8a6dc2fc17d18c
push id50529
push userkwierso@gmail.com
push dateThu, 23 Mar 2017 00:23:51 +0000
reviewerstnikkel
bugs1343499
milestone55.0a1
Bug 1343499 - Expose native image sizes to imagelib users. r=tnikkel
image/DynamicImage.cpp
image/DynamicImage.h
image/FrameTimeout.h
image/ImageMetadata.h
image/ImageOps.cpp
image/ImageOps.h
image/ImageWrapper.cpp
image/ImageWrapper.h
image/OrientedImage.cpp
image/OrientedImage.h
image/RasterImage.cpp
image/RasterImage.h
image/VectorImage.cpp
image/VectorImage.h
image/decoders/nsICODecoder.cpp
image/imgFrame.h
image/imgIContainer.idl
image/moz.build
image/test/gtest/Common.cpp
image/test/gtest/Common.h
image/test/gtest/TestDecodeToSurface.cpp
image/test/gtest/TestDecoders.cpp
image/test/gtest/green-multiple-sizes.ico
image/test/gtest/moz.build
--- a/image/DynamicImage.cpp
+++ b/image/DynamicImage.cpp
@@ -122,16 +122,22 @@ DynamicImage::GetWidth(int32_t* aWidth)
 
 NS_IMETHODIMP
 DynamicImage::GetHeight(int32_t* aHeight)
 {
   *aHeight = mDrawable->Size().height;
   return NS_OK;
 }
 
+nsresult
+DynamicImage::GetNativeSizes(nsTArray<IntSize>& aNativeSizes) const
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 NS_IMETHODIMP
 DynamicImage::GetIntrinsicSize(nsSize* aSize)
 {
   IntSize intSize(mDrawable->Size());
   *aSize = nsSize(intSize.width, intSize.height);
   return NS_OK;
 }
 
--- a/image/DynamicImage.h
+++ b/image/DynamicImage.h
@@ -26,16 +26,17 @@ public:
 
   explicit DynamicImage(gfxDrawable* aDrawable)
     : mDrawable(aDrawable)
   {
     MOZ_ASSERT(aDrawable, "Must have a gfxDrawable to wrap");
   }
 
   // Inherited methods from Image.
+  nsresult GetNativeSizes(nsTArray<gfx::IntSize>& aNativeSizes) const override;
   virtual already_AddRefed<ProgressTracker> GetProgressTracker() override;
   virtual size_t SizeOfSourceWithComputedFallback(
                                  MallocSizeOf aMallocSizeOf) const override;
   virtual void CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                      MallocSizeOf aMallocSizeOf) const override;
 
   virtual void IncrementAnimationConsumers() override;
   virtual void DecrementAnimationConsumers() override;
new file mode 100644
--- /dev/null
+++ b/image/FrameTimeout.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_image_FrameTimeout_h
+#define mozilla_image_FrameTimeout_h
+
+#include <stdint.h>
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+namespace image {
+
+/**
+ * FrameTimeout wraps a frame timeout value (measured in milliseconds) after
+ * first normalizing it. This normalization is necessary because some tools
+ * generate incorrect frame timeout values which we nevertheless have to
+ * support. For this reason, code that deals with frame timeouts should always
+ * use a FrameTimeout value rather than the raw value from the image header.
+ */
+struct FrameTimeout
+{
+  /**
+   * @return a FrameTimeout of zero. This should be used only for math
+   * involving FrameTimeout values. You can't obtain a zero FrameTimeout from
+   * FromRawMilliseconds().
+   */
+  static FrameTimeout Zero() { return FrameTimeout(0); }
+
+  /// @return an infinite FrameTimeout.
+  static FrameTimeout Forever() { return FrameTimeout(-1); }
+
+  /// @return a FrameTimeout obtained by normalizing a raw timeout value.
+  static FrameTimeout FromRawMilliseconds(int32_t aRawMilliseconds)
+  {
+    // Normalize all infinite timeouts to the same value.
+    if (aRawMilliseconds < 0) {
+      return FrameTimeout::Forever();
+    }
+
+    // Very small timeout values are problematic for two reasons: we don't want
+    // to burn energy redrawing animated images extremely fast, and broken tools
+    // generate these values when they actually want a "default" value, so such
+    // images won't play back right without normalization. For some context,
+    // see bug 890743, bug 125137, bug 139677, and bug 207059. The historical
+    // behavior of IE and Opera was:
+    //   IE 6/Win:
+    //     10 - 50ms is normalized to 100ms.
+    //     >50ms is used unnormalized.
+    //   Opera 7 final/Win:
+    //     10ms is normalized to 100ms.
+    //     >10ms is used unnormalized.
+    if (aRawMilliseconds >= 0 && aRawMilliseconds <= 10 ) {
+      return FrameTimeout(100);
+    }
+
+    // The provided timeout value is OK as-is.
+    return FrameTimeout(aRawMilliseconds);
+  }
+
+  bool operator==(const FrameTimeout& aOther) const
+  {
+    return mTimeout == aOther.mTimeout;
+  }
+
+  bool operator!=(const FrameTimeout& aOther) const { return !(*this == aOther); }
+
+  FrameTimeout operator+(const FrameTimeout& aOther)
+  {
+    if (*this == Forever() || aOther == Forever()) {
+      return Forever();
+    }
+
+    return FrameTimeout(mTimeout + aOther.mTimeout);
+  }
+
+  FrameTimeout& operator+=(const FrameTimeout& aOther)
+  {
+    *this = *this + aOther;
+    return *this;
+  }
+
+  /**
+   * @return this FrameTimeout's value in milliseconds. Illegal to call on a
+   * an infinite FrameTimeout value.
+   */
+  uint32_t AsMilliseconds() const
+  {
+    if (*this == Forever()) {
+      MOZ_ASSERT_UNREACHABLE("Calling AsMilliseconds() on an infinite FrameTimeout");
+      return 100;  // Fail to something sane.
+    }
+
+    return uint32_t(mTimeout);
+  }
+
+  /**
+   * @return this FrameTimeout value encoded so that non-negative values
+   * represent a timeout in milliseconds, and -1 represents an infinite
+   * timeout.
+   *
+   * XXX(seth): This is a backwards compatibility hack that should be removed.
+   */
+  int32_t AsEncodedValueDeprecated() const { return mTimeout; }
+
+private:
+  explicit FrameTimeout(int32_t aTimeout)
+    : mTimeout(aTimeout)
+  { }
+
+  int32_t mTimeout;
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_FrameTimeout_h
--- a/image/ImageMetadata.h
+++ b/image/ImageMetadata.h
@@ -6,22 +6,21 @@
 
 #ifndef mozilla_image_ImageMetadata_h
 #define mozilla_image_ImageMetadata_h
 
 #include <stdint.h>
 #include "mozilla/Maybe.h"
 #include "nsSize.h"
 #include "Orientation.h"
+#include "FrameTimeout.h"
 
 namespace mozilla {
 namespace image {
 
-class RasterImage;
-
 // The metadata about an image that decoders accumulate as they decode.
 class ImageMetadata
 {
 public:
   ImageMetadata()
     : mLoopCount(-1)
     , mFirstFrameTimeout(FrameTimeout::Forever())
     , mHasAnimation(false)
@@ -59,16 +58,23 @@ public:
     if (!HasSize()) {
       mSize.emplace(nsIntSize(width, height));
       mOrientation.emplace(orientation);
     }
   }
   nsIntSize GetSize() const { return *mSize; }
   bool HasSize() const { return mSize.isSome(); }
 
+  void AddNativeSize(const nsIntSize& aSize)
+  {
+    mNativeSizes.AppendElement(aSize);
+  }
+
+  const nsTArray<nsIntSize>& GetNativeSizes() const { return mNativeSizes; }
+
   Orientation GetOrientation() const { return *mOrientation; }
   bool HasOrientation() const { return mOrientation.isSome(); }
 
   void SetHasAnimation() { mHasAnimation = true; }
   bool HasAnimation() const { return mHasAnimation; }
 
 private:
   /// The hotspot found on cursors, if present.
@@ -85,15 +91,18 @@ private:
 
   // The area of the image that needs to be invalidated when the animation
   // loops.
   Maybe<gfx::IntRect> mFirstFrameRefreshArea;
 
   Maybe<nsIntSize> mSize;
   Maybe<Orientation> mOrientation;
 
+  // Sizes the image can natively decode to.
+  nsTArray<nsIntSize> mNativeSizes;
+
   bool mHasAnimation : 1;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_ImageMetadata_h
--- a/image/ImageOps.cpp
+++ b/image/ImageOps.cpp
@@ -9,16 +9,17 @@
 #include "ClippedImage.h"
 #include "DecodePool.h"
 #include "Decoder.h"
 #include "DecoderFactory.h"
 #include "DynamicImage.h"
 #include "FrozenImage.h"
 #include "IDecodingTask.h"
 #include "Image.h"
+#include "ImageMetadata.h"
 #include "imgIContainer.h"
 #include "mozilla/gfx/2D.h"
 #include "nsStreamUtils.h"
 #include "OrientedImage.h"
 #include "SourceBuffer.h"
 
 using namespace mozilla::gfx;
 
@@ -74,20 +75,37 @@ ImageOps::Orient(imgIContainer* aImage, 
 
 /* static */ already_AddRefed<imgIContainer>
 ImageOps::CreateFromDrawable(gfxDrawable* aDrawable)
 {
   nsCOMPtr<imgIContainer> drawableImage = new DynamicImage(aDrawable);
   return drawableImage.forget();
 }
 
-/* static */ already_AddRefed<gfx::SourceSurface>
-ImageOps::DecodeToSurface(nsIInputStream* aInputStream,
-                          const nsACString& aMimeType,
-                          uint32_t aFlags)
+class ImageOps::ImageBufferImpl final : public ImageOps::ImageBuffer {
+public:
+  ImageBufferImpl(already_AddRefed<SourceBuffer> aSourceBuffer)
+    : mSourceBuffer(aSourceBuffer)
+  { }
+
+protected:
+  ~ImageBufferImpl() override { }
+
+  virtual already_AddRefed<SourceBuffer> GetSourceBuffer()
+  {
+    RefPtr<SourceBuffer> sourceBuffer = mSourceBuffer;
+    return sourceBuffer.forget();
+  }
+
+private:
+  RefPtr<SourceBuffer> mSourceBuffer;
+};
+
+/* static */ already_AddRefed<ImageOps::ImageBuffer>
+ImageOps::CreateImageBuffer(nsIInputStream* aInputStream)
 {
   MOZ_ASSERT(aInputStream);
 
   nsresult rv;
 
   // Prepare the input stream.
   nsCOMPtr<nsIInputStream> inputStream = aInputStream;
   if (!NS_InputStreamIsBuffered(aInputStream)) {
@@ -102,37 +120,115 @@ ImageOps::DecodeToSurface(nsIInputStream
   // Figure out how much data we've been passed.
   uint64_t length;
   rv = inputStream->Available(&length);
   if (NS_FAILED(rv) || length > UINT32_MAX) {
     return nullptr;
   }
 
   // Write the data into a SourceBuffer.
-  NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+  RefPtr<SourceBuffer> sourceBuffer = new SourceBuffer();
   sourceBuffer->ExpectLength(length);
   rv = sourceBuffer->AppendFromInputStream(inputStream, length);
   if (NS_FAILED(rv)) {
     return nullptr;
   }
   // Make sure our sourceBuffer is marked as complete.
   if (sourceBuffer->IsComplete()) {
     NS_WARNING("The SourceBuffer was unexpectedly marked as complete. This may "
                "indicate either an OOM condition, or that imagelib was not "
                "initialized properly.");
     return nullptr;
   }
   sourceBuffer->Complete(NS_OK);
 
+  RefPtr<ImageBuffer> imageBuffer = new ImageBufferImpl(sourceBuffer.forget());
+  return imageBuffer.forget();
+}
+
+/* static */ nsresult
+ImageOps::DecodeMetadata(nsIInputStream* aInputStream,
+                         const nsACString& aMimeType,
+                         ImageMetadata& aMetadata)
+{
+  RefPtr<ImageBuffer> buffer = CreateImageBuffer(aInputStream);
+  return DecodeMetadata(buffer, aMimeType, aMetadata);
+}
+
+/* static */ nsresult
+ImageOps::DecodeMetadata(ImageBuffer* aBuffer,
+                         const nsACString& aMimeType,
+                         ImageMetadata& aMetadata)
+{
+  if (!aBuffer) {
+    return NS_ERROR_FAILURE;
+  }
+
+  RefPtr<SourceBuffer> sourceBuffer = aBuffer->GetSourceBuffer();
+  if (NS_WARN_IF(!sourceBuffer)) {
+    return NS_ERROR_FAILURE;
+  }
+
   // Create a decoder.
   DecoderType decoderType =
     DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get());
   RefPtr<Decoder> decoder =
-    DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer,
-                                           Nothing(), ToSurfaceFlags(aFlags));
+    DecoderFactory::CreateAnonymousMetadataDecoder(decoderType,
+                                                   WrapNotNull(sourceBuffer));
+  if (!decoder) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Run the decoder synchronously.
+  RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
+  task->Run();
+  if (!decoder->GetDecodeDone() || decoder->HasError()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  aMetadata = decoder->GetImageMetadata();
+  if (aMetadata.GetNativeSizes().IsEmpty() && aMetadata.HasSize()) {
+    aMetadata.AddNativeSize(aMetadata.GetSize());
+  }
+
+  return NS_OK;
+}
+
+/* static */ already_AddRefed<gfx::SourceSurface>
+ImageOps::DecodeToSurface(nsIInputStream* aInputStream,
+                          const nsACString& aMimeType,
+                          uint32_t aFlags,
+                          Maybe<IntSize> aSize /* = Nothing() */)
+{
+  RefPtr<ImageBuffer> buffer = CreateImageBuffer(aInputStream);
+  return DecodeToSurface(buffer, aMimeType, aFlags, aSize);
+}
+
+/* static */ already_AddRefed<gfx::SourceSurface>
+ImageOps::DecodeToSurface(ImageBuffer* aBuffer,
+                          const nsACString& aMimeType,
+                          uint32_t aFlags,
+                          Maybe<IntSize> aSize /* = Nothing() */)
+{
+  if (!aBuffer) {
+    return nullptr;
+  }
+
+  RefPtr<SourceBuffer> sourceBuffer = aBuffer->GetSourceBuffer();
+  if (NS_WARN_IF(!sourceBuffer)) {
+    return nullptr;
+  }
+
+  // Create a decoder.
+  DecoderType decoderType =
+    DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get());
+  RefPtr<Decoder> decoder =
+    DecoderFactory::CreateAnonymousDecoder(decoderType,
+                                           WrapNotNull(sourceBuffer),
+                                           aSize, ToSurfaceFlags(aFlags));
   if (!decoder) {
     return nullptr;
   }
 
   // Run the decoder synchronously.
   RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
   task->Run();
   if (!decoder->GetDecodeDone() || decoder->HasError()) {
--- a/image/ImageOps.h
+++ b/image/ImageOps.h
@@ -4,35 +4,49 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_image_ImageOps_h
 #define mozilla_image_ImageOps_h
 
 #include "nsCOMPtr.h"
 #include "nsRect.h"
+#include "ImageMetadata.h"
 
 class gfxDrawable;
 class imgIContainer;
 class nsIInputStream;
 
 namespace mozilla {
 
 namespace gfx {
 class SourceSurface;
 }
 
 namespace image {
 
 class Image;
 struct Orientation;
+class SourceBuffer;
 
 class ImageOps
 {
 public:
+  class ImageBuffer {
+  public:
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageOps::ImageBuffer);
+  protected:
+    friend class ImageOps;
+
+    ImageBuffer() { }
+    virtual ~ImageBuffer() { }
+
+    virtual already_AddRefed<SourceBuffer> GetSourceBuffer() = 0;
+  };
+
   /**
    * Creates a version of an existing image which does not animate and is frozen
    * at the first frame.
    *
    * @param aImage         The existing image.
    */
   static already_AddRefed<Image> Freeze(Image* aImage);
   static already_AddRefed<imgIContainer> Freeze(imgIContainer* aImage);
@@ -70,33 +84,78 @@ public:
    * Creates an image from a gfxDrawable.
    *
    * @param aDrawable      The gfxDrawable.
    */
   static already_AddRefed<imgIContainer>
   CreateFromDrawable(gfxDrawable* aDrawable);
 
   /**
+   * Create a buffer to be used with DecodeMetadata and DecodeToSurface. Reusing
+   * an ImageBuffer representing the given input stream is more efficient if one
+   * has multiple Decode* calls to make on that stream.
+   *
+   * @param aInputStream An input stream containing an encoded image.
+   * @return An image buffer derived from the input stream.
+   */
+  static already_AddRefed<ImageBuffer>
+  CreateImageBuffer(nsIInputStream* aInputStream);
+
+  /**
+   * Decodes an image's metadata from an nsIInputStream into the given
+   * structure. This function may be called off-main-thread.
+   *
+   * @param aInputStream An input stream containing an encoded image.
+   * @param aMimeType The MIME type of the image.
+   * @param aMetadata Where the image metadata is stored upon success.
+   * @return The status of the operation.
+   */
+  static nsresult
+  DecodeMetadata(nsIInputStream* aInputStream,
+                 const nsACString& aMimeType,
+                 ImageMetadata& aMetadata);
+
+  /**
+   * Same as above but takes an ImageBuffer instead of nsIInputStream.
+   */
+  static nsresult
+  DecodeMetadata(ImageBuffer* aBuffer,
+                 const nsACString& aMimeType,
+                 ImageMetadata& aMetadata);
+
+  /**
    * Decodes an image from an nsIInputStream directly into a SourceSurface,
    * without ever creating an Image or imgIContainer (which are mostly
    * main-thread-only). That means that this function may be called
    * off-main-thread.
    *
    * @param aInputStream An input stream containing an encoded image.
    * @param aMimeType The MIME type of the image.
    * @param aFlags Flags of the imgIContainer::FLAG_DECODE_* variety.
    * @return A SourceSurface containing the first frame of the image at its
    *         intrinsic size, or nullptr if the image cannot be decoded.
    */
   static already_AddRefed<gfx::SourceSurface>
   DecodeToSurface(nsIInputStream* aInputStream,
                   const nsACString& aMimeType,
-                  uint32_t aFlags);
+                  uint32_t aFlags,
+                  Maybe<gfx::IntSize> aSize = Nothing());
+
+  /**
+   * Same as above but takes an ImageBuffer instead of nsIInputStream.
+   */
+  static already_AddRefed<gfx::SourceSurface>
+  DecodeToSurface(ImageBuffer* aBuffer,
+                  const nsACString& aMimeType,
+                  uint32_t aFlags,
+                  Maybe<gfx::IntSize> aSize = Nothing());
 
 private:
+  class ImageBufferImpl;
+
   // This is a static utility class, so disallow instantiation.
   virtual ~ImageOps() = 0;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_ImageOps_h
--- a/image/ImageWrapper.cpp
+++ b/image/ImageWrapper.cpp
@@ -134,16 +134,22 @@ ImageWrapper::GetWidth(int32_t* aWidth)
 }
 
 NS_IMETHODIMP
 ImageWrapper::GetHeight(int32_t* aHeight)
 {
   return mInnerImage->GetHeight(aHeight);
 }
 
+nsresult
+ImageWrapper::GetNativeSizes(nsTArray<IntSize>& aNativeSizes) const
+{
+  return mInnerImage->GetNativeSizes(aNativeSizes);
+}
+
 NS_IMETHODIMP
 ImageWrapper::GetIntrinsicSize(nsSize* aSize)
 {
   return mInnerImage->GetIntrinsicSize(aSize);
 }
 
 NS_IMETHODIMP
 ImageWrapper::GetIntrinsicRatio(nsSize* aSize)
--- a/image/ImageWrapper.h
+++ b/image/ImageWrapper.h
@@ -17,16 +17,17 @@ namespace image {
  */
 class ImageWrapper : public Image
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_IMGICONTAINER
 
   // Inherited methods from Image.
+  nsresult GetNativeSizes(nsTArray<gfx::IntSize>& aNativeSizes) const override;
   virtual already_AddRefed<ProgressTracker> GetProgressTracker() override;
 
   virtual size_t
     SizeOfSourceWithComputedFallback(MallocSizeOf aMallocSizeOf) const override;
   virtual void CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                      MallocSizeOf aMallocSizeOf) const override;
 
   virtual void IncrementAnimationConsumers() override;
--- a/image/OrientedImage.cpp
+++ b/image/OrientedImage.cpp
@@ -41,16 +41,32 @@ OrientedImage::GetHeight(int32_t* aHeigh
 {
   if (mOrientation.SwapsWidthAndHeight()) {
     return InnerImage()->GetWidth(aHeight);
   } else {
     return InnerImage()->GetHeight(aHeight);
   }
 }
 
+nsresult
+OrientedImage::GetNativeSizes(nsTArray<IntSize>& aNativeSizes) const
+{
+  nsresult rv = InnerImage()->GetNativeSizes(aNativeSizes);
+
+  if (mOrientation.SwapsWidthAndHeight()) {
+    auto i = aNativeSizes.Length();
+    while (i > 0) {
+      --i;
+      swap(aNativeSizes[i].width, aNativeSizes[i].height);
+    }
+  }
+
+  return rv;
+}
+
 NS_IMETHODIMP
 OrientedImage::GetIntrinsicSize(nsSize* aSize)
 {
   nsresult rv = InnerImage()->GetIntrinsicSize(aSize);
 
   if (mOrientation.SwapsWidthAndHeight()) {
     swap(aSize->width, aSize->height);
   }
--- a/image/OrientedImage.h
+++ b/image/OrientedImage.h
@@ -25,16 +25,17 @@ class OrientedImage : public ImageWrappe
 {
   typedef gfx::SourceSurface SourceSurface;
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
   NS_IMETHOD GetWidth(int32_t* aWidth) override;
   NS_IMETHOD GetHeight(int32_t* aHeight) override;
+  nsresult GetNativeSizes(nsTArray<gfx::IntSize>& aNativeSizes) const override;
   NS_IMETHOD GetIntrinsicSize(nsSize* aSize) override;
   NS_IMETHOD GetIntrinsicRatio(nsSize* aRatio) override;
   NS_IMETHOD_(already_AddRefed<SourceSurface>)
     GetFrame(uint32_t aWhichFrame, uint32_t aFlags) override;
   NS_IMETHOD_(already_AddRefed<SourceSurface>)
     GetFrameAtSize(const gfx::IntSize& aSize,
                    uint32_t aWhichFrame,
                    uint32_t aFlags) override;
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -216,16 +216,34 @@ RasterImage::GetHeight(int32_t* aHeight)
     return NS_ERROR_FAILURE;
   }
 
   *aHeight = mSize.height;
   return NS_OK;
 }
 
 //******************************************************************************
+nsresult
+RasterImage::GetNativeSizes(nsTArray<IntSize>& aNativeSizes) const
+{
+  if (mError) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (mNativeSizes.IsEmpty()) {
+    aNativeSizes.Clear();
+    aNativeSizes.AppendElement(mSize);
+  } else {
+    aNativeSizes = mNativeSizes;
+  }
+
+  return NS_OK;
+}
+
+//******************************************************************************
 NS_IMETHODIMP
 RasterImage::GetIntrinsicSize(nsSize* aSize)
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   *aSize = nsSize(nsPresContext::CSSPixelsToAppUnits(mSize.width),
@@ -698,16 +716,17 @@ RasterImage::SetMetadata(const ImageMeta
                  "This should not happen!");
       DoError();
       return true;
     }
 
     // Set the size and flag that we have it.
     mSize = size;
     mOrientation = orientation;
+    mNativeSizes = aMetadata.GetNativeSizes();
     mHasSize = true;
   }
 
   if (mHasSize && aMetadata.HasAnimation() && !mAnimationState) {
     // We're becoming animated, so initialize animation stuff.
     mAnimationState.emplace(mAnimationMode);
     mFrameAnimator = MakeUnique<FrameAnimator>(this, mSize);
 
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -155,16 +155,17 @@ public:
   MOZ_DECLARE_WEAKREFERENCE_TYPENAME(RasterImage)
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIPROPERTIES
   NS_DECL_IMGICONTAINER
 #ifdef DEBUG
   NS_DECL_IMGICONTAINERDEBUG
 #endif
 
+  nsresult GetNativeSizes(nsTArray<gfx::IntSize>& aNativeSizes) const override;
   virtual nsresult StartAnimation() override;
   virtual nsresult StopAnimation() override;
 
   // Methods inherited from Image
   virtual void OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) override;
 
   virtual size_t SizeOfSourceWithComputedFallback(MallocSizeOf aMallocSizeOf)
     const override;
@@ -375,16 +376,17 @@ private:
    *
    * RecoverFromInvalidFrames discards all existing frames and redecodes using
    * the provided @aSize and @aFlags.
    */
   void RecoverFromInvalidFrames(const nsIntSize& aSize, uint32_t aFlags);
 
 private: // data
   nsIntSize                  mSize;
+  nsTArray<nsIntSize>        mNativeSizes;
   Orientation                mOrientation;
 
   /// If this has a value, we're waiting for SetSize() to send the load event.
   Maybe<Progress>            mLoadProgress;
 
   nsCOMPtr<nsIProperties>   mProperties;
 
   /// If this image is animated, a FrameAnimator which manages its animation.
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -516,16 +516,23 @@ VectorImage::GetWidth(int32_t* aWidth)
     *aWidth = 0;
     return NS_ERROR_FAILURE;
   }
   *aWidth = rootElemWidth;
   return NS_OK;
 }
 
 //******************************************************************************
+nsresult
+VectorImage::GetNativeSizes(nsTArray<IntSize>& aNativeSizes) const
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+//******************************************************************************
 NS_IMETHODIMP_(void)
 VectorImage::RequestRefresh(const TimeStamp& aTime)
 {
   if (HadRecentRefresh(aTime)) {
     return;
   }
 
   PendingAnimationTracker* tracker =
--- a/image/VectorImage.h
+++ b/image/VectorImage.h
@@ -29,16 +29,17 @@ public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIREQUESTOBSERVER
   NS_DECL_NSISTREAMLISTENER
   NS_DECL_IMGICONTAINER
 
   // (no public constructor - use ImageFactory)
 
   // Methods inherited from Image
+  nsresult GetNativeSizes(nsTArray<gfx::IntSize>& aNativeSizes) const override;
   virtual size_t SizeOfSourceWithComputedFallback(MallocSizeOf aMallocSizeOf)
     const override;
   virtual void CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                      MallocSizeOf aMallocSizeOf) const override;
 
   virtual nsresult OnImageDataAvailable(nsIRequest* aRequest,
                                         nsISupports* aContext,
                                         nsIInputStream* aInStr,
--- a/image/decoders/nsICODecoder.cpp
+++ b/image/decoders/nsICODecoder.cpp
@@ -237,16 +237,18 @@ nsICODecoder::ReadDirEntry(const char* a
     mBiggestResourceColorDepth = e.mBitCount;
     mBiggestResourceHotSpot = IntSize(e.mXHotspot, e.mYHotspot);
 
     if (!desiredSize) {
       mDirEntry = e;
     }
   }
 
+  mImageMetadata.AddNativeSize(entrySize);
+
   if (desiredSize) {
     // Calculate the delta between this resource's size and the desired size, so
     // we can see if it is better than our current-best option.  In the case of
     // several equally-good resources, we use the last one. "Better" in this
     // case is determined by |delta|, a measure of the difference in size
     // between the entry we've found and the desired size. We will choose the
     // smallest resource that is greater than or equal to the desired size (i.e.
     // we assume it's better to downscale a larger icon than to upscale a
--- a/image/imgFrame.h
+++ b/image/imgFrame.h
@@ -6,16 +6,17 @@
 
 #ifndef mozilla_image_imgFrame_h
 #define mozilla_image_imgFrame_h
 
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Monitor.h"
 #include "mozilla/Move.h"
+#include "FrameTimeout.h"
 #include "gfxDrawable.h"
 #include "imgIContainer.h"
 #include "MainThreadUtils.h"
 #include "nsAutoPtr.h"
 
 namespace mozilla {
 namespace image {
 
@@ -42,116 +43,16 @@ enum class DisposalMethod : int8_t {
 };
 
 enum class Opacity : uint8_t {
   FULLY_OPAQUE,
   SOME_TRANSPARENCY
 };
 
 /**
- * FrameTimeout wraps a frame timeout value (measured in milliseconds) after
- * first normalizing it. This normalization is necessary because some tools
- * generate incorrect frame timeout values which we nevertheless have to
- * support. For this reason, code that deals with frame timeouts should always
- * use a FrameTimeout value rather than the raw value from the image header.
- */
-struct FrameTimeout
-{
-  /**
-   * @return a FrameTimeout of zero. This should be used only for math
-   * involving FrameTimeout values. You can't obtain a zero FrameTimeout from
-   * FromRawMilliseconds().
-   */
-  static FrameTimeout Zero() { return FrameTimeout(0); }
-
-  /// @return an infinite FrameTimeout.
-  static FrameTimeout Forever() { return FrameTimeout(-1); }
-
-  /// @return a FrameTimeout obtained by normalizing a raw timeout value.
-  static FrameTimeout FromRawMilliseconds(int32_t aRawMilliseconds)
-  {
-    // Normalize all infinite timeouts to the same value.
-    if (aRawMilliseconds < 0) {
-      return FrameTimeout::Forever();
-    }
-
-    // Very small timeout values are problematic for two reasons: we don't want
-    // to burn energy redrawing animated images extremely fast, and broken tools
-    // generate these values when they actually want a "default" value, so such
-    // images won't play back right without normalization. For some context,
-    // see bug 890743, bug 125137, bug 139677, and bug 207059. The historical
-    // behavior of IE and Opera was:
-    //   IE 6/Win:
-    //     10 - 50ms is normalized to 100ms.
-    //     >50ms is used unnormalized.
-    //   Opera 7 final/Win:
-    //     10ms is normalized to 100ms.
-    //     >10ms is used unnormalized.
-    if (aRawMilliseconds >= 0 && aRawMilliseconds <= 10 ) {
-      return FrameTimeout(100);
-    }
-
-    // The provided timeout value is OK as-is.
-    return FrameTimeout(aRawMilliseconds);
-  }
-
-  bool operator==(const FrameTimeout& aOther) const
-  {
-    return mTimeout == aOther.mTimeout;
-  }
-
-  bool operator!=(const FrameTimeout& aOther) const { return !(*this == aOther); }
-
-  FrameTimeout operator+(const FrameTimeout& aOther)
-  {
-    if (*this == Forever() || aOther == Forever()) {
-      return Forever();
-    }
-
-    return FrameTimeout(mTimeout + aOther.mTimeout);
-  }
-
-  FrameTimeout& operator+=(const FrameTimeout& aOther)
-  {
-    *this = *this + aOther;
-    return *this;
-  }
-
-  /**
-   * @return this FrameTimeout's value in milliseconds. Illegal to call on a
-   * an infinite FrameTimeout value.
-   */
-  uint32_t AsMilliseconds() const
-  {
-    if (*this == Forever()) {
-      MOZ_ASSERT_UNREACHABLE("Calling AsMilliseconds() on an infinite FrameTimeout");
-      return 100;  // Fail to something sane.
-    }
-
-    return uint32_t(mTimeout);
-  }
-
-  /**
-   * @return this FrameTimeout value encoded so that non-negative values
-   * represent a timeout in milliseconds, and -1 represents an infinite
-   * timeout.
-   *
-   * XXX(seth): This is a backwards compatibility hack that should be removed.
-   */
-  int32_t AsEncodedValueDeprecated() const { return mTimeout; }
-
-private:
-  explicit FrameTimeout(int32_t aTimeout)
-    : mTimeout(aTimeout)
-  { }
-
-  int32_t mTimeout;
-};
-
-/**
  * AnimationData contains all of the information necessary for using an imgFrame
  * as part of an animation.
  *
  * It includes pointers to the raw image data of the underlying imgFrame, but
  * does not own that data. A RawAccessFrameRef for the underlying imgFrame must
  * outlive the AnimationData for it to remain valid.
  */
 struct AnimationData
--- a/image/imgIContainer.idl
+++ b/image/imgIContainer.idl
@@ -85,16 +85,20 @@ interface imgIContainer : nsISupports
   readonly attribute int32_t width;
 
   /**
    * The height of the container rectangle.  In the case of any error,
    * zero is returned, and an exception will be thrown.
    */
   readonly attribute int32_t height;
 
+  %{C++
+  virtual nsresult GetNativeSizes(nsTArray<nsIntSize>& aNativeSizes) const = 0;
+  %}
+
   /**
    * The intrinsic size of this image in appunits. If the image has no intrinsic
    * size in a dimension, -1 will be returned for that dimension. In the case of
    * any error, an exception will be thrown.
    */
   [noscript] readonly attribute nsSize intrinsicSize;
 
   /**
--- a/image/moz.build
+++ b/image/moz.build
@@ -32,18 +32,20 @@ XPIDL_SOURCES += [
     'imgITools.idl',
     'nsIIconURI.idl',
 ]
 
 XPIDL_MODULE = 'imglib2'
 
 EXPORTS += [
     'DrawResult.h',
+    'FrameTimeout.h',
     'ImageCacheKey.h',
     'ImageLogging.h',
+    'ImageMetadata.h',
     'ImageOps.h',
     'ImageRegion.h',
     'imgLoader.h',
     'imgRequest.h',
     'imgRequestProxy.h',
     'IProgressObserver.h',
     'Orientation.h',
     'SurfaceCacheUtils.h',
--- a/image/test/gtest/Common.cpp
+++ b/image/test/gtest/Common.cpp
@@ -674,10 +674,16 @@ ImageTestCase DownscaledTransparentICOWi
                        TEST_CASE_IS_TRANSPARENT | TEST_CASE_IGNORE_OUTPUT);
 }
 
 ImageTestCase TruncatedSmallGIFTestCase()
 {
   return ImageTestCase("green-1x1-truncated.gif", "image/gif", IntSize(1, 1));
 }
 
+ImageTestCase GreenMultipleSizesICOTestCase()
+{
+  return ImageTestCase("green-multiple-sizes.ico", "image/x-icon",
+                       IntSize(256, 256));
+}
+
 } // namespace image
 } // namespace mozilla
--- a/image/test/gtest/Common.h
+++ b/image/test/gtest/Common.h
@@ -409,12 +409,14 @@ ImageTestCase DownscaledGIFTestCase();
 ImageTestCase DownscaledJPGTestCase();
 ImageTestCase DownscaledBMPTestCase();
 ImageTestCase DownscaledICOTestCase();
 ImageTestCase DownscaledIconTestCase();
 ImageTestCase DownscaledTransparentICOWithANDMaskTestCase();
 
 ImageTestCase TruncatedSmallGIFTestCase();
 
+ImageTestCase GreenMultipleSizesICOTestCase();
+
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_test_gtest_Common_h
--- a/image/test/gtest/TestDecodeToSurface.cpp
+++ b/image/test/gtest/TestDecodeToSurface.cpp
@@ -22,67 +22,93 @@ using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 
 class DecodeToSurfaceRunnable : public Runnable
 {
 public:
   DecodeToSurfaceRunnable(RefPtr<SourceSurface>& aSurface,
                           nsIInputStream* aInputStream,
+                          ImageOps::ImageBuffer* aImageBuffer,
                           const ImageTestCase& aTestCase)
     : mSurface(aSurface)
     , mInputStream(aInputStream)
+    , mImageBuffer(aImageBuffer)
     , mTestCase(aTestCase)
   { }
 
   NS_IMETHOD Run() override
   {
     Go();
     return NS_OK;
   }
 
   void Go()
   {
-    mSurface =
-      ImageOps::DecodeToSurface(mInputStream,
-                                nsDependentCString(mTestCase.mMimeType),
-                                imgIContainer::DECODE_FLAGS_DEFAULT);
+    Maybe<IntSize> outputSize;
+    if (mTestCase.mOutputSize != mTestCase.mSize) {
+      outputSize.emplace(mTestCase.mOutputSize);
+    }
+
+    if (mImageBuffer) {
+      mSurface =
+        ImageOps::DecodeToSurface(mImageBuffer,
+                                  nsDependentCString(mTestCase.mMimeType),
+                                  imgIContainer::DECODE_FLAGS_DEFAULT,
+                                  outputSize);
+    } else {
+      mSurface =
+        ImageOps::DecodeToSurface(mInputStream,
+                                  nsDependentCString(mTestCase.mMimeType),
+                                  imgIContainer::DECODE_FLAGS_DEFAULT,
+                                  outputSize);
+    }
     ASSERT_TRUE(mSurface != nullptr);
 
     EXPECT_TRUE(mSurface->IsDataSourceSurface());
     EXPECT_TRUE(mSurface->GetFormat() == SurfaceFormat::B8G8R8X8 ||
                 mSurface->GetFormat() == SurfaceFormat::B8G8R8A8);
-    EXPECT_EQ(mTestCase.mSize, mSurface->GetSize());
+
+    if (outputSize) {
+      EXPECT_EQ(*outputSize, mSurface->GetSize());
+    } else {
+      EXPECT_EQ(mTestCase.mSize, mSurface->GetSize());
+    }
 
     EXPECT_TRUE(IsSolidColor(mSurface, BGRAColor::Green(),
                              mTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
   }
 
 private:
   RefPtr<SourceSurface>& mSurface;
   nsCOMPtr<nsIInputStream> mInputStream;
+  RefPtr<ImageOps::ImageBuffer> mImageBuffer;
   ImageTestCase mTestCase;
 };
 
 static void
-RunDecodeToSurface(const ImageTestCase& aTestCase)
+RunDecodeToSurface(const ImageTestCase& aTestCase,
+                   ImageOps::ImageBuffer* aImageBuffer = nullptr)
 {
-  nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
-  ASSERT_TRUE(inputStream != nullptr);
+  nsCOMPtr<nsIInputStream> inputStream;
+  if (!aImageBuffer) {
+    inputStream = LoadFile(aTestCase.mPath);
+    ASSERT_TRUE(inputStream != nullptr);
+  }
 
   nsCOMPtr<nsIThread> thread;
   nsresult rv =
     NS_NewNamedThread("DecodeToSurface", getter_AddRefs(thread), nullptr);
   ASSERT_TRUE(NS_SUCCEEDED(rv));
 
   // We run the DecodeToSurface tests off-main-thread to ensure that
   // DecodeToSurface doesn't require any main-thread-only code.
   RefPtr<SourceSurface> surface;
   nsCOMPtr<nsIRunnable> runnable =
-    new DecodeToSurfaceRunnable(surface, inputStream, aTestCase);
+    new DecodeToSurfaceRunnable(surface, inputStream, aImageBuffer, aTestCase);
   thread->Dispatch(runnable, nsIThread::DISPATCH_SYNC);
 
   thread->Shutdown();
 
   // Explicitly release the SourceSurface on the main thread.
   surface = nullptr;
 }
 
@@ -117,8 +143,48 @@ TEST_F(ImageDecodeToSurface, Corrupt)
   ASSERT_TRUE(inputStream != nullptr);
 
   RefPtr<SourceSurface> surface =
     ImageOps::DecodeToSurface(inputStream,
                               nsDependentCString(testCase.mMimeType),
                               imgIContainer::DECODE_FLAGS_DEFAULT);
   EXPECT_TRUE(surface == nullptr);
 }
+
+TEST_F(ImageDecodeToSurface, ICOMultipleSizes)
+{
+  ImageTestCase testCase = GreenMultipleSizesICOTestCase();
+
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+  ASSERT_TRUE(inputStream != nullptr);
+
+  RefPtr<ImageOps::ImageBuffer> buffer =
+    ImageOps::CreateImageBuffer(inputStream);
+  ASSERT_TRUE(buffer != nullptr);
+
+  ImageMetadata metadata;
+  nsresult rv = ImageOps::DecodeMetadata(buffer,
+                                         nsDependentCString(testCase.mMimeType),
+                                         metadata);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  ASSERT_TRUE(metadata.HasSize());
+  EXPECT_EQ(testCase.mSize, metadata.GetSize());
+
+  const nsTArray<IntSize>& nativeSizes = metadata.GetNativeSizes();
+  ASSERT_EQ(6, nativeSizes.Length());
+
+  IntSize expectedSizes[] = {
+    IntSize(16, 16),
+    IntSize(32, 32),
+    IntSize(64, 64),
+    IntSize(128, 128),
+    IntSize(256, 256),
+    IntSize(256, 128),
+  };
+
+  for (int i = 0; i < 6; ++i) {
+    EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+
+    // Request decoding at native size
+    testCase.mOutputSize = nativeSizes[i];
+    RunDecodeToSurface(testCase, buffer);
+  }
+}
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -667,8 +667,92 @@ TEST_F(ImageDecoders, AnimatedGIFWithExt
   EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
   EXPECT_TRUE(bool(result.Surface()));
 }
 
 TEST_F(ImageDecoders, TruncatedSmallGIFSingleChunk)
 {
   CheckDecoderSingleChunk(TruncatedSmallGIFTestCase());
 }
+
+TEST_F(ImageDecoders, MultipleSizesICOSingleChunk)
+{
+  ImageTestCase testCase = GreenMultipleSizesICOTestCase();
+
+  // Create an image.
+  RefPtr<Image> image =
+    ImageFactory::CreateAnonymousImage(nsDependentCString(testCase.mMimeType));
+  ASSERT_TRUE(!image->HasError());
+
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(testCase.mPath);
+  ASSERT_TRUE(inputStream);
+
+  // Figure out how much data we have.
+  uint64_t length;
+  nsresult rv = inputStream->Available(&length);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  // Write the data into the image.
+  rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+                                   static_cast<uint32_t>(length));
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  // Let the image know we've sent all the data.
+  rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
+  tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
+
+  // Use GetFrame() to force a sync decode of the image.
+  RefPtr<SourceSurface> surface =
+    image->GetFrame(imgIContainer::FRAME_CURRENT,
+                    imgIContainer::FLAG_SYNC_DECODE);
+
+  // Ensure that the image's metadata meets our expectations.
+  IntSize imageSize(0, 0);
+  rv = image->GetWidth(&imageSize.width);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  rv = image->GetHeight(&imageSize.height);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+  EXPECT_EQ(testCase.mSize.width, imageSize.width);
+  EXPECT_EQ(testCase.mSize.height, imageSize.height);
+
+  nsTArray<IntSize> nativeSizes;
+  rv = image->GetNativeSizes(nativeSizes);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  ASSERT_EQ(6, nativeSizes.Length());
+
+  IntSize expectedSizes[] = {
+    IntSize(16, 16),
+    IntSize(32, 32),
+    IntSize(64, 64),
+    IntSize(128, 128),
+    IntSize(256, 256),
+    IntSize(256, 128)
+  };
+
+  for (int i = 0; i < 6; ++i) {
+    EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+  }
+
+  RefPtr<Image> image90 =
+    ImageOps::Orient(image, Orientation(Angle::D90, Flip::Unflipped));
+  rv = image90->GetNativeSizes(nativeSizes);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  ASSERT_EQ(6, nativeSizes.Length());
+
+  for (int i = 0; i < 5; ++i) {
+    EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+  }
+  EXPECT_EQ(IntSize(128, 256), nativeSizes[5]);
+
+  RefPtr<Image> image180 =
+    ImageOps::Orient(image, Orientation(Angle::D180, Flip::Unflipped));
+  rv = image180->GetNativeSizes(nativeSizes);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  ASSERT_EQ(6, nativeSizes.Length());
+
+  for (int i = 0; i < 6; ++i) {
+    EXPECT_EQ(expectedSizes[i], nativeSizes[i]);
+  }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b9463d0c897117204b2edacfa6cbfc4f5f61d771
GIT binary patch
literal 14144
zc%1Fnze>bF5XbS~o)Y9BlIlboV<A}D306^ZYCz6YuCTN^L=Zb$gPreSag}!B1E`=6
z;u~07#@YNK_Xa2W{gBP>WWzE{vBgMEfug8mv(vMrv5h#c;!P*@Y$Qn?&r5q9DbMRT
zlCA@loMK(r{Lr&kvmZ;*lC)u;B7biApDX`Q)aukjg*`=~S@ZeEs&Z`d&8PN%TfVdA
z>y@agQB$v~K<$!x0r32Nk^6<4x-S;meeMeY00000{-3+yU_b1xbZr@C2kA-KIPjf=
zIh)2W+v_$3!^2Tuh3^+!y`CQ1m(?uY8J&NAy}Qt&kIDDZ<>J<)IrA-Cvm<Vn?;m}X
k^#|$G?%A#HOw@<Bor$Q;gw6y20000000000004i_4+vE^wg3PC
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -42,16 +42,17 @@ TEST_HARNESS_FILES.gtest += [
     'downscaled.ico',
     'downscaled.icon',
     'downscaled.jpg',
     'downscaled.png',
     'first-frame-green.gif',
     'first-frame-green.png',
     'first-frame-padding.gif',
     'green-1x1-truncated.gif',
+    'green-multiple-sizes.ico',
     'green.bmp',
     'green.gif',
     'green.ico',
     'green.icon',
     'green.jpg',
     'green.png',
     'invalid-truncated-metadata.bmp',
     'no-frame-delay.gif',