SkPngRustDecoder: Add HDR metadata support

To the rust_png FFI interface, add ColorSpacePrimaries,
MasteringDisplayColorVolume, and ContentLightLevelInfo structures.

Add a test that ensure that metadata specified to encode match metadata
retrieved after decode. Once this is landed, it will be used to create
test images.

To the reader interface, add try_get_mdcv_chunk and try_get_clli_chunk
methods.

Bug: 376550658
Change-Id: I49c1d9d6cfae1f8fa1b684520c82b1d1c480e903
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1055198
Reviewed-by: Christopher Cameron <ccameron@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
Commit-Queue: Christopher Cameron <ccameron@google.com>
diff --git a/relnotes/sk_png_hdr.md b/relnotes/sk_png_hdr.md
index c710f4e..828ef8d 100644
--- a/relnotes/sk_png_hdr.md
+++ b/relnotes/sk_png_hdr.md
@@ -1 +1,2 @@
-Add HDR metadata support to `SkPngCodec` and `SkPngEncoder`.
+Add HDR metadata support to `SkPngDecoder`, `SkPngRustDecoder`, and
+`SkPngEncoder`.
diff --git a/rust/png/FFI.rs b/rust/png/FFI.rs
index 778a10f..8544486 100644
--- a/rust/png/FFI.rs
+++ b/rust/png/FFI.rs
@@ -91,6 +91,31 @@
         LimitsExceededError,
     }
 
