Decode and encode ISO 21496-1 JPEG images

Add SkJpegMetadataDecoder::getISOGainmapMetadata, to extract the
ISO 21496-1 metadata blob from a JPEG file.

Update SkJpegMetadataDecoderImpl::findGainmapImage to search for
ISO 21496-1 metadata in the base JPEG image.

Update the extract_gainmap function to also search for
ISO 21496-1 metadata. If that metadata is found, and if it indicates
not to use the base image color space for applying the gainmap,
then parse the gainmap image's ICC profile.

Update lots of tests.

Bug: b/338342146
Change-Id: I998316b9242b5fc2fa59219855b73946582aa2ac
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/854696
Reviewed-by: Brian Osman <brianosman@google.com>
Reviewed-by: Christopher Cameron <ccameron@google.com>
Commit-Queue: Christopher Cameron <ccameron@google.com>
diff --git a/include/private/SkGainmapInfo.h b/include/private/SkGainmapInfo.h
index 07512a4..033c933 100644
--- a/include/private/SkGainmapInfo.h
+++ b/include/private/SkGainmapInfo.h
@@ -95,6 +95,11 @@
     sk_sp<SkColorSpace> fGainmapMathColorSpace = nullptr;
 
     /**
+     * Return true if this can be encoded as an UltraHDR v1 image.
+     */
+    bool isUltraHDRv1Compatible() const;
+
+    /**
      * If |data| contains an ISO 21496-1 version that is supported, return true. Otherwise return
      * false.
      */
diff --git a/include/private/SkJpegMetadataDecoder.h b/include/private/SkJpegMetadataDecoder.h
index f7c5dbd..c9e0b34 100644
--- a/include/private/SkJpegMetadataDecoder.h
+++ b/include/private/SkJpegMetadataDecoder.h
@@ -62,6 +62,13 @@
     virtual sk_sp<SkData> getICCProfileData(bool copyData) const = 0;
 
     /**
+     * Return the ISO 21496-1 metadata, if any, and nullptr otherwise. If |copyData| is false,
+     * then the returned SkData may directly reference the data provided when this object was
+     * created.
+     */
+    virtual sk_sp<SkData> getISOGainmapMetadata(bool copyData) const = 0;
+
+    /**
      * Return true if there is a possibility that this image contains a gainmap image.
      */
     virtual bool mightHaveGainmapImage() const = 0;
diff --git a/resources/images/gainmap_gcontainer_only.jpg b/resources/images/gainmap_gcontainer_only.jpg
new file mode 100644
index 0000000..4a869b6
--- /dev/null
+++ b/resources/images/gainmap_gcontainer_only.jpg
Binary files differ
diff --git a/resources/images/gainmap_iso21496_1.jpg b/resources/images/gainmap_iso21496_1.jpg
new file mode 100644
index 0000000..1fd0643
--- /dev/null
+++ b/resources/images/gainmap_iso21496_1.jpg
Binary files differ
diff --git a/resources/images/gainmap_iso21496_1_adobe_gcontainer.jpg b/resources/images/gainmap_iso21496_1_adobe_gcontainer.jpg
new file mode 100644
index 0000000..061e2e5
--- /dev/null
+++ b/resources/images/gainmap_iso21496_1_adobe_gcontainer.jpg
Binary files differ
diff --git a/src/codec/SkGainmapInfo.cpp b/src/codec/SkGainmapInfo.cpp
index 54c2568..74c2090 100644
--- a/src/codec/SkGainmapInfo.cpp
+++ b/src/codec/SkGainmapInfo.cpp
@@ -219,6 +219,19 @@
     return true;
 }
 
