Reland: Add ISO 21496-1 gainmap parsing

To SkGainmapInfo, add the functions ParseVersion, Parse,
SerializeVersion, and serialize. These generate the ISO 21496-1
binary blobs.

Reverted because of unused function in test. Moved function inside
appropriate ifdef.

Bug: b/338342146
Change-Id: I98436f3757f08668c3129b02f40dd5bbe223a073
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/853216
Reviewed-by: Brian Osman <brianosman@google.com>
Reviewed-by: Christopher Cameron <ccameron@google.com>
Commit-Queue: Christopher Cameron <ccameron@google.com>
diff --git a/gn/codec.gni b/gn/codec.gni
index b77a6a7..5a3aa9c 100644
--- a/gn/codec.gni
+++ b/gn/codec.gni
@@ -43,6 +43,7 @@
   "$_src/codec/SkColorPalette.h",
   "$_src/codec/SkExif.cpp",
   "$_src/codec/SkFrameHolder.h",
+  "$_src/codec/SkGainmapInfo.cpp",
   "$_src/codec/SkImageGenerator_FromEncoded.cpp",
   "$_src/codec/SkMaskSwizzler.cpp",
   "$_src/codec/SkMaskSwizzler.h",
diff --git a/include/private/SkGainmapInfo.h b/include/private/SkGainmapInfo.h
index a4a7a6c..07512a4 100644
--- a/include/private/SkGainmapInfo.h
+++ b/include/private/SkGainmapInfo.h
@@ -10,6 +10,8 @@
 
 #include "include/core/SkColor.h"
 #include "include/core/SkColorSpace.h"
+#include "include/core/SkRefCnt.h"
+class SkData;
 
 /**
  *  Gainmap rendering parameters. Suppose our display has HDR to SDR ratio of H and we wish to
@@ -92,13 +94,38 @@
      */
     sk_sp<SkColorSpace> fGainmapMathColorSpace = nullptr;
 
+    /**
+     * If |data| contains an ISO 21496-1 version that is supported, return true. Otherwise return
+     * false.
+     */
+    static bool ParseVersion(const SkData* data);
+
+    /**
+     * If |data| constains ISO 21496-1 metadata then parse that metadata then use it to populate
+     * |info| and return true, otherwise return false. If |data| indicates that that the base image
+     * color space primaries should be used for gainmap application then set
+     * |fGainmapMathColorSpace| to nullptr, otherwise set |fGainmapMathColorSpace| to sRGB (the
+     * default, to be overwritten by the image decoder).
+     */
+    static bool Parse(const SkData* data, SkGainmapInfo& info);
+
+    /**
+     * Serialize an ISO 21496-1 version 0 blob containing only the version structure.
+     */
+    static sk_sp<SkData> SerializeVersion();
+
+    /**
+     * Serialize an ISO 21496-1 version 0 blob containing this' gainmap parameters.
+     */
+    sk_sp<SkData> serialize() const;
+
     inline bool operator==(const SkGainmapInfo& other) const {
         return fGainmapRatioMin == other.fGainmapRatioMin &&
                fGainmapRatioMax == other.fGainmapRatioMax && fGainmapGamma == other.fGainmapGamma &&
                fEpsilonSdr == other.fEpsilonSdr && fEpsilonHdr == other.fEpsilonHdr &&
                fDisplayRatioSdr == other.fDisplayRatioSdr &&
                fDisplayRatioHdr == other.fDisplayRatioHdr &&
-               fBaseImageType == other.fBaseImageType &&
+               fBaseImageType == other.fBaseImageType && fType == other.fType &&
                SkColorSpace::Equals(fGainmapMathColorSpace.get(),
                                     other.fGainmapMathColorSpace.get());
     }
diff --git a/public.bzl b/public.bzl
index 885dc1b..7465670 100644
--- a/public.bzl
+++ b/public.bzl
@@ -1827,6 +1827,7 @@
     "src/codec/SkEncodedInfo.cpp",
     "src/codec/SkExif.cpp",
     "src/codec/SkFrameHolder.h",
+    "src/codec/SkGainmapInfo.cpp",
     "src/codec/SkImageGenerator_FromEncoded.cpp",
     "src/codec/SkJpegCodec.cpp",
     "src/codec/SkJpegCodec.h",