+    /// FFI/layering-friendly equivalent of `SkColorSpacePrimaries from C/C++.
+    struct ColorSpacePrimaries {
+        fRX: f32,
+        fRY: f32,
+        fGX: f32,
+        fGY: f32,
+        fBX: f32,
+        fBY: f32,
+        fWX: f32,
+        fWY: f32,
+    }
+
+    /// FFI/layering-friendly equivalent of `skhdr::MasteringDisplayColorVolume` from C/C++.
+    struct MasteringDisplayColorVolume {
+        fDisplayPrimaries: ColorSpacePrimaries,
+        fMaximumDisplayMasteringLuminance: f32,
+        fMinimumDisplayMasteringLuminance: f32,
+    }
+
+    /// FFI/layering-friendly equivalent of `skhdr::ContentLightLevelInformation` from C/C++.
+    struct ContentLightLevelInfo {
+        fMaxCLL: f32,
+        fMaxFALL : f32,
+    }
+
     unsafe extern "C++" {
         include!("rust/png/FFI.h");
 
@@ -136,14 +161,7 @@
         fn is_srgb(self: &Reader) -> bool;
         fn try_get_chrm(
             self: &Reader,
-            wx: &mut f32,
-            wy: &mut f32,
-            rx: &mut f32,
-            ry: &mut f32,
-            gx: &mut f32,
-            gy: &mut f32,
-            bx: &mut f32,
-            by: &mut f32,
+            chrm: &mut ColorSpacePrimaries,
         ) -> bool;
         fn try_get_cicp_chunk(
             self: &Reader,
@@ -152,6 +170,14 @@
             matrix_id: &mut u8,
             is_full_range: &mut bool,
         ) -> bool;
+        fn try_get_mdcv_chunk(
+            self: &Reader,
+            mdcv: &mut MasteringDisplayColorVolume,
+        ) -> bool;
+        fn try_get_clli_chunk(
+            self: &Reader,
+            clli: &mut ContentLightLevelInfo,
+        ) -> bool;
         fn try_get_gama(self: &Reader, gamma: &mut f32) -> bool;
         fn has_exif_chunk(self: &Reader) -> bool;
         fn get_exif_chunk(self: &Reader) -> &[u8];
@@ -475,14 +501,7 @@
     /// etc.).  Otherwise, returns `false`.
     fn try_get_chrm(
         &self,
-        wx: &mut f32,
-        wy: &mut f32,
-        rx: &mut f32,
-        ry: &mut f32,
-        gx: &mut f32,
-        gy: &mut f32,
-        bx: &mut f32,
-        by: &mut f32,
+        chrm: &mut ffi::ColorSpacePrimaries,
     ) -> bool {
         fn copy_channel(channel: &(png::ScaledFloat, png::ScaledFloat), x: &mut f32, y: &mut f32) {
             *x = png_u32_into_f32(channel.0);
@@ -491,11 +510,11 @@
 
         match self.reader.info().chrm_chunk.as_ref() {
             None => false,
-            Some(chrm) => {
-                copy_channel(&chrm.white, wx, wy);
-                copy_channel(&chrm.red, rx, ry);
-                copy_channel(&chrm.green, gx, gy);
-                copy_channel(&chrm.blue, bx, by);
+            Some(png_chrm) => {
+                copy_channel(&png_chrm.white, &mut chrm.fWX, &mut chrm.fWY);
+                copy_channel(&png_chrm.red, &mut chrm.fRX, &mut chrm.fRY);
+                copy_channel(&png_chrm.green, &mut chrm.fGX, &mut chrm.fGY);
+                copy_channel(&png_chrm.blue, &mut chrm.fBX, &mut chrm.fBY);
                 true
             }
         }
@@ -523,6 +542,60 @@
         }
     }
 
+    /// If the decoded PNG image contained a `mDCV` chunk then
+    /// `try_get_mdcv_chunk` returns `true` and populates the out parameters
+    /// as values that are CIE 1931 xy coordinates or values in cd/m^2.
+    /// Otherwise, returns `false`.
+    fn try_get_mdcv_chunk(
+        self: &Reader,
+        mdcv: &mut ffi::MasteringDisplayColorVolume,
+    ) -> bool {
+        match self.reader.info().mastering_display_color_volume.as_ref() {
+            None => false,
+            Some(png_mdcv) => {
+                *mdcv = ffi::MasteringDisplayColorVolume {
+                    fDisplayPrimaries: ffi::ColorSpacePrimaries {
+                        fRX: png_mdcv.chromaticities.red.0.into_value(),
+                        fRY: png_mdcv.chromaticities.red.1.into_value(),
+                        fGX: png_mdcv.chromaticities.green.0.into_value(),
+                        fGY: png_mdcv.chromaticities.green.1.into_value(),
+                        fBX: png_mdcv.chromaticities.blue.0.into_value(),
+                        fBY: png_mdcv.chromaticities.blue.1.into_value(),
+                        fWX: png_mdcv.chromaticities.white.0.into_value(),
+                        fWY: png_mdcv.chromaticities.white.1.into_value(),
+                    },
+                    fMaximumDisplayMasteringLuminance:
+                        png_mdcv.max_luminance as f32 / 10_000.0,
+                    fMinimumDisplayMasteringLuminance:
+                        png_mdcv.min_luminance as f32 / 10_000.0,
+                };
+                true
+            }
+        }
+    }
+
+    /// If the decoded PNG image contained a `cLLI` chunk then
+    /// `try_get_clli_chunk` returns `true` and populates the out
+    /// parameters as values in cd/m^2.  Otherwise, returns `false`.
+    fn try_get_clli_chunk(
+        &self,
+        clli: &mut ffi::ContentLightLevelInfo,
+    ) -> bool {
+        match self.reader.info().content_light_level.as_ref() {
+            None => false,
+            Some(png_clli) => {
+                *clli = ffi::ContentLightLevelInfo {
+                    fMaxCLL:
+                        png_clli.max_content_light_level as f32 / 10_000.0,
+                    fMaxFALL:
+                        png_clli.max_frame_average_light_level as f32 /
+                        10_000.0,
+                };
+                true
+            }
+        }
+    }
+
     /// If the decoded PNG image contained a `gAMA` chunk then `try_get_gama`
     /// returns `true` and populates the `gamma` out parameter.  Otherwise,
     /// returns `false`.
diff --git a/src/codec/SkPngRustCodec.cpp b/src/codec/SkPngRustCodec.cpp
index aa71444..0517600 100644
--- a/src/codec/SkPngRustCodec.cpp
+++ b/src/codec/SkPngRustCodec.cpp
@@ -14,6 +14,7 @@
 #include "include/core/SkColorSpace.h"
 #include "include/core/SkStream.h"
 #include "include/private/SkEncodedInfo.h"
+#include "include/private/SkHdrMetadata.h"
 #include "include/private/base/SkAssert.h"
 #include "include/private/base/SkSafe32.h"
 #include "include/private/base/SkTemplates.h"
@@ -106,6 +107,21 @@
     SK_ABORT("Unexpected `rust_png::BlendOp`: %d", static_cast<int>(op));
 }
 
+SkColorSpacePrimaries ToSkColorSpacePrimaries(const rust_png::ColorSpacePrimaries& p) {
+    return SkColorSpacePrimaries({p.fRX, p.fRY, p.fGX, p.fGY, p.fBX, p.fBY, p.fWX, p.fWY});
+}
+
+skhdr::MasteringDisplayColorVolume ToSkMDCV(const rust_png::MasteringDisplayColorVolume& mdcv) {
+    return skhdr::MasteringDisplayColorVolume({
+        ToSkColorSpacePrimaries(mdcv.fDisplayPrimaries),
+        mdcv.fMaximumDisplayMasteringLuminance,
+        mdcv.fMinimumDisplayMasteringLuminance});
+}
+
+skhdr::ContentLightLevelInformation ToSkCLLI(const rust_png::ContentLightLevelInfo& clli) {
+    return skhdr::ContentLightLevelInformation({clli.fMaxCLL, clli.fMaxFALL});
+}
+
 std::unique_ptr<SkEncodedInfo::ICCProfile> CreateColorProfile(const rust_png::Reader& reader) {
     // NOTE: This method is based on `read_color_profile` in
     // `src/codec/SkPngCodec.cpp` but has been refactored to use Rust inputs
@@ -171,15 +187,8 @@
         // so we match the behavior of Safari and Firefox instead (compat).
         return nullptr;
     }
-    float rx = 0.0;
-    float ry = 0.0;
-    float gx = 0.0;
-    float gy = 0.0;
-    float bx = 0.0;
-    float by = 0.0;
-    float wx = 0.0;
-    float wy = 0.0;
-    const bool got_chrm = reader.try_get_chrm(wx, wy, rx, ry, gx, gy, bx, by);
+    rust_png::ColorSpacePrimaries chrm;
+    const bool got_chrm = reader.try_get_chrm(chrm);
     if (!got_chrm) {
         // If there is no `cHRM` chunk then check if `gamma` is neutral (in PNG
         // / `SkNamedTransferFn::k2Dot2` sense).  `kPngGammaThreshold` mimics
@@ -202,7 +211,7 @@
     // Construct a color profile based on `cHRM` and `gAMA` chunks.
     skcms_Matrix3x3 toXYZD50;
     if (got_chrm) {
-        if (!skcms_PrimariesToXYZD50(rx, ry, gx, gy, bx, by, wx, wy, &toXYZD50)) {
+        if (!ToSkColorSpacePrimaries(chrm).toXYZD50(&toXYZD50)) {
             return nullptr;
         }
     } else {
@@ -236,6 +245,18 @@
         profile = nullptr;
     }
 
+    skhdr::Metadata hdrMetadata;
+    {
+        rust_png::MasteringDisplayColorVolume rust_mdcv;
+        if (reader.try_get_mdcv_chunk(rust_mdcv)) {
+            hdrMetadata.setMasteringDisplayColorVolume(ToSkMDCV(rust_mdcv));
+        }
+        rust_png::ContentLightLevelInfo rust_clli;
+        if (reader.try_get_clli_chunk(rust_clli)) {
+            hdrMetadata.setContentLightLevelInformation(ToSkCLLI(rust_clli));
+        }
+    }
+
     // Protect against large PNGs. See http://bugzil.la/251381 for more details.
     constexpr uint32_t kMaxPNGSize = 1000000;
     if ((reader.width() > kMaxPNGSize) || (reader.height() > kMaxPNGSize)) {
@@ -256,8 +277,10 @@
                                height,
                                skColor,
                                ToAlpha(rustColor, reader),
-                               reader.output_bits_per_component(),
-                               std::move(profile));
+                               reader.output_bits_per_component(), // bitsPerComponent
+                               reader.output_bits_per_component(), // colorDepth
+                               std::move(profile),
+                               hdrMetadata);
 }
 
 SkCodec::Result ToSkCodecResult(rust_png::DecodingResult rustResult) {
diff --git a/tests/CodecTest.cpp b/tests/CodecTest.cpp
index 4fcfeac..2b65f20 100644
--- a/tests/CodecTest.cpp
+++ b/tests/CodecTest.cpp
@@ -54,6 +54,10 @@
 #include "tools/Resources.h"
 #include "tools/ToolUtils.h"
 
+#if defined(SK_CODEC_DECODES_PNG_WITH_RUST)
+#include "include/codec/SkPngRustDecoder.h"
+#endif
+
 #ifdef SK_ENABLE_ANDROID_UTILS
 #include "client_utils/android/FrontBufferedStream.h"
 #endif
@@ -2634,3 +2638,28 @@
 }
 #endif
 
+#if defined(SK_CODEC_DECODES_PNG_WITH_RUST) && \
+    defined(SK_CODEC_ENCODES_PNG_WITH_LIBPNG) && \
+    !defined(SK_PNG_DISABLE_TESTS)
+DEF_TEST(PngRustHdrMetadataRoundTrip, r) {
+    SkBitmap bm;
+    bm.allocPixels(SkImageInfo::MakeN32Premul(10, 10));
+
+    // The SkPngRustEncoder doesn't support writing HDR metadata, so this uses the libpng encoder.
+    SkPngEncoder::Options options;
+    options.fHdrMetadata.setMasteringDisplayColorVolume(
+        skhdr::MasteringDisplayColorVolume({SkNamedPrimaries::kRec2020, 500.f, 0.0005f}));
+    options.fHdrMetadata.setContentLightLevelInformation(
+        skhdr::ContentLightLevelInformation({1000.f, 150.f}));
+
+    SkDynamicMemoryWStream wstream;
+    SkPngEncoder::Encode(&wstream, bm.pixmap(), options);
+
+    SkCodec::Result result;
+    auto codec = SkPngRustDecoder::Decode(
+        std::make_unique<SkMemoryStream>(wstream.detachAsData()), &result);
+
+    REPORTER_ASSERT(r, options.fHdrMetadata == codec->getHdrMetadata());
+}
+#endif
+