Bug 1680387 - Read and expose EXIF image resolution data. r=tnikkel,aosmond
authorEmilio Cobos Álvarez <emilio@crisal.io>
Wed, 05 May 2021 09:41:23 +0000
changeset 646694 9317cd872430c5ecbbd2e7e0008985588b969916
parent 646693 c09cb1410f568ec1fdeaced2ccd4de837f7fa95f
child 646695 4f74cdfb4fdc403cd75aeafddce3cb400f9c70ec
push id15485
push userffxbld-merge
push dateMon, 31 May 2021 15:17:16 +0000
treeherdermozilla-beta@a3d6c3cd3b32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel, aosmond
bugs1680387
milestone90.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 1680387 - Read and expose EXIF image resolution data. r=tnikkel,aosmond Differential Revision: https://phabricator.services.mozilla.com/D113264
image/Decoder.cpp
image/Decoder.h
image/DynamicImage.cpp
image/ImageMetadata.h
image/ImageWrapper.cpp
image/RasterImage.cpp
image/RasterImage.h
image/Resolution.h
image/VectorImage.cpp
image/decoders/EXIF.cpp
image/decoders/EXIF.h
image/decoders/nsJPEGDecoder.cpp
image/decoders/nsJPEGDecoder.h
image/imgIContainer.idl
image/moz.build
image/test/gtest/Common.cpp
image/test/gtest/Common.h
image/test/gtest/TestDecoders.cpp
image/test/gtest/exif_resolution.jpg
image/test/gtest/moz.build
modules/libpref/init/StaticPrefList.yaml
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -444,23 +444,23 @@ nsresult Decoder::FinishWithErrorInterna
   return NS_OK;
 }
 
 /*
  * Progress Notifications
  */
 
 void Decoder::PostSize(int32_t aWidth, int32_t aHeight,
-                       Orientation aOrientation /* = Orientation()*/) {
+                       Orientation aOrientation, Resolution aResolution) {
   // Validate.
   MOZ_ASSERT(aWidth >= 0, "Width can't be negative!");
   MOZ_ASSERT(aHeight >= 0, "Height can't be negative!");
 
   // Set our intrinsic size.
-  mImageMetadata.SetSize(aWidth, aHeight, aOrientation);
+  mImageMetadata.SetSize(aWidth, aHeight, aOrientation, aResolution);
 
   // Verify it is the expected size, if given. Note that this is only used by
   // the ICO decoder for embedded image types, so only its subdecoders are
   // required to handle failures in PostSize.
   if (!IsExpectedSize()) {
     PostError();
     return;
   }
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -10,16 +10,17 @@
 #include "RasterImage.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/RefPtr.h"
 #include "AnimationParams.h"
 #include "DecoderFlags.h"
 #include "ImageMetadata.h"
 #include "Orientation.h"
+#include "Resolution.h"
 #include "SourceBuffer.h"
 #include "StreamingLexer.h"
 #include "SurfaceFlags.h"
 #include "qcms.h"
 
 enum class CMSMode : int32_t;
 
 namespace mozilla {
@@ -468,18 +469,18 @@ class Decoder {
   }
 
   /*
    * Progress notifications.
    */
 
   // Called by decoders when they determine the size of the image. Informs
   // the image of its size and sends notifications.
-  void PostSize(int32_t aWidth, int32_t aHeight,
-                Orientation aOrientation = Orientation());
+  void PostSize(int32_t aWidth, int32_t aHeight, Orientation = Orientation(),
+                Resolution = Resolution());
 
   // Called by decoders if they determine that the image has transparency.
   //
   // This should be fired as early as possible to allow observers to do things
   // that affect content, so it's necessarily pessimistic - if there's a
   // 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
--- a/image/DynamicImage.cpp
+++ b/image/DynamicImage.cpp
@@ -8,16 +8,17 @@
 #include "gfxPlatform.h"
 #include "gfxUtils.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/Logging.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/SVGImageContext.h"
 #include "ImageRegion.h"
 #include "Orientation.h"
+#include "mozilla/image/Resolution.h"
 
 #include "mozilla/MemoryReporting.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using mozilla::layers::ImageContainer;
 using mozilla::layers::LayerManager;
 
@@ -108,16 +109,19 @@ DynamicImage::GetIntrinsicSize(nsSize* a
 Maybe<AspectRatio> DynamicImage::GetIntrinsicRatio() {
   auto size = mDrawable->Size();
   return Some(AspectRatio::FromSize(size.width, size.height));
 }
 
 NS_IMETHODIMP_(Orientation)
 DynamicImage::GetOrientation() { return Orientation(); }
 
+NS_IMETHODIMP_(Resolution)
+DynamicImage::GetResolution() { return {}; }
+
 NS_IMETHODIMP
 DynamicImage::GetType(uint16_t* aType) {
   *aType = imgIContainer::TYPE_RASTER;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 DynamicImage::GetProducerId(uint32_t* aId) {
--- a/image/ImageMetadata.h
+++ b/image/ImageMetadata.h
@@ -9,29 +9,26 @@
 
 #include <stdint.h>
 #include <utility>
 #include "FrameTimeout.h"
 #include "Orientation.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/gfx/Point.h"
 #include "mozilla/gfx/Rect.h"
+#include "mozilla/image/Resolution.h"
 #include "nsSize.h"
 #include "nsTArray.h"
 
-namespace mozilla {
-namespace image {
+namespace mozilla::image {
 
 // The metadata about an image that decoders accumulate as they decode.
 class ImageMetadata {
  public:
-  ImageMetadata()
-      : mLoopCount(-1),
-        mFirstFrameTimeout(FrameTimeout::Forever()),
-        mHasAnimation(false) {}
+  ImageMetadata() = default;
 
   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 SetLoopCount(int32_t loopcount) { mLoopCount = loopcount; }
@@ -51,59 +48,65 @@ class ImageMetadata {
   }
   gfx::IntRect GetFirstFrameRefreshArea() const {
     return *mFirstFrameRefreshArea;
   }
   bool HasFirstFrameRefreshArea() const {
     return mFirstFrameRefreshArea.isSome();
   }
 
-  void SetSize(int32_t width, int32_t height, Orientation orientation) {
+  void SetSize(int32_t aWidth, int32_t aHeight, Orientation aOrientation,
+               Resolution aResolution) {
     if (!HasSize()) {
-      mSize.emplace(nsIntSize(width, height));
-      mOrientation.emplace(orientation);
+      mSize.emplace(nsIntSize(aWidth, aHeight));
+      mOrientation.emplace(aOrientation);
+      mResolution = aResolution;
     }
   }
   nsIntSize GetSize() const { return *mSize; }
   bool HasSize() const { return mSize.isSome(); }
 
   void AddNativeSize(const nsIntSize& aSize) {
     mNativeSizes.AppendElement(aSize);
   }
 
+  Resolution GetResolution() const { return mResolution; }
+
   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.
   Maybe<gfx::IntPoint> mHotspot;
 
   /// The loop count for animated images, or -1 for infinite loop.
-  int32_t mLoopCount;
+  int32_t mLoopCount = -1;
+
+  /// The resolution of the image in dppx.
+  Resolution mResolution;
 
   // The total length of a single loop through an animated image.
   Maybe<FrameTimeout> mLoopLength;
 
   /// The timeout of an animated image's first frame.
-  FrameTimeout mFirstFrameTimeout;
+  FrameTimeout mFirstFrameTimeout = FrameTimeout::Forever();
 
   // 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.
   CopyableTArray<nsIntSize> mNativeSizes;
 
-  bool mHasAnimation : 1;
+  bool mHasAnimation = false;
 };
 
-}  // namespace image
-}  // namespace mozilla
+}  // namespace mozilla::image
 
 #endif  // mozilla_image_ImageMetadata_h
--- a/image/ImageWrapper.cpp
+++ b/image/ImageWrapper.cpp
@@ -2,23 +2,23 @@
 /* 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 "ImageWrapper.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/RefPtr.h"
 #include "Orientation.h"
+#include "mozilla/image/Resolution.h"
 
 #include "mozilla/MemoryReporting.h"
 
 namespace mozilla {
 
 using dom::Document;
-using gfx::DataSourceSurface;
 using gfx::IntSize;
 using gfx::SamplingFilter;
 using gfx::SourceSurface;
 using layers::ImageContainer;
 using layers::LayerManager;
 
 namespace image {
 
@@ -130,16 +130,19 @@ nsresult ImageWrapper::GetHotspotX(int32
 
 nsresult ImageWrapper::GetHotspotY(int32_t* aY) {
   return Image::GetHotspotY(aY);
 }
 
 NS_IMETHODIMP_(Orientation)
 ImageWrapper::GetOrientation() { return mInnerImage->GetOrientation(); }
 
+NS_IMETHODIMP_(Resolution)
+ImageWrapper::GetResolution() { return mInnerImage->GetResolution(); }
+
 NS_IMETHODIMP
 ImageWrapper::GetType(uint16_t* aType) { return mInnerImage->GetType(aType); }
 
 NS_IMETHODIMP
 ImageWrapper::GetProducerId(uint32_t* aId) {
   return mInnerImage->GetProducerId(aId);
 }
 
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -254,16 +254,19 @@ Maybe<AspectRatio> RasterImage::GetIntri
   }
 
   return Some(AspectRatio::FromSize(mSize.width, mSize.height));
 }
 
 NS_IMETHODIMP_(Orientation)
 RasterImage::GetOrientation() { return mOrientation; }
 
+NS_IMETHODIMP_(Resolution)
+RasterImage::GetResolution() { return mResolution; }
+
 //******************************************************************************
 NS_IMETHODIMP
 RasterImage::GetType(uint16_t* aType) {
   NS_ENSURE_ARG_POINTER(aType);
 
   *aType = imgIContainer::TYPE_RASTER;
   return NS_OK;
 }
@@ -705,16 +708,18 @@ void RasterImage::CollectSizeOfSurfaces(
 bool RasterImage::SetMetadata(const ImageMetadata& aMetadata,
                               bool aFromMetadataDecode) {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mError) {
     return true;
   }
 
+  mResolution = aMetadata.GetResolution();
+
   if (aMetadata.HasSize()) {
     auto metadataSize = UnorientedIntSize::FromUnknownSize(aMetadata.GetSize());
     if (metadataSize.width < 0 || metadataSize.height < 0) {
       NS_WARNING("Image has negative intrinsic size");
       DoError();
       return true;
     }
 
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -32,16 +32,17 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/StaticPrefs_image.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/UniquePtr.h"
+#include "mozilla/image/Resolution.h"
 #include "ImageContainer.h"
 #include "PlaybackType.h"
 #ifdef DEBUG
 #  include "imgIContainerDebug.h"
 #endif
 
 class nsIInputStream;
 class nsIRequest;
@@ -407,16 +408,19 @@ class RasterImage final : public ImageRe
  private:  // data
   OrientedIntSize mSize;
   nsTArray<OrientedIntSize> mNativeSizes;
 
   // The orientation required to correctly orient the image, from the image's
   // metadata. RasterImage will handle and apply this orientation itself.
   Orientation mOrientation;
 
+  // The resolution as specified in the image metadata, in dppx.
+  Resolution mResolution;
+
   /// If this has a value, we're waiting for SetSize() to send the load event.
   Maybe<Progress> mLoadProgress;
 
   // Hotspot of this image, or (0, 0) if there is no hotspot data.
   //
   // We assume (and assert) that no image has both orientation metadata and a
   // hotspot, so we store this as an untyped point.
   gfx::IntPoint mHotspot;
new file mode 100644
--- /dev/null
+++ b/image/Resolution.h
@@ -0,0 +1,74 @@
+/* -*- 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_Resolution_h
+#define mozilla_image_Resolution_h
+
+#include "mozilla/Assertions.h"
+#include <cmath>
+
+namespace mozilla {
+namespace image {
+
+/**
+ * The resolution of an image, in dppx.
+ */
+struct Resolution {
+  Resolution() = default;
+  Resolution(float aX, float aY) : mX(aX), mY(aY) {
+    MOZ_ASSERT(mX != 0.0f);
+    MOZ_ASSERT(mY != 0.0f);
+  }
+
+  bool operator==(const Resolution& aOther) const {
+    return mX == aOther.mX && mY == aOther.mY;
+  }
+  bool operator!=(const Resolution& aOther) const { return !(*this == aOther); }
+
+  float mX = 1.0f;
+  float mY = 1.0f;
+
+  void ApplyXTo(int32_t& aWidth) const {
+    if (mX != 1.0f) {
+      aWidth = std::round(float(aWidth) / mX);
+    }
+  }
+
+  void ApplyXTo(float& aWidth) const {
+    if (mX != 1.0f) {
+      aWidth /= mX;
+    }
+  }
+
+  void ApplyYTo(int32_t& aHeight) const {
+    if (mY != 1.0f) {
+      aHeight = std::round(float(aHeight) / mY);
+    }
+  }
+
+  void ApplyYTo(float& aHeight) const {
+    if (mY != 1.0f) {
+      aHeight /= mY;
+    }
+  }
+
+  void ApplyTo(int32_t& aWidth, int32_t& aHeight) const {
+    ApplyXTo(aWidth);
+    ApplyYTo(aHeight);
+  }
+
+  void ApplyTo(float& aWidth, float& aHeight) const {
+    ApplyXTo(aWidth);
+    ApplyYTo(aHeight);
+  }
+};
+
+}  // namespace image
+
+using ImageResolution = image::Resolution;
+
+}  // namespace mozilla
+
+#endif
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -39,16 +39,17 @@
 #include "Orientation.h"
 #include "SVGDocumentWrapper.h"
 #include "SVGDrawingCallback.h"
 #include "SVGDrawingParameters.h"
 #include "nsIDOMEventListener.h"
 #include "SurfaceCache.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/image/Resolution.h"
 
 namespace mozilla {
 
 using namespace dom;
 using namespace dom::SVGPreserveAspectRatio_Binding;
 using namespace gfx;
 using namespace layers;
 
@@ -598,16 +599,19 @@ Maybe<AspectRatio> VectorImage::GetIntri
   }
 
   return Some(rootFrame->GetIntrinsicRatio());
 }
 
 NS_IMETHODIMP_(Orientation)
 VectorImage::GetOrientation() { return Orientation(); }
 
+NS_IMETHODIMP_(Resolution)
+VectorImage::GetResolution() { return {}; }
+
 //******************************************************************************
 NS_IMETHODIMP
 VectorImage::GetType(uint16_t* aType) {
   NS_ENSURE_ARG_POINTER(aType);
 
   *aType = imgIContainer::TYPE_VECTOR;
   return NS_OK;
 }
--- a/image/decoders/EXIF.cpp
+++ b/image/decoders/EXIF.cpp
@@ -1,44 +1,67 @@
 /* -*- 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 "EXIF.h"
 
 #include "mozilla/EndianUtils.h"
+#include "mozilla/StaticPrefs_image.h"
 
-namespace mozilla {
-namespace image {
+namespace mozilla::image {
 
 // Section references in this file refer to the EXIF v2.3 standard, also known
 // as CIPA DC-008-Translation-2010.
 
 // See Section 4.6.4, Table 4.
 // Typesafe enums are intentionally not used here since we're comparing to raw
 // integers produced by parsing.
-enum EXIFTag {
-  OrientationTag = 0x112,
+enum class EXIFTag : uint16_t {
+  Orientation = 0x112,
+  XResolution = 0x11a,
+  YResolution = 0x11b,
+  ResolutionUnit = 0x128,
 };
 
 // See Section 4.6.2.
 enum EXIFType {
   ByteType = 1,
   ASCIIType = 2,
   ShortType = 3,
   LongType = 4,
   RationalType = 5,
   UndefinedType = 7,
   SignedLongType = 9,
   SignedRational = 10,
 };
 
 static const char* EXIFHeader = "Exif\0\0";
 static const uint32_t EXIFHeaderLength = 6;
+static const uint32_t TIFFHeaderStart = EXIFHeaderLength;
+
+struct ParsedEXIFData {
+  Orientation orientation;
+  float resolutionX = 72.0f;
+  float resolutionY = 72.0f;
+  ResolutionUnit resolutionUnit = ResolutionUnit::Dpi;
+};
+
+static float ToDppx(float aResolution, ResolutionUnit aUnit) {
+  constexpr float kPointsPerInch = 72.0f;
+  constexpr float kPointsPerCm = 1.0f / 2.54f;
+  switch (aUnit) {
+    case ResolutionUnit::Dpi:
+      return aResolution / kPointsPerInch;
+    case ResolutionUnit::Dpcm:
+      return aResolution / kPointsPerCm;
+  }
+  MOZ_CRASH("Unknown resolution unit?");
+}
 
 /////////////////////////////////////////////////////////////
 // Parse EXIF data, typically found in a JPEG's APP1 segment.
 /////////////////////////////////////////////////////////////
 EXIFData EXIFParser::ParseEXIF(const uint8_t* aData, const uint32_t aLength) {
   if (!Initialize(aData, aLength)) {
     return EXIFData();
   }
@@ -49,24 +72,23 @@ EXIFData EXIFParser::ParseEXIF(const uin
 
   uint32_t offsetIFD;
   if (!ParseTIFFHeader(offsetIFD)) {
     return EXIFData();
   }
 
   JumpTo(offsetIFD);
 
-  Orientation orientation;
-  if (!ParseIFD0(orientation)) {
-    return EXIFData();
-  }
-
-  // We only care about orientation at this point, so we don't bother with the
-  // other IFDs. If we got this far we're done.
-  return EXIFData(orientation);
+  // We only care about IFD0 at this point, so we don't bother with the other
+  // IFDs. If we got this far we're done.
+  ParsedEXIFData data;
+  ParseIFD0(data);
+  return EXIFData{data.orientation,
+                  Resolution(ToDppx(data.resolutionX, data.resolutionUnit),
+                             ToDppx(data.resolutionY, data.resolutionUnit))};
 }
 
 /////////////////////////////////////////////////////////
 // Parse the EXIF header. (Section 4.7.2, Figure 30)
 /////////////////////////////////////////////////////////
 bool EXIFParser::ParseEXIFHeader() {
   return MatchString(EXIFHeader, EXIFHeaderLength);
 }
@@ -89,65 +111,144 @@ bool EXIFParser::ParseTIFFHeader(uint32_
   uint32_t ifd0Offset;
   if (!ReadUInt32(ifd0Offset) || ifd0Offset > 64 * 1024) {
     return false;
   }
 
   // The IFD offset is relative to the beginning of the TIFF header, which
   // begins after the EXIF header, so we need to increase the offset
   // appropriately.
-  aIFD0OffsetOut = ifd0Offset + EXIFHeaderLength;
+  aIFD0OffsetOut = ifd0Offset + TIFFHeaderStart;
   return true;
 }
 
 /////////////////////////////////////////////////////////
 // Parse the entries in IFD0. (Section 4.6.2)
 /////////////////////////////////////////////////////////
-bool EXIFParser::ParseIFD0(Orientation& aOrientationOut) {
+void EXIFParser::ParseIFD0(ParsedEXIFData& aData) {
   uint16_t entryCount;
   if (!ReadUInt16(entryCount)) {
-    return false;
+    return;
   }
 
   for (uint16_t entry = 0; entry < entryCount; ++entry) {
-    // Read the fields of the entry.
+    // Read the fields of the 12-byte entry.
     uint16_t tag;
     if (!ReadUInt16(tag)) {
-      return false;
-    }
-
-    // Right now, we only care about orientation, so we immediately skip to the
-    // next entry if we find anything else.
-    if (tag != OrientationTag) {
-      Advance(10);
-      continue;
+      return;
     }
 
     uint16_t type;
     if (!ReadUInt16(type)) {
-      return false;
+      return;
     }
 
     uint32_t count;
     if (!ReadUInt32(count)) {
-      return false;
-    }
-
-    // We should have an orientation value here; go ahead and parse it.
-    if (!ParseOrientation(type, count, aOrientationOut)) {
-      return false;
+      return;
     }
 
-    // Since the orientation is all we care about, we're done.
+    switch (EXIFTag(tag)) {
+      case EXIFTag::Orientation:
+        // We should have an orientation value here; go ahead and parse it.
+        if (!ParseOrientation(type, count, aData.orientation)) {
+          return;
+        }
+        break;
+      case EXIFTag::ResolutionUnit:
+        if (!ParseResolutionUnit(type, count, aData.resolutionUnit)) {
+          return;
+        }
+        break;
+      case EXIFTag::XResolution:
+        if (!ParseResolution(type, count, aData.resolutionX)) {
+          return;
+        }
+        break;
+      case EXIFTag::YResolution:
+        if (!ParseResolution(type, count, aData.resolutionY)) {
+          return;
+        }
+        break;
+      default:
+        Advance(4);
+        break;
+    }
+  }
+}
+
+bool EXIFParser::ReadRational(float& aOut) {
+  // Values larger than 4 bytes (like rationals) are specified as an offset into
+  // the TIFF header.
+  uint32_t valueOffset;
+  if (!ReadUInt32(valueOffset)) {
+    return false;
+  }
+  ScopedJump jumpToHeader(*this, valueOffset + TIFFHeaderStart);
+  uint32_t numerator;
+  if (!ReadUInt32(numerator)) {
+    return false;
+  }
+  uint32_t denominator;
+  if (!ReadUInt32(denominator)) {
+    return false;
+  }
+  if (denominator == 0) {
+    return false;
+  }
+  aOut = float(numerator) / float(denominator);
+  return true;
+}
+
+bool EXIFParser::ParseResolution(uint16_t aType, uint32_t aCount, float& aOut) {
+  if (!StaticPrefs::image_exif_density_correction_enabled()) {
+    Advance(4);
     return true;
   }
+  if (aType != RationalType || aCount != 1) {
+    return false;
+  }
+  float value;
+  if (!ReadRational(value)) {
+    return false;
+  }
+  if (value == 0.0f) {
+    return false;
+  }
+  aOut = value;
+  return true;
+}
+
+bool EXIFParser::ParseResolutionUnit(uint16_t aType, uint32_t aCount,
+                                     ResolutionUnit& aOut) {
+  if (!StaticPrefs::image_exif_density_correction_enabled()) {
+    Advance(4);
+    return true;
+  }
+  if (aType != ShortType || aCount != 1) {
+    return false;
+  }
+  uint16_t value;
+  if (!ReadUInt16(value)) {
+    return false;
+  }
+  switch (value) {
+    case 2:
+      aOut = ResolutionUnit::Dpi;
+      break;
+    case 3:
+      aOut = ResolutionUnit::Dpcm;
+      break;
+    default:
+      return false;
+  }
 
-  // We didn't find an orientation field in the IFD. That's OK; we assume the
-  // default orientation in that case.
-  aOrientationOut = Orientation();
+  // This is a 32-bit field, but the unit value only occupies the first 16 bits.
+  // We need to advance another 16 bits to consume the entire field.
+  Advance(2);
   return true;
 }
 
 bool EXIFParser::ParseOrientation(uint16_t aType, uint32_t aCount,
                                   Orientation& aOut) {
   // Sanity check the type and count.
   if (aType != ShortType || aCount != 1) {
     return false;
@@ -314,10 +415,9 @@ bool EXIFParser::ReadUInt32(uint32_t& aV
 
   if (matched) {
     Advance(4);
   }
 
   return matched;
 }
 
-}  // namespace image
-}  // namespace mozilla
+}  // namespace mozilla::image
--- a/image/decoders/EXIF.h
+++ b/image/decoders/EXIF.h
@@ -5,27 +5,32 @@
 
 #ifndef mozilla_image_decoders_EXIF_h
 #define mozilla_image_decoders_EXIF_h
 
 #include <stdint.h>
 #include "nsDebug.h"
 
 #include "Orientation.h"
+#include "mozilla/image/Resolution.h"
 
-namespace mozilla {
-namespace image {
+namespace mozilla::image {
 
 enum class ByteOrder : uint8_t { Unknown, LittleEndian, BigEndian };
 
 struct EXIFData {
-  EXIFData() {}
-  explicit EXIFData(Orientation aOrientation) : orientation(aOrientation) {}
+  const Orientation orientation = Orientation();
+  const Resolution resolution = Resolution();
+};
 
-  const Orientation orientation;
+struct ParsedEXIFData;
+
+enum class ResolutionUnit : uint8_t {
+  Dpi,
+  Dpcm,
 };
 
 class EXIFParser {
  public:
   static EXIFData Parse(const uint8_t* aData, const uint32_t aLength) {
     EXIFParser parser;
     return parser.ParseEXIF(aData, aLength);
   }
@@ -36,31 +41,49 @@ class EXIFParser {
         mCurrent(nullptr),
         mLength(0),
         mRemainingLength(0),
         mByteOrder(ByteOrder::Unknown) {}
 
   EXIFData ParseEXIF(const uint8_t* aData, const uint32_t aLength);
   bool ParseEXIFHeader();
   bool ParseTIFFHeader(uint32_t& aIFD0OffsetOut);
-  bool ParseIFD0(Orientation& aOrientationOut);
-  bool ParseOrientation(uint16_t aType, uint32_t aCount, Orientation& aOut);
+
+  void ParseIFD0(ParsedEXIFData&);
+  bool ParseOrientation(uint16_t aType, uint32_t aCount, Orientation&);
+  bool ParseResolution(uint16_t aType, uint32_t aCount, float&);
+  bool ParseResolutionUnit(uint16_t aType, uint32_t aCount, ResolutionUnit&);
 
   bool Initialize(const uint8_t* aData, const uint32_t aLength);
   void Advance(const uint32_t aDistance);
   void JumpTo(const uint32_t aOffset);
 
+  uint32_t CurrentOffset() const { return mCurrent - mStart; }
+
+  class ScopedJump {
+    EXIFParser& mParser;
+    uint32_t mOldOffset;
+
+   public:
+    ScopedJump(EXIFParser& aParser, uint32_t aOffset)
+        : mParser(aParser), mOldOffset(aParser.CurrentOffset()) {
+      mParser.JumpTo(aOffset);
+    }
+
+    ~ScopedJump() { mParser.JumpTo(mOldOffset); }
+  };
+
   bool MatchString(const char* aString, const uint32_t aLength);
   bool MatchUInt16(const uint16_t aValue);
   bool ReadUInt16(uint16_t& aOut);
   bool ReadUInt32(uint32_t& aOut);
+  bool ReadRational(float& aOut);
 
   const uint8_t* mStart;
   const uint8_t* mCurrent;
   uint32_t mLength;
   uint32_t mRemainingLength;
   ByteOrder mByteOrder;
 };
 
-}  // namespace image
-}  // namespace mozilla
+}  // namespace mozilla::image
 
 #endif  // mozilla_image_decoders_EXIF_h
--- a/image/decoders/nsJPEGDecoder.cpp
+++ b/image/decoders/nsJPEGDecoder.cpp
@@ -231,18 +231,19 @@ LexerTransition<nsJPEGDecoder::State> ns
       if (jpeg_read_header(&mInfo, TRUE) == JPEG_SUSPENDED) {
         MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
                 ("} (JPEG_SUSPENDED)"));
         return Transition::ContinueUnbuffered(
             State::JPEG_DATA);  // I/O suspension
       }
 
       // Post our size to the superclass
-      PostSize(mInfo.image_width, mInfo.image_height,
-               ReadOrientationFromEXIF());
+      EXIFData exif = ReadExifData();
+      PostSize(mInfo.image_width, mInfo.image_height, exif.orientation,
+               exif.resolution);
       if (HasError()) {
         // Setting the size led to an error.
         mState = JPEG_ERROR;
         return Transition::TerminateFailure();
       }
 
       // If we're doing a metadata decode, we're done.
       if (IsMetadataDecode()) {
@@ -596,35 +597,33 @@ LexerTransition<nsJPEGDecoder::State> ns
 
 LexerTransition<nsJPEGDecoder::State> nsJPEGDecoder::FinishedJPEGData() {
   // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read
   // all that data something is really wrong.
   MOZ_ASSERT_UNREACHABLE("Read the entire address space?");
   return Transition::TerminateFailure();
 }
 
-Orientation nsJPEGDecoder::ReadOrientationFromEXIF() {
+EXIFData nsJPEGDecoder::ReadExifData() const {
   jpeg_saved_marker_ptr marker;
 
   // Locate the APP1 marker, where EXIF data is stored, in the marker list.
   for (marker = mInfo.marker_list; marker != nullptr; marker = marker->next) {
     if (marker->marker == JPEG_APP0 + 1) {
       break;
     }
   }
 
   // If we're at the end of the list, there's no EXIF data.
   if (!marker) {
-    return Orientation();
+    return EXIFData();
   }
 
-  // Extract the orientation information.
-  EXIFData exif = EXIFParser::Parse(marker->data,
-                                    static_cast<uint32_t>(marker->data_length));
-  return exif.orientation;
+  return EXIFParser::Parse(marker->data,
+                           static_cast<uint32_t>(marker->data_length));
 }
 
 void nsJPEGDecoder::NotifyDone() {
   PostFrameStop(Opacity::FULLY_OPAQUE);
   PostDecodeDone();
 }
 
 WriteState nsJPEGDecoder::OutputScanlines() {
--- a/image/decoders/nsJPEGDecoder.h
+++ b/image/decoders/nsJPEGDecoder.h
@@ -4,32 +4,32 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_image_decoders_nsJPEGDecoder_h
 #define mozilla_image_decoders_nsJPEGDecoder_h
 
 #include "RasterImage.h"
 #include "SurfacePipe.h"
+#include "EXIF.h"
 
 // On Windows systems, RasterImage.h brings in 'windows.h', which defines INT32.
 // But the jpeg decoder has its own definition of INT32. To avoid build issues,
 // we need to undefine the version from 'windows.h'.
 #undef INT32
 
 #include "Decoder.h"
 
 extern "C" {
 #include "jpeglib.h"
 }
 
 #include <setjmp.h>
 
-namespace mozilla {
-namespace image {
+namespace mozilla::image {
 
 typedef struct {
   struct jpeg_error_mgr pub;  // "public" fields for IJG library
   jmp_buf setjmp_buffer;      // For handling catastropic errors
 } decoder_error_mgr;
 
 typedef enum {
   JPEG_HEADER,  // Reading JFIF headers
@@ -57,17 +57,17 @@ class nsJPEGDecoder : public Decoder {
   nsresult InitInternal() override;
   LexerResult DoDecode(SourceBufferIterator& aIterator,
                        IResumable* aOnResume) override;
   nsresult FinishInternal() override;
 
   Maybe<Telemetry::HistogramID> SpeedHistogram() const override;
 
  protected:
-  Orientation ReadOrientationFromEXIF();
+  EXIFData ReadExifData() const;
   WriteState OutputScanlines();
 
  private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   nsJPEGDecoder(RasterImage* aImage, Decoder::DecodeStyle aDecodeStyle);
 
@@ -102,12 +102,11 @@ class nsJPEGDecoder : public Decoder {
 
   bool mReading;
 
   const Decoder::DecodeStyle mDecodeStyle;
 
   SurfacePipe mPipe;
 };
 
-}  // namespace image
-}  // namespace mozilla
+}  // namespace mozilla::image
 
 #endif  // mozilla_image_decoders_nsJPEGDecoder_h
--- a/image/imgIContainer.idl
+++ b/image/imgIContainer.idl
@@ -43,16 +43,17 @@ class SVGImageContext;
 struct MediaFeatureChange;
 }
 
 namespace mozilla {
 namespace image {
 
 class ImageRegion;
 struct Orientation;
+struct Resolution;
 
 }
 }
 
 %}
 
 native MaybeAspectRatio(mozilla::Maybe<mozilla::AspectRatio>);
 native ImgDrawResult(mozilla::image::ImgDrawResult);
@@ -66,16 +67,17 @@ native nsIntRectByVal(nsIntRect);
 [ref] native nsIntSize(nsIntSize);
 native nsSize(nsSize);
 [ptr] native nsIFrame(nsIFrame);
 native TempRefImageContainer(already_AddRefed<mozilla::layers::ImageContainer>);
 [ptr] native ImageContainer(mozilla::layers::ImageContainer);
 [ref] native ImageRegion(mozilla::image::ImageRegion);
 [ptr] native LayerManager(mozilla::layers::LayerManager);
 native Orientation(mozilla::image::Orientation);
+native ImageResolution(mozilla::image::Resolution);
 [ref] native TimeStamp(mozilla::TimeStamp);
 [ref] native MaybeSVGImageContext(mozilla::Maybe<mozilla::SVGImageContext>);
 native TempRefSourceSurface(already_AddRefed<mozilla::gfx::SourceSurface>);
 native TempRefImgIContainer(already_AddRefed<imgIContainer>);
 native nsIntSizeByVal(nsIntSize);
 
 
 /**
@@ -635,16 +637,22 @@ interface imgIContainer : nsISupports
 
   /*
    * Returns the inherent orientation of the image, as described in the image's
    * metadata (e.g. EXIF).
    */
   [notxpcom] Orientation getOrientation();
 
   /*
+   * Returns the intrinsic resolution of the image, or 1.0 if the image doesn't
+   * declare any.
+   */
+  [notxpcom] ImageResolution getResolution();
+
+  /*
    * Returns the delay, in ms, between the first and second frame. If this
    * returns 0, there is no delay between first and second frame (i.e., this
    * image could render differently whenever it draws).
    *
    * If this image is not animated, or not known to be animated (see attribute
    * animated), returns -1.
    */
   [notxpcom] int32_t getFirstFrameDelay();
--- a/image/moz.build
+++ b/image/moz.build
@@ -56,16 +56,17 @@ EXPORTS += [
 
 EXPORTS.mozilla.image += [
     "encoders/bmp/nsBMPEncoder.h",
     "encoders/ico/nsICOEncoder.h",
     "encoders/jpeg/nsJPEGEncoder.h",
     "encoders/png/nsPNGEncoder.h",
     "ICOFileHeaders.h",
     "ImageMemoryReporter.h",
+    "Resolution.h",
 ]
 
 UNIFIED_SOURCES += [
     "AnimationFrameBuffer.cpp",
     "AnimationSurfaceProvider.cpp",
     "ClippedImage.cpp",
     "DecodedSurfaceProvider.cpp",
     "Decoder.cpp",
--- a/image/test/gtest/Common.cpp
+++ b/image/test/gtest/Common.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Common.h"
 
 #include <cstdlib>
 
 #include "gfxPlatform.h"
 
+#include "ImageFactory.h"
 #include "imgITools.h"
 #include "mozilla/Preferences.h"
 #include "nsComponentManagerUtils.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsIFile.h"
 #include "nsIInputStream.h"
 #include "nsIProperties.h"
 #include "nsNetUtil.h"
@@ -826,10 +827,46 @@ ImageTestCase PerfRgbAlphaLossyWebPTestC
   return ImageTestCase("perf_srgb_alpha_lossy.webp", "image/webp",
                        IntSize(1000, 1000), TEST_CASE_IS_TRANSPARENT);
 }
 
 ImageTestCase PerfRgbGIFTestCase() {
   return ImageTestCase("perf_srgb.gif", "image/gif", IntSize(1000, 1000));
 }
 
+ImageTestCase ExifResolutionTestCase() {
+  return ImageTestCase("exif_resolution.jpg", "image/jpeg", IntSize(100, 50));
+}
+
+RefPtr<Image> TestCaseToDecodedImage(const ImageTestCase& aTestCase) {
+  RefPtr<Image> image = ImageFactory::CreateAnonymousImage(
+      nsDependentCString(aTestCase.mMimeType));
+  MOZ_RELEASE_ASSERT(!image->HasError());
+
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+  MOZ_RELEASE_ASSERT(inputStream);
+
+  // Figure out how much data we have.
+  uint64_t length;
+  nsresult rv = inputStream->Available(&length);
+  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+  // Write the data into the image.
+  rv = image->OnImageDataAvailable(nullptr, nullptr, inputStream, 0,
+                                   static_cast<uint32_t>(length));
+  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+  // Let the image know we've sent all the data.
+  rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
+  MOZ_RELEASE_ASSERT(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);
+  Unused << surface;
+  return image;
+}
+
 }  // namespace image
 }  // namespace mozilla
--- a/image/test/gtest/Common.h
+++ b/image/test/gtest/Common.h
@@ -547,12 +547,17 @@ ImageTestCase PerfRgbLossyWebPTestCase()
 ImageTestCase PerfRgbAlphaLossyWebPTestCase();
 ImageTestCase PerfRgbGIFTestCase();
 
 ImageTestCase CorruptAVIFTestCase();
 ImageTestCase DownscaledAVIFTestCase();
 ImageTestCase LargeAVIFTestCase();
 ImageTestCase MultiLayerAVIFTestCase();
 ImageTestCase TransparentAVIFTestCase();
+
+ImageTestCase ExifResolutionTestCase();
+
+RefPtr<Image> TestCaseToDecodedImage(const ImageTestCase&);
+
 }  // namespace image
 }  // namespace mozilla
 
 #endif  // mozilla_image_test_gtest_Common_h
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -861,54 +861,29 @@ TEST_F(ImageDecoders, AnimatedGIFWithFRA
 TEST_F(ImageDecoders, AnimatedGIFWithExtraImageSubBlocks) {
   ImageTestCase testCase = ExtraImageSubBlocksAnimatedGIFTestCase();
 
   // Verify that we can decode this test case and get two frames, even though
   // there are extra image sub blocks between the first and second frame. The
   // extra data shouldn't confuse the decoder or cause the decode to fail.
 
   // 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);
+  RefPtr<Image> image = TestCaseToDecodedImage(testCase);
 
   // Ensure that the image's metadata meets our expectations.
   IntSize imageSize(0, 0);
-  rv = image->GetWidth(&imageSize.width);
+  nsresult 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);
 
+  RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
   Progress imageProgress = tracker->GetProgress();
 
   EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
   EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
 
   // Ensure that we decoded both frames of the image.
   LookupResult result =
       SurfaceCache::Lookup(ImageKey(image.get()),
@@ -1023,8 +998,13 @@ TEST_F(ImageDecoders, MultipleSizesICOSi
   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]);
   }
 }