+bool SkGainmapInfo::isUltraHDRv1Compatible() const {
+    // UltraHDR v1 supports having the base image be HDR in theory, but it is largely
+    // untested.
+    if (fBaseImageType == BaseImageType::kHDR) {
+        return false;
+    }
+    // UltraHDR v1 doesn't support a non-base gainmap math color space.
+    if (fGainmapMathColorSpace) {
+        return false;
+    }
+    return true;
+}
+
 bool SkGainmapInfo::ParseVersion(const SkData* data) {
     if (!data) {
         return false;
diff --git a/src/codec/SkJpegConstants.h b/src/codec/SkJpegConstants.h
index eeb3276..f2be02f 100644
--- a/src/codec/SkJpegConstants.h
+++ b/src/codec/SkJpegConstants.h
@@ -59,4 +59,10 @@
 static constexpr uint32_t kMpfMarker = kJpegMarkerAPP0 + 2;
 static constexpr uint8_t kMpfSig[] = {'M', 'P', 'F', '\0'};
 
+// ISO 21496-1 marker and signature.
+static constexpr uint32_t kISOGainmapMarker = kJpegMarkerAPP0 + 2;
+static constexpr uint8_t kISOGainmapSig[] = {'u', 'r', 'n', ':', 'i', 's', 'o', ':', 's', 't',
+                                             'd', ':', 'i', 's', 'o', ':', 't', 's', ':', '2',
+                                             '1', '4', '9', '6', ':', '-', '1', '\0'};
+
 #endif
diff --git a/src/codec/SkJpegMetadataDecoderImpl.cpp b/src/codec/SkJpegMetadataDecoderImpl.cpp
index 8d571ff..2a49fce 100644
--- a/src/codec/SkJpegMetadataDecoderImpl.cpp
+++ b/src/codec/SkJpegMetadataDecoderImpl.cpp
@@ -22,6 +22,7 @@
 #include "include/private/SkExif.h"
 #include "include/private/SkGainmapInfo.h"
 #include "include/private/SkXmp.h"
+#include "src/base/SkEndian.h"
 #include "src/codec/SkJpegMultiPicture.h"
 #include "src/codec/SkJpegSegmentScan.h"
 #include "src/codec/SkJpegSourceMgr.h"
@@ -95,6 +96,7 @@
 static bool extract_gainmap(SkJpegSourceMgr* decoderSource,
                             size_t offset,
                             size_t size,
+                            bool baseImageHasIsoVersion,
                             bool baseImageHasAdobeXmp,
                             bool baseImageHasAppleExif,
                             float baseImageAppleHdrHeadroom,
@@ -110,22 +112,44 @@
 
     // Parse the potential gainmap image's metadata.
     SkJpegMetadataDecoderImpl metadataDecoder(imageData);
-    auto xmp = metadataDecoder.getXmpMetadata();
-    if (!xmp) {
-        return false;
-    }
 
-    // Check if this image identifies itself as a gainmap.
+    // If this image identifies itself as a gainmap, then populate |info|.
     bool didPopulateInfo = false;
     SkGainmapInfo info;
 
-    // Check for Adobe gainmap metadata only if the base image specified hdrgm:Version="1.0".
-    didPopulateInfo = baseImageHasAdobeXmp && xmp->getGainmapInfoAdobe(&info);
+    // Check for ISO 21496-1 gain map metadata.
+    if (baseImageHasIsoVersion) {
+        didPopulateInfo = SkGainmapInfo::Parse(
+                metadataDecoder.getISOGainmapMetadata(/*copyData=*/false).get(), info);
+        if (didPopulateInfo && info.fGainmapMathColorSpace) {
+            auto iccData = metadataDecoder.getICCProfileData(/*copyData=*/false);
+            skcms_ICCProfile iccProfile;
+            if (iccData && skcms_Parse(iccData->data(), iccData->size(), &iccProfile)) {
+                auto iccProfileSpace = SkColorSpace::Make(iccProfile);
+                if (iccProfileSpace) {
+                    info.fGainmapMathColorSpace = std::move(iccProfileSpace);
+                }
+            }
+        }
+    }
 
-    // Next try for Apple gainmap metadata. This does not require anything specific from the base
-    // image.
     if (!didPopulateInfo) {
-        didPopulateInfo = xmp->getGainmapInfoApple(baseImageAppleHdrHeadroom, &info);
+        // The Adobe and Apple gain map metadata require XMP. Parse it now.
+        auto xmp = metadataDecoder.getXmpMetadata();
+        if (!xmp) {
+            return false;
+        }
+
+        // Check for Adobe gain map metadata only if the base image specified hdrgm:Version="1.0".
+        if (!didPopulateInfo && baseImageHasAdobeXmp) {
+            didPopulateInfo = xmp->getGainmapInfoAdobe(&info);
+        }
+
+        // Next try for Apple gain map metadata. This does not require anything specific from the
+        // base image.
+        if (!didPopulateInfo && baseImageHasAppleExif) {
+            didPopulateInfo = xmp->getGainmapInfoApple(baseImageAppleHdrHeadroom, &info);
+        }
     }
 
     // If none of the formats identified itself as a gainmap and populated |info| then fail.
@@ -151,6 +175,10 @@
     SkExifMetadata baseExif(getExifMetadata(/*copyData=*/false));
     auto xmp = getXmpMetadata();
 
+    // Determine if a support ISO 21496-1 gain map version is present in the base image.
+    bool isoGainmapPresent =
+            SkGainmapInfo::ParseVersion(getISOGainmapMetadata(/*copyData=*/false).get());
+
     // Determine if Apple HDR headroom is indicated in the base image.
     float appleHdrHeadroom = 1.f;
     bool appleHdrHeadroomPresent = baseExif.getHdrHeadroom(&appleHdrHeadroom);
@@ -185,6 +213,7 @@
             if (extract_gainmap(sourceMgr,
                                 mpImageOffset,
                                 mpImageSize,
+                                isoGainmapPresent,
                                 adobeGainmapPresent,
                                 appleHdrHeadroomPresent,
                                 appleHdrHeadroom,
@@ -206,6 +235,7 @@
         if (extract_gainmap(sourceMgr,
                             containerGainmapOffset,
                             containerGainmapSize,
+                            /*baseImageHasIsoVersion=*/false,
                             adobeGainmapPresent,
                             /*baseImageHasAppleExif=*/false,
                             /*baseImageAppleHdrHeadroom=*/1.0,
@@ -431,6 +461,16 @@
                          copyData);
 }
 
+sk_sp<SkData> SkJpegMetadataDecoderImpl::getISOGainmapMetadata(bool copyData) const {
+    return read_metadata(fMarkerList,
+                         kISOGainmapMarker,
+                         kISOGainmapSig,
+                         sizeof(kISOGainmapSig),
+                         /*signaturePadding=*/0,
+                         /*bytesInIndex=*/0,
+                         copyData);
+}
+
 bool SkJpegMetadataDecoderImpl::mightHaveGainmapImage() const {
 #ifdef SK_CODEC_DECODES_JPEG_GAINMAPS
     // All supported gainmap formats require MPF. Reject images that do not have MPF.
diff --git a/src/codec/SkJpegMetadataDecoderImpl.h b/src/codec/SkJpegMetadataDecoderImpl.h
index 8d02728..284b02a 100644
--- a/src/codec/SkJpegMetadataDecoderImpl.h
+++ b/src/codec/SkJpegMetadataDecoderImpl.h
@@ -40,6 +40,7 @@
     // SkJpegMetadataDecoder implementation:
     sk_sp<SkData> getExifMetadata(bool copyData) const override;
     sk_sp<SkData> getICCProfileData(bool copyData) const override;
+    sk_sp<SkData> getISOGainmapMetadata(bool copyData) const override;
     bool mightHaveGainmapImage() const override;
     bool findGainmapImage(sk_sp<SkData> baseImageData,
                           sk_sp<SkData>& outGainmapImageData,
diff --git a/src/encode/SkJpegGainmapEncoder.cpp b/src/encode/SkJpegGainmapEncoder.cpp
index a8c8daa..562a724 100644
--- a/src/encode/SkJpegGainmapEncoder.cpp
+++ b/src/encode/SkJpegGainmapEncoder.cpp
@@ -180,19 +180,43 @@
     return s.detachAsData();
 }
 
+static sk_sp<SkData> get_iso_gainmap_segment_params(sk_sp<SkData> data) {
+    SkDynamicMemoryWStream s;
+    s.write(kISOGainmapSig, sizeof(kISOGainmapSig));
+    s.write(data->data(), data->size());
+    return s.detachAsData();
+}
+
 bool SkJpegGainmapEncoder::EncodeHDRGM(SkWStream* dst,
                                        const SkPixmap& base,
                                        const SkJpegEncoder::Options& baseOptions,
                                        const SkPixmap& gainmap,
                                        const SkJpegEncoder::Options& gainmapOptions,
                                        const SkGainmapInfo& gainmapInfo) {
+    bool includeUltraHDRv1 = gainmapInfo.isUltraHDRv1Compatible();
+
     // Encode the gainmap image.
     sk_sp<SkData> gainmapData;
     {
         SkJpegMetadataEncoder::SegmentList metadataSegments;
-        SkJpegMetadataEncoder::AppendXMPStandard(metadataSegments,
-                                                 get_gainmap_image_xmp_metadata(gainmapInfo).get());
 
+        // Add XMP metadata.
+        if (includeUltraHDRv1) {
+            SkJpegMetadataEncoder::AppendXMPStandard(
+                    metadataSegments, get_gainmap_image_xmp_metadata(gainmapInfo).get());
+        }
+
+        // Include the ICC profile of the alternate color space, if it is used.
+        if (gainmapInfo.fGainmapMathColorSpace) {
+            SkJpegMetadataEncoder::AppendICC(
+                    metadataSegments, gainmapOptions, gainmapInfo.fGainmapMathColorSpace.get());
+        }
+
+        // Add the ISO 21946-1 metadata.
+        metadataSegments.emplace_back(kISOGainmapMarker,
+                                      get_iso_gainmap_segment_params(gainmapInfo.serialize()));
+
+        // Encode the gainmap image.
         gainmapData = encode_to_data(gainmap, gainmapOptions, metadataSegments);
         if (!gainmapData) {
             SkCodecPrintf("Failed to encode gainmap image.\n");
@@ -204,11 +228,23 @@
     sk_sp<SkData> baseData;
     {
         SkJpegMetadataEncoder::SegmentList metadataSegments;
-        SkJpegMetadataEncoder::AppendXMPStandard(
-                metadataSegments,
-                get_base_image_xmp_metadata(static_cast<int32_t>(gainmapData->size())).get());
+
+        // Include XMP.
+        if (includeUltraHDRv1) {
+            SkJpegMetadataEncoder::AppendXMPStandard(
+                    metadataSegments,
+                    get_base_image_xmp_metadata(static_cast<int32_t>(gainmapData->size())).get());
+        }
+
+        // Include ICC profile metadata.
         SkJpegMetadataEncoder::AppendICC(metadataSegments, baseOptions, base.colorSpace());
 
+        // Include the ISO 21946-1 version metadata.
+        metadataSegments.emplace_back(
+                kISOGainmapMarker,
+                get_iso_gainmap_segment_params(SkGainmapInfo::SerializeVersion()));
+
+        // Encode the base image.
         baseData = encode_to_data(base, baseOptions, metadataSegments);
         if (!baseData) {
             SkCodecPrintf("Failed to encode base image.\n");
diff --git a/tests/JpegGainmapTest.cpp b/tests/JpegGainmapTest.cpp
index d7c1902..17cba91 100644
--- a/tests/JpegGainmapTest.cpp
+++ b/tests/JpegGainmapTest.cpp
@@ -46,6 +46,54 @@
     return true;
 }
 
+static bool approx_eq(const SkColor4f& x, const SkColor4f& y, float epsilon) {
+    return approx_eq(x.fR, y.fR, epsilon) && approx_eq(x.fG, y.fG, epsilon) &&
+           approx_eq(x.fB, y.fB, epsilon);
+}
+
+template <typename Reporter>
+void expect_approx_eq_info(Reporter& r, const SkGainmapInfo& a, const SkGainmapInfo& b) {
+    float kEpsilon = 1e-4f;
+    REPORTER_ASSERT(r, approx_eq(a.fGainmapRatioMin, b.fGainmapRatioMin, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fGainmapRatioMin, b.fGainmapRatioMin, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fGainmapGamma, b.fGainmapGamma, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fEpsilonSdr, b.fEpsilonSdr, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fEpsilonHdr, b.fEpsilonHdr, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fDisplayRatioSdr, b.fDisplayRatioSdr, kEpsilon));
+    REPORTER_ASSERT(r, approx_eq(a.fDisplayRatioHdr, b.fDisplayRatioHdr, kEpsilon));
+    REPORTER_ASSERT(r, a.fType == b.fType);
+    REPORTER_ASSERT(r, a.fBaseImageType == b.fBaseImageType);
+
+    REPORTER_ASSERT(r, !!a.fGainmapMathColorSpace == !!b.fGainmapMathColorSpace);
+    if (a.fGainmapMathColorSpace) {
+        skcms_TransferFunction a_fn;
+        skcms_Matrix3x3 a_m;
+        a.fGainmapMathColorSpace->transferFn(&a_fn);
+        a.fGainmapMathColorSpace->toXYZD50(&a_m);
+        skcms_TransferFunction b_fn;
+        skcms_Matrix3x3 b_m;
+        b.fGainmapMathColorSpace->transferFn(&b_fn);
+        b.fGainmapMathColorSpace->toXYZD50(&b_m);
+
+        REPORTER_ASSERT(r, approx_eq(a_fn.g, b_fn.g, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.a, b_fn.a, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.b, b_fn.b, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.c, b_fn.c, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.d, b_fn.d, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.e, b_fn.e, kEpsilon));
+        REPORTER_ASSERT(r, approx_eq(a_fn.f, b_fn.f, kEpsilon));
+
+        // The round-trip of the color space through the ICC profile loses significant precision.
+        // Use a larger epsilon for it.
+        const float kMatrixEpsilon = 1e-2f;
+        for (int i = 0; i < 3; ++i) {
+            for (int j = 0; j < 3; ++j) {
+                REPORTER_ASSERT(r, approx_eq(a_m.vals[i][j], b_m.vals[i][j], kMatrixEpsilon));
+            }
+        }
+    }
+}
+
 // A test stream to stress the different SkJpegSourceMgr sub-classes.
 class TestStream : public SkStream {
 public:
@@ -475,27 +523,78 @@
         SkISize dimensions;
         SkColor originColor;
         SkColor farCornerColor;
-        float ratioMin;
-        float ratioMax;
-        float hdrRatioMin;
-        float hdrRatioMax;
+        SkGainmapInfo info;
     } recs[] = {
             {"images/iphone_13_pro.jpeg",
              SkISize::Make(1512, 2016),
              0xFF3B3B3B,
              0xFF101010,
-             1.f,
-             3.482202f,
-             1.f,
-             3.482202f},
+             {{1.f, 1.f, 1.f, 1.f},
+              {3.482202f, 3.482202f, 3.482202f, 1.f},
+              {1.f, 1.f, 1.f, 1.f},
+              {0.f, 0.f, 0.f, 1.f},
+              {0.f, 0.f, 0.f, 1.f},
+              1.f,
+              3.482202f,
+              SkGainmapInfo::BaseImageType::kSDR,
+              SkGainmapInfo::Type::kApple,
+              nullptr}},
             {"images/iphone_15.jpeg",
              SkISize::Make(2016, 1512),
              0xFF5C5C5C,
              0xFF656565,
-             1.f,
-             3.755272f,
-             1.f,
-             3.755272f},
+             {{1.f, 1.f, 1.f, 1.f},
+              {3.755272f, 3.755272f, 3.755272f, 1.f},
+              {1.f, 1.f, 1.f, 1.f},
+              {0.f, 0.f, 0.f, 1.f},
+              {0.f, 0.f, 0.f, 1.f},
+              1.f,
+              3.755272f,
+              SkGainmapInfo::BaseImageType::kSDR,
+              SkGainmapInfo::Type::kApple,
+              nullptr}},
+            {"images/gainmap_gcontainer_only.jpg",
+             SkISize::Make(32, 32),
+             0xffffffff,
+             0xffffffff,
+             {{25.f, 0.5f, 1.f, 1.f},
+              {2.f, 4.f, 8.f, 1.f},
+              {0.5, 1.f, 2.f, 1.f},
+              {0.01f, 0.001f, 0.0001f, 1.f},
+              {0.0001f, 0.001f, 0.01f, 1.f},
+              2.f,
+              4.f,
+              SkGainmapInfo::BaseImageType::kSDR,
+              SkGainmapInfo::Type::kDefault,
+              nullptr}},
+            {"images/gainmap_iso21496_1_adobe_gcontainer.jpg",
+             SkISize::Make(32, 32),
+             0xffffffff,
+             0xff000000,
+             {{25.f, 0.5f, 1.f, 1.f},
+              {2.f, 4.f, 8.f, 1.f},
+              {0.5, 1.f, 2.f, 1.f},
+              {0.01f, 0.001f, 0.0001f, 1.f},
+              {0.0001f, 0.001f, 0.01f, 1.f},
+              2.f,
+              4.f,
+              SkGainmapInfo::BaseImageType::kSDR,
+              SkGainmapInfo::Type::kDefault,
+              nullptr}},
+            {"images/gainmap_iso21496_1.jpg",
+             SkISize::Make(32, 32),
+             0xffffffff,
+             0xff000000,
+             {{25.f, 0.5f, 1.f, 1.f},
+              {2.f, 4.f, 8.f, 1.f},
+              {0.5, 1.f, 2.f, 1.f},
+              {0.01f, 0.001f, 0.0001f, 1.f},
+              {0.0001f, 0.001f, 0.01f, 1.f},
+              2.f,
+              4.f,
+              SkGainmapInfo::BaseImageType::kHDR,
+              SkGainmapInfo::Type::kDefault,
+              SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kRec2020)}},
     };
 
     TestStream::Type kStreamTypes[] = {
@@ -524,17 +623,7 @@
                             rec.farCornerColor);
 
             // Verify the gainmap rendering parameters.
-            constexpr float kEpsilon = 1e-3f;
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMin.fR, rec.ratioMin, kEpsilon));
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMin.fG, rec.ratioMin, kEpsilon));
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMin.fB, rec.ratioMin, kEpsilon));
-
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMax.fR, rec.ratioMax, kEpsilon));
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMax.fG, rec.ratioMax, kEpsilon));
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fGainmapRatioMax.fB, rec.ratioMax, kEpsilon));
-
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fDisplayRatioSdr, rec.hdrRatioMin, kEpsilon));
-            REPORTER_ASSERT(r, approx_eq(gainmapInfo.fDisplayRatioHdr, rec.hdrRatioMax, kEpsilon));
+            expect_approx_eq_info(r, rec.info, gainmapInfo);
         }
     }
 }
@@ -580,83 +669,92 @@
 
 #if !defined(SK_ENABLE_NDK_IMAGES)
 
-static bool approx_eq(const SkColor4f& x, const SkColor4f& y, float epsilon) {
-    return approx_eq(x.fR, y.fR, epsilon) && approx_eq(x.fG, y.fG, epsilon) &&
-           approx_eq(x.fB, y.fB, epsilon);
-}
-
-template <typename Reporter>
-void expect_approx_eq_info(Reporter& r, const SkGainmapInfo& a, const SkGainmapInfo& b) {
-    float kEpsilon = 1e-4f;
-    REPORTER_ASSERT(r, approx_eq(a.fGainmapRatioMin, b.fGainmapRatioMin, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fGainmapRatioMin, b.fGainmapRatioMin, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fGainmapGamma, b.fGainmapGamma, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fEpsilonSdr, b.fEpsilonSdr, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fEpsilonHdr, b.fEpsilonHdr, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fDisplayRatioSdr, b.fDisplayRatioSdr, kEpsilon));
-    REPORTER_ASSERT(r, approx_eq(a.fDisplayRatioHdr, b.fDisplayRatioHdr, kEpsilon));
-    REPORTER_ASSERT(r, a.fType == b.fType);
-    REPORTER_ASSERT(r, a.fBaseImageType == b.fBaseImageType);
-    REPORTER_ASSERT(
-            r,
-            SkColorSpace::Equals(a.fGainmapMathColorSpace.get(), b.fGainmapMathColorSpace.get()));
-}
-
 DEF_TEST(AndroidCodec_gainmapInfoEncode, r) {
     SkDynamicMemoryWStream encodeStream;
-    SkGainmapInfo gainmapInfo;
+
+    constexpr size_t kNumTests = 4;
 
     SkBitmap baseBitmap;
-    baseBitmap.allocPixels(SkImageInfo::MakeN32Premul(8, 8));
+    baseBitmap.allocPixels(SkImageInfo::MakeN32Premul(16, 16));
 
-    SkBitmap gainmapBitmap;
-    gainmapBitmap.allocPixels(SkImageInfo::MakeN32Premul(8, 8));
+    SkBitmap gainmapBitmaps[kNumTests];
+    gainmapBitmaps[0].allocPixels(SkImageInfo::MakeN32Premul(16, 16));
+    gainmapBitmaps[1].allocPixels(SkImageInfo::MakeN32Premul(8, 8));
+    gainmapBitmaps[2].allocPixels(
+            SkImageInfo::Make(4, 4, kAlpha_8_SkColorType, kPremul_SkAlphaType));
+    gainmapBitmaps[3].allocPixels(
+            SkImageInfo::Make(8, 8, kGray_8_SkColorType, kPremul_SkAlphaType));
 
-    gainmapInfo.fGainmapRatioMin.fR = 1.f;
-    gainmapInfo.fGainmapRatioMin.fG = 2.f;
-    gainmapInfo.fGainmapRatioMin.fB = 4.f;
-    gainmapInfo.fGainmapRatioMax.fR = 8.f;
-    gainmapInfo.fGainmapRatioMax.fG = 16.f;
-    gainmapInfo.fGainmapRatioMax.fB = 32.f;
-    gainmapInfo.fGainmapGamma.fR = 64.f;
-    gainmapInfo.fGainmapGamma.fG = 128.f;
-    gainmapInfo.fGainmapGamma.fB = 256.f;
-    gainmapInfo.fEpsilonSdr.fR = 1 / 10.f;
-    gainmapInfo.fEpsilonSdr.fG = 1 / 11.f;
-    gainmapInfo.fEpsilonSdr.fB = 1 / 12.f;
-    gainmapInfo.fEpsilonHdr.fR = 1 / 13.f;
-    gainmapInfo.fEpsilonHdr.fG = 1 / 14.f;
-    gainmapInfo.fEpsilonHdr.fB = 1 / 15.f;
-    gainmapInfo.fDisplayRatioSdr = 4.f;
-    gainmapInfo.fDisplayRatioHdr = 32.f;
+    SkGainmapInfo infos[kNumTests] = {
+            // Multi-channel, UltraHDR-compatible.
+            {{1.f, 2.f, 4.f, 1.f},
+             {8.f, 16.f, 32.f, 1.f},
+             {64.f, 128.f, 256.f, 1.f},
+             {1 / 10.f, 1 / 11.f, 1 / 12.f, 1.f},
+             {1 / 13.f, 1 / 14.f, 1 / 15.f, 1.f},
+             4.f,
+             32.f,
+             SkGainmapInfo::BaseImageType::kSDR,
+             SkGainmapInfo::Type::kDefault,
+             nullptr},
+            // Multi-channel, not UltraHDR-compatible.
+            {{1.f, 2.f, 4.f, 1.f},
+             {8.f, 16.f, 32.f, 1.f},
+             {64.f, 128.f, 256.f, 1.f},
+             {1 / 10.f, 1 / 11.f, 1 / 12.f, 1.f},
+             {1 / 13.f, 1 / 14.f, 1 / 15.f, 1.f},
+             4.f,
+             32.f,
+             SkGainmapInfo::BaseImageType::kSDR,
+             SkGainmapInfo::Type::kDefault,
+             SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3)},
+            // Single-channel, UltraHDR-compatible.
+            {{1.f, 1.f, 1.f, 1.f},
+             {8.f, 8.f, 8.f, 1.f},
+             {64.f, 64.f, 64.f, 1.f},
+             {1 / 128.f, 1 / 128.f, 1 / 128.f, 1.f},
+             {1 / 256.f, 1 / 256.f, 1 / 256.f, 1.f},
+             4.f,
+             32.f,
+             SkGainmapInfo::BaseImageType::kSDR,
+             SkGainmapInfo::Type::kDefault,
+             nullptr},
+            // Single-channel, not UltraHDR-compatible.
+            {{1.f, 1.f, 1.f, 1.f},
+             {8.f, 8.f, 8.f, 1.f},
+             {64.f, 64.f, 64.f, 1.f},
+             {1 / 128.f, 1 / 128.f, 1 / 128.f, 1.f},
+             {1 / 256.f, 1 / 256.f, 1 / 256.f, 1.f},
+             4.f,
+             32.f,
+             SkGainmapInfo::BaseImageType::kHDR,
+             SkGainmapInfo::Type::kDefault,
+             SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3)},
+    };
 
-    for (int i = 0; i < 2; ++i) {
-        // In the second iteration, change some of the lists to scalars.
-        if (i == 1) {
-            gainmapInfo.fGainmapRatioMax.fR = 32.f;
-            gainmapInfo.fGainmapRatioMax.fG = 32.f;
-            gainmapInfo.fGainmapRatioMax.fB = 32.f;
-            gainmapInfo.fEpsilonSdr.fR = 1 / 10.f;
-            gainmapInfo.fEpsilonSdr.fG = 1 / 10.f;
-            gainmapInfo.fEpsilonSdr.fB = 1 / 10.f;
-        }
-
+    for (size_t i = 0; i < kNumTests; ++i) {
         // Encode |gainmapInfo|.
         bool encodeResult = SkJpegGainmapEncoder::EncodeHDRGM(&encodeStream,
                                                               baseBitmap.pixmap(),
                                                               SkJpegEncoder::Options(),
-                                                              gainmapBitmap.pixmap(),
+                                                              gainmapBitmaps[i].pixmap(),
                                                               SkJpegEncoder::Options(),
-                                                              gainmapInfo);
+                                                              infos[i]);
         REPORTER_ASSERT(r, encodeResult);
 
         // Decode into |decodedGainmapInfo|.
         SkGainmapInfo decodedGainmapInfo;
+        SkBitmap decodedBaseBitmap;
+        SkBitmap decodedGainmapBitmap;
         auto decodeStream = std::make_unique<SkMemoryStream>(encodeStream.detachAsData());
-        decode_all(r, std::move(decodeStream), baseBitmap, gainmapBitmap, decodedGainmapInfo);
+        decode_all(r,
+                   std::move(decodeStream),
+                   decodedBaseBitmap,
+                   decodedGainmapBitmap,
+                   decodedGainmapInfo);
 
-        // Verify they are |gainmapInfo| matches |decodedGainmapInfo|.
-        expect_approx_eq_info(r, gainmapInfo, decodedGainmapInfo);
+        // Verify that the decode reproducd the input.
+        expect_approx_eq_info(r, infos[i], decodedGainmapInfo);
     }
 }