Bug 1343499 - Expose native image sizes to imagelib users. r=tnikkel
☠☠ backed out by 2bd7b9296a2e ☠ ☠
authorAndrew Osmond <aosmond@mozilla.com>
Wed, 22 Mar 2017 09:05:36 -0400
changeset 348963 619b5b27ce873a7966c1198a7bf2393198653585
parent 348962 f0f8950030a5b600e29df14da687ef35b70a5ce5
child 348964 b0b8784e62afafb507c105fab8fe2eed5930a01b
push id31540
push userkwierso@gmail.com
push dateThu, 23 Mar 2017 00:10:08 +0000
treeherdermozilla-central@7513b3f42058 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1343499
milestone55.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 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(6u, 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(6u, 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(6u, 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(6u, 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',