+
+TEST_F(ImageDecoders, ExifResolutionEven) {
+  RefPtr<Image> image = TestCaseToDecodedImage(ExifResolutionTestCase());
+  EXPECT_EQ(image->GetResolution(), Resolution(2.0, 2.0));
+}
new file mode 100644
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -57,16 +57,17 @@ TEST_HARNESS_FILES.gtest += [
     "downscaled.avif",
     "downscaled.bmp",
     "downscaled.gif",
     "downscaled.ico",
     "downscaled.icon",
     "downscaled.jpg",
     "downscaled.png",
     "downscaled.webp",
+    "exif_resolution.jpg",
     "first-frame-green.gif",
     "first-frame-green.png",
     "first-frame-green.webp",
     "first-frame-padding.gif",
     "green-1x1-truncated.gif",
     "green-large-bmp.ico",
     "green-large-png.ico",
     "green-multiple-sizes.ico",
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -5302,16 +5302,27 @@
   mirror: always
 
 # Whether we attempt to downscale images during decoding.
 - name: image.downscale-during-decode.enabled
   type: RelaxedAtomicBool
   value: true
   mirror: always
 
+# Whether we use EXIF metadata for image density.
+#
+# NOTE: Before shipping this, make sure that the issue described in the
+# following comment is addressed:
+#
+#   https://github.com/whatwg/html/pull/5574#issuecomment-826335244
+- name: image.exif-density-correction.enabled
+  type: RelaxedAtomicBool
+  value: @IS_NIGHTLY_BUILD@
+  mirror: always
+
 # The threshold for inferring that changes to an <img> element's |src|
 # attribute by JavaScript represent an animation, in milliseconds. If the |src|
 # attribute is changing more frequently than this value, then we enter a
 # special "animation mode" which is designed to eliminate flicker. Set to 0 to
 # disable.
 - name: image.infer-src-animation.threshold-ms
   type: RelaxedAtomicUint32
   value: 2000