diff --git a/src/codec/BUILD.bazel b/src/codec/BUILD.bazel
index 154e48c..13a5eb3 100644
--- a/src/codec/BUILD.bazel
+++ b/src/codec/BUILD.bazel
@@ -27,6 +27,7 @@
     "SkColorPalette.h",
     "SkExif.cpp",
     "SkFrameHolder.h",
+    "SkGainmapInfo.cpp",
     "SkImageGenerator_FromEncoded.cpp",
     "SkMaskSwizzler.cpp",
     "SkMaskSwizzler.h",
@@ -319,6 +320,7 @@
         "SkColorPalette.cpp",
         "SkEncodedInfo.cpp",
         "SkExif.cpp",
+        "SkGainmapInfo.cpp",
         "SkImageGenerator_FromEncoded.cpp",
         "SkMaskSwizzler.cpp",
         "SkParseEncodedOrigin.cpp",
diff --git a/src/codec/SkGainmapInfo.cpp b/src/codec/SkGainmapInfo.cpp
new file mode 100644
index 0000000..d0fd75d
--- /dev/null
+++ b/src/codec/SkGainmapInfo.cpp
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/private/SkGainmapInfo.h"
+
+#include "include/core/SkColor.h"
+#include "include/core/SkData.h"
+#include "include/core/SkRefCnt.h"
+#include "include/core/SkStream.h"
+#include "src/base/SkEndian.h"
+#include "src/codec/SkCodecPriv.h"
+
+#include <cmath>
+#include <cstdint>
+#include <memory>
+
+namespace {
+constexpr uint8_t kIsMultiChannelMask = (1u << 7);
+constexpr uint8_t kUseBaseColourSpaceMask = (1u << 6);
+}  // namespace
+
+static void write_u16_be(SkWStream* s, uint16_t value) {
+    value = SkEndian_SwapBE16(value);
+    s->write16(value);
+}
+
+static void write_u32_be(SkWStream* s, uint32_t value) {
+    value = SkEndian_SwapBE32(value);
+    s->write32(value);
+}
+
+static void write_s32_be(SkWStream* s, int32_t value) {
+    value = SkEndian_SwapBE32(value);
+    s->write32(value);
+}
+
+static void write_rational_be(SkWStream* s, float x) {
+    // TODO(b/338342146): Select denominator to get maximum precision and robustness.
+    uint32_t denominator = 0x10000000;
+    if (std::abs(x) > 1.f) {
+        denominator = 0x1000;
+    }
+    int32_t numerator = static_cast<int32_t>(static_cast<double>(x) * denominator + 0.5);
+    write_s32_be(s, numerator);
+    write_u32_be(s, denominator);
+}
+
+static void write_positive_rational_be(SkWStream* s, float x) {
+    // TODO(b/338342146): Select denominator to get maximum precision and robustness.
+    uint32_t denominator = 0x10000000;
+    if (x > 1.f) {
+        denominator = 0x1000;
+    }
+    uint32_t numerator = static_cast<uint32_t>(static_cast<double>(x) * denominator + 0.5);
+    write_u32_be(s, numerator);
+    write_u32_be(s, denominator);
+}
+
+static bool read_u16_be(SkStream* s, uint16_t* value) {
+    if (!s->readU16(value)) {
+        return false;
+    }
+    *value = SkEndian_SwapBE16(*value);
+    return true;
+}
+
+static bool read_u32_be(SkStream* s, uint32_t* value) {
+    if (!s->readU32(value)) {
+        return false;
+    }
+    *value = SkEndian_SwapBE32(*value);
+    return true;
+}
+
+static bool read_s32_be(SkStream* s, int32_t* value) {
+    if (!s->readS32(value)) {
+        return false;
+    }
+    *value = SkEndian_SwapBE32(*value);
+    return true;
+}
+
+static bool read_rational_be(SkStream* s, float* value) {
+    int32_t numerator = 0;
+    uint32_t denominator = 0;
+    if (!read_s32_be(s, &numerator)) {
+        return false;
+    }
+    if (!read_u32_be(s, &denominator)) {
+        return false;
+    }
+    *value = static_cast<float>(static_cast<double>(numerator) / static_cast<double>(denominator));
+    return true;
+}
+
+static bool read_positive_rational_be(SkStream* s, float* value) {
+    uint32_t numerator = 0;
+    uint32_t denominator = 0;
+    if (!read_u32_be(s, &numerator)) {
+        return false;
+    }
+    if (!read_u32_be(s, &denominator)) {
+        return false;
+    }
+    *value = static_cast<float>(static_cast<double>(numerator) / static_cast<double>(denominator));
+    return true;
+}
+
+static bool read_iso_gainmap_version(SkStream* s) {
+    // Ensure minimum version is 0.
+    uint16_t minimum_version = 0;
+    if (!read_u16_be(s, &minimum_version)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 minimum version.\n");
+        return false;
+    }
+    if (minimum_version != 0) {
+        SkCodecPrintf("Unsupported ISO 21496-1 minimum version.\n");
+        return false;
+    }
+
+    // Ensure writer version is present. No value is invalid.
+    uint16_t writer_version = 0;
+    if (!read_u16_be(s, &writer_version)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 version.\n");
+        return false;
+    }
+
+    return true;
+}
+
+static bool read_iso_gainmap_info(SkStream* s, SkGainmapInfo& info) {
+    if (!read_iso_gainmap_version(s)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 version.\n");
+        return false;
+    }
+
+    uint8_t flags = 0;
+    if (!s->readU8(&flags)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 flags.\n");
+        return false;
+    }
+    bool isMultiChannel = (flags & kIsMultiChannelMask) != 0;
+    bool useBaseColourSpace = (flags & kUseBaseColourSpaceMask) != 0;
+
+    float baseHdrHeadroom = 0.f;
+    if (!read_positive_rational_be(s, &baseHdrHeadroom)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 base HDR headroom.\n");
+        return false;
+    }
+    float altrHdrHeadroom = 0.f;
+    if (!read_positive_rational_be(s, &altrHdrHeadroom)) {
+        SkCodecPrintf("Failed to read ISO 21496-1 altr HDR headroom.\n");
+        return false;
+    }
+
+    float gainMapMin[3] = {0.f};
+    float gainMapMax[3] = {0.f};
+    float gamma[3] = {0.f};
+    float baseOffset[3] = {0.f};
+    float altrOffset[3] = {0.f};
+
+    int channelCount = isMultiChannel ? 3 : 1;
+    for (int i = 0; i < channelCount; ++i) {
+        if (!read_rational_be(s, gainMapMin + i)) {
+            SkCodecPrintf("Failed to read ISO 21496-1 gainmap minimum.\n");
+            return false;
+        }
+        if (!read_rational_be(s, gainMapMax + i)) {
+            SkCodecPrintf("Failed to read ISO 21496-1 gainmap maximum.\n");
+            return false;
+        }
+        if (!read_positive_rational_be(s, gamma + i)) {
+            SkCodecPrintf("Failed to read ISO 21496-1 gamma.\n");
+            return false;
+        }
+        if (!read_rational_be(s, baseOffset + i)) {
+            SkCodecPrintf("Failed to read ISO 21496-1 base offset.\n");
+            return false;
+        }
+        if (!read_rational_be(s, altrOffset + i)) {
+            SkCodecPrintf("Failed to read ISO 21496-1 altr offset.\n");
+            return false;
+        }
+    }
+
+    info = SkGainmapInfo();
+    if (!useBaseColourSpace) {
+        info.fGainmapMathColorSpace = SkColorSpace::MakeSRGB();
+    }
+    if (baseHdrHeadroom < altrHdrHeadroom) {
+        info.fBaseImageType = SkGainmapInfo::BaseImageType::kSDR;
+        info.fDisplayRatioSdr = std::exp2(baseHdrHeadroom);
+        info.fDisplayRatioHdr = std::exp2(altrHdrHeadroom);
+    } else {
+        info.fBaseImageType = SkGainmapInfo::BaseImageType::kHDR;
+        info.fDisplayRatioHdr = std::exp2(baseHdrHeadroom);
+        info.fDisplayRatioSdr = std::exp2(altrHdrHeadroom);
+    }
+    for (int i = 0; i < 3; ++i) {
+        int j = i >= channelCount ? 0 : i;
+        info.fGainmapRatioMin[i] = std::exp2(gainMapMin[j]);
+        info.fGainmapRatioMax[i] = std::exp2(gainMapMax[j]);
+        info.fGainmapGamma[i] = 1.f / gamma[j];
+        switch (info.fBaseImageType) {
+            case SkGainmapInfo::BaseImageType::kSDR:
+                info.fEpsilonSdr[i] = baseOffset[j];
+                info.fEpsilonHdr[i] = altrOffset[j];
+                break;
+            case SkGainmapInfo::BaseImageType::kHDR:
+                info.fEpsilonHdr[i] = baseOffset[j];
+                info.fEpsilonSdr[i] = altrOffset[j];
+                break;
+        }
+    }
+    return true;
+}
+
+bool SkGainmapInfo::ParseVersion(const SkData* data) {
+    if (!data) {
+        return false;
+    }
+    auto s = SkMemoryStream::MakeDirect(data->data(), data->size());
+    return read_iso_gainmap_version(s.get());
+}
+
+bool SkGainmapInfo::Parse(const SkData* data, SkGainmapInfo& info) {
+    if (!data) {
+        return false;
+    }
+    auto s = SkMemoryStream::MakeDirect(data->data(), data->size());
+    return read_iso_gainmap_info(s.get(), info);
+}
+
+sk_sp<SkData> SkGainmapInfo::SerializeVersion() {
+    SkDynamicMemoryWStream s;
+    write_u16_be(&s, 0);  // Minimum reader version
+    write_u16_be(&s, 0);  // Writer version
+    return s.detachAsData();
+}
+
+static bool is_single_channel(SkColor4f c) { return c.fR == c.fG && c.fG == c.fB; };
+
+sk_sp<SkData> SkGainmapInfo::serialize() const {
+    SkDynamicMemoryWStream s;
+    // Version.
+    write_u16_be(&s, 0);  // Minimum reader version
+    write_u16_be(&s, 0);  // Writer version
+
+    // Flags.
+    bool all_single_channel = is_single_channel(fGainmapRatioMin) &&
+                              is_single_channel(fGainmapRatioMax) &&
+                              is_single_channel(fGainmapGamma) && is_single_channel(fEpsilonSdr) &&
+                              is_single_channel(fEpsilonHdr);
+    uint8_t flags = 0;
+    if (!fGainmapMathColorSpace) {
+        flags |= kUseBaseColourSpaceMask;
+    }
+    if (!all_single_channel) {
+        flags |= kIsMultiChannelMask;
+    }
+    s.write8(flags);
+
+    // Base and altr headroom.
+    switch (fBaseImageType) {
+        case SkGainmapInfo::BaseImageType::kSDR:
+            write_positive_rational_be(&s, std::log2(fDisplayRatioSdr));
+            write_positive_rational_be(&s, std::log2(fDisplayRatioHdr));
+            break;
+        case SkGainmapInfo::BaseImageType::kHDR:
+            write_positive_rational_be(&s, std::log2(fDisplayRatioHdr));
+            write_positive_rational_be(&s, std::log2(fDisplayRatioSdr));
+            break;
+    }
+
+    // Per-channel information.
+    for (int i = 0; i < (all_single_channel ? 1 : 3); ++i) {
+        write_rational_be(&s, std::log2(fGainmapRatioMin[i]));
+        write_rational_be(&s, std::log2(fGainmapRatioMax[i]));
+        write_positive_rational_be(&s, 1.f / fGainmapGamma[i]);
+        switch (fBaseImageType) {
+            case SkGainmapInfo::BaseImageType::kSDR:
+                write_rational_be(&s, fEpsilonSdr[i]);
+                write_rational_be(&s, fEpsilonHdr[i]);
+                break;
+            case SkGainmapInfo::BaseImageType::kHDR:
+                write_rational_be(&s, fEpsilonHdr[i]);
+                write_rational_be(&s, fEpsilonSdr[i]);
+                break;
+        }
+    }
+    return s.detachAsData();
+}
diff --git a/tests/JpegGainmapTest.cpp b/tests/JpegGainmapTest.cpp
index 0e85eb0..d7c1902 100644
--- a/tests/JpegGainmapTest.cpp
+++ b/tests/JpegGainmapTest.cpp
@@ -35,6 +35,17 @@
 
 namespace {
 
+// Return true if the relative difference between x and y is less than epsilon.
+static bool approx_eq(float x, float y, float epsilon) {
+    float numerator = std::abs(x - y);
+    // To avoid being too sensitive around zero, set the minimum denominator to epsilon.
+    float denominator = std::max(std::min(std::abs(x), std::abs(y)), epsilon);
+    if (numerator / denominator > epsilon) {
+        return false;
+    }
+    return true;
+}
+
 // A test stream to stress the different SkJpegSourceMgr sub-classes.
 class TestStream : public SkStream {
 public:
@@ -458,8 +469,6 @@
                                                                  gainmapBitmap.rowBytes()));
 }
 
-static bool approx_eq(float x, float y, float epsilon) { return std::abs(x - y) < epsilon; }
-
 DEF_TEST(AndroidCodec_jpegGainmapDecode, r) {
     const struct Rec {
         const char* path;
@@ -571,11 +580,28 @@
 
 #if !defined(SK_ENABLE_NDK_IMAGES)
 
-static bool approx_eq_rgb(const SkColor4f& x, const SkColor4f& y, float epsilon) {
+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;
@@ -630,7 +656,7 @@
         decode_all(r, std::move(decodeStream), baseBitmap, gainmapBitmap, decodedGainmapInfo);
 
         // Verify they are |gainmapInfo| matches |decodedGainmapInfo|.
-        REPORTER_ASSERT(r, gainmapInfo == decodedGainmapInfo);
+        expect_approx_eq_info(r, gainmapInfo, decodedGainmapInfo);
     }
 }
 
@@ -728,36 +754,7 @@
         decode_all(r, std::move(decodeStream), baseBitmap[1], gainmapBitmap[1], gainmapInfo[1]);
 
         // HDRGM will have the same rendering parameters.
-        REPORTER_ASSERT(
-                r,
-                approx_eq_rgb(
-                        gainmapInfo[0].fGainmapRatioMin, gainmapInfo[1].fGainmapRatioMin,kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq_rgb(
-                        gainmapInfo[0].fGainmapRatioMax, gainmapInfo[1].fGainmapRatioMax, kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq_rgb(
-                        gainmapInfo[0].fGainmapGamma, gainmapInfo[1].fGainmapGamma, kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq(gainmapInfo[0].fEpsilonSdr.fR, gainmapInfo[1].fEpsilonSdr.fR, kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq(gainmapInfo[0].fEpsilonHdr.fR, gainmapInfo[1].fEpsilonHdr.fR, kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq(
-                        gainmapInfo[0].fDisplayRatioSdr,
-                        gainmapInfo[1].fDisplayRatioSdr,
-                        kEpsilon));
-        REPORTER_ASSERT(
-                r,
-                approx_eq(
-                        gainmapInfo[0].fDisplayRatioHdr,
-                        gainmapInfo[1].fDisplayRatioHdr,
-                        kEpsilon));
+        expect_approx_eq_info(r, gainmapInfo[0], gainmapInfo[1]);
 
         // Render a few pixels and verify that they come out the same. Rendering requires SkSL.
         const struct Rec {
@@ -796,8 +793,88 @@
             SkColor4f p1 = render_gainmap_pixel(
                     rec.hdrRatio, baseBitmap[1], gainmapBitmap[1], gainmapInfo[1], rec.x, rec.y);
 
-            REPORTER_ASSERT(r, approx_eq_rgb(p0, p1, kEpsilon));
+            REPORTER_ASSERT(r, approx_eq(p0, p1, kEpsilon));
         }
     }
 }
+
+DEF_TEST(AndroidCodec_gainmapInfoParse, r) {
+    const uint8_t versionData[] = {
+            0x00,  // Minimum version
+            0x00,
+            0x00,  // Writer version
+            0x00,
+    };
+    const uint8_t data[] = {
+            0x00, 0x00,                                      // Minimum version
+            0x00, 0x00,                                      // Writer version
+            0xc0,                                            // Flags
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,  // Base HDR headroom
+            0x00, 0x01, 0x45, 0x3e, 0x00, 0x00, 0x80, 0x00,  // Altr HDR headroom
+            0xfc, 0x23, 0x05, 0x14, 0x40, 0x00, 0x00, 0x00,  // Red: Gainmap min
+            0x00, 0x01, 0x1f, 0xe1, 0x00, 0x00, 0x80, 0x00,  // Red: Gainmap max
+            0x10, 0x4b, 0x9f, 0x0a, 0x40, 0x00, 0x00, 0x00,  // Red: Gamma
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Red: Base offset
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Red: Altr offset
+            0xfd, 0xdb, 0x68, 0x04, 0x40, 0x00, 0x00, 0x00,  // Green: Gainmap min
+            0x00, 0x01, 0x11, 0x68, 0x00, 0x00, 0x80, 0x00,  // Green: Gainmap max
+            0x10, 0x28, 0xf9, 0x53, 0x40, 0x00, 0x00, 0x00,  // Green: Gamma
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Green: Base offset
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Green: Altr offset
+            0xf7, 0x16, 0x7b, 0x90, 0x40, 0x00, 0x00, 0x00,  // Blue: Gainmap min
+            0x00, 0x01, 0x0f, 0x9a, 0x00, 0x00, 0x80, 0x00,  // Blue: Gainmap max
+            0x12, 0x95, 0xa8, 0x3f, 0x40, 0x00, 0x00, 0x00,  // Blue: Gamma
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Blue: Base offset
+            0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // Blue: Altr offset
+    };
+    SkGainmapInfo kExpectedInfo = {{0.959023f, 0.977058f, 0.907989f, 1.f},
+                                   {4.753710f, 4.395375f, 4.352630f, 1.f},
+                                   {3.927490f, 3.960382f, 3.443712f, 1.f},
+                                   {0.015625f, 0.015625f, 0.015625f, 1.f},
+                                   {0.015625f, 0.015625f, 0.015625f, 1.f},
+                                   1.000000f,
+                                   5.819739f,
+                                   SkGainmapInfo::BaseImageType::kSDR,
+                                   SkGainmapInfo::Type::kDefault,
+                                   nullptr};
+    SkGainmapInfo kSingleChannelInfo = {{0.1234567e-4f, 0.1234567e-4f, 0.1234567e-4f, 1.f},
+                                        {-0.1234567e-4f, -0.1234567e-4f, -0.1234567e-4f, 1.f},
+                                        {0.1234567e+0f, 0.1234567e+0f, 0.1234567e+0f, 1.f},
+                                        {0.1234567e+4f, 0.1234567e+4f, 0.1234567e+4f, 1.f},
+                                        {0.1234567e+4f, 0.1234567e+4f, 0.1234567e+4f, 1.f},
+                                        1.,
+                                        4.f,
+                                        SkGainmapInfo::BaseImageType::kHDR,
+                                        SkGainmapInfo::Type::kDefault,
+                                        SkColorSpace::MakeSRGB()};
+
+    // Verify the version from data.
+    REPORTER_ASSERT(r,
+                    SkGainmapInfo::ParseVersion(
+                            SkData::MakeWithoutCopy(versionData, sizeof(versionData)).get()));
+
+    // Verify the SkGainmapInfo from data.
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r,
+                    SkGainmapInfo::Parse(SkData::MakeWithoutCopy(data, sizeof(data)).get(), info));
+    expect_approx_eq_info(r, info, kExpectedInfo);
+
+    // Verify the parsed version.
+    REPORTER_ASSERT(r, SkGainmapInfo::ParseVersion(SkGainmapInfo::SerializeVersion().get()));
+
+    // Verify the round-trip SkGainmapInfo.
+    auto dataInfo = info.serialize();
+    SkGainmapInfo infoRoundTrip;
+    REPORTER_ASSERT(r, SkGainmapInfo::Parse(dataInfo.get(), infoRoundTrip));
+    expect_approx_eq_info(r, info, infoRoundTrip);
+
+    // Serialize a single-channel SkGainmapInfo. The serialized data should be smaller.
+    auto dataSingleChannelInfo = kSingleChannelInfo.serialize();
+    REPORTER_ASSERT(r, dataSingleChannelInfo->size() < dataInfo->size());
+    SkGainmapInfo singleChannelInfoRoundTrip;
+    REPORTER_ASSERT(r,
+                    SkGainmapInfo::Parse(dataSingleChannelInfo.get(), singleChannelInfoRoundTrip));
+    expect_approx_eq_info(r, singleChannelInfoRoundTrip, kSingleChannelInfo);
+}
+
 #endif  // !defined(SK_ENABLE_NDK_IMAGES)