Merge "Add color space connection"
diff --git a/include/ui/ColorSpace.h b/include/ui/ColorSpace.h
index c6a20c9..15e6628 100644
--- a/include/ui/ColorSpace.h
+++ b/include/ui/ColorSpace.h
@@ -20,6 +20,7 @@
 #include <array>
 #include <cmath>
 #include <functional>
+#include <memory>
 #include <string>
 
 #include <ui/mat3.h>
@@ -165,6 +166,30 @@
     static const ColorSpace ACES();
     static const ColorSpace ACEScg();
 
+    class Connector {
+    public:
+        Connector(const ColorSpace& src, const ColorSpace& dst) noexcept;
+
+        constexpr const ColorSpace& getSource() const noexcept { return mSource; }
+        constexpr const ColorSpace& getDestination() const noexcept { return mDestination; }
+
+        constexpr const mat3& getTransform() const noexcept { return mTransform; }
+
+        constexpr float3 transform(const float3& v) const noexcept {
+            float3 linear = mSource.toLinear(apply(v, mSource.getClamper()));
+            return apply(mDestination.fromLinear(mTransform * linear), mDestination.getClamper());
+        }
+
+    private:
+        const ColorSpace& mSource;
+        const ColorSpace& mDestination;
+        mat3 mTransform;
+    };
+
+    static const Connector connect(const ColorSpace& src, const ColorSpace& dst) {
+        return Connector(src, dst);
+    }
+
 private:
     static constexpr mat3 computeXYZMatrix(
             const std::array<float2, 3>& primaries, const float2& whitePoint);
diff --git a/libs/ui/ColorSpace.cpp b/libs/ui/ColorSpace.cpp
index 6296abe..6f005f4 100644
--- a/libs/ui/ColorSpace.cpp
+++ b/libs/ui/ColorSpace.cpp
@@ -96,6 +96,47 @@
     };
 }
 
+static const float2 ILLUMINANT_D50_XY = {0.34567f, 0.35850f};
+static const float3 ILLUMINANT_D50_XYZ = {0.964212f, 1.0f, 0.825188f};
+static const mat3 VON_KRIES = mat3{
+    float3{ 0.8951f, -0.7502f,  0.0389f},
+    float3{ 0.2664f,  1.7135f, -0.0685f},
+    float3{-0.1614f,  0.0367f,  1.0296f}
+};
+
+static mat3 adaptation(const mat3& matrix, const float3& srcWhitePoint, const float3& dstWhitePoint) {
+    float3 srcLMS = matrix * srcWhitePoint;
+    float3 dstLMS = matrix * dstWhitePoint;
+    return inverse(matrix) * mat3{dstLMS / srcLMS} * matrix;
+}
+
+ColorSpace::Connector::Connector(
+        const ColorSpace& src,
+        const ColorSpace& dst) noexcept
+        : mSource(src)
+        , mDestination(dst) {
+
+    if (all(lessThan(abs(src.getWhitePoint() - dst.getWhitePoint()), float2{1e-3f}))) {
+        mTransform = dst.getXYZtoRGB() * src.getRGBtoXYZ();
+    } else {
+        mat3 rgbToXYZ(src.getRGBtoXYZ());
+        mat3 xyzToRGB(dst.getXYZtoRGB());
+
+        float3 srcXYZ = XYZ(float3{src.getWhitePoint(), 1});
+        float3 dstXYZ = XYZ(float3{dst.getWhitePoint(), 1});
+
+        if (any(greaterThan(abs(src.getWhitePoint() - ILLUMINANT_D50_XY), float2{1e-3f}))) {
+            rgbToXYZ = adaptation(VON_KRIES, srcXYZ, ILLUMINANT_D50_XYZ) * src.getRGBtoXYZ();
+        }
+
+        if (any(greaterThan(abs(dst.getWhitePoint() - ILLUMINANT_D50_XY), float2{1e-3f}))) {
+            xyzToRGB = inverse(adaptation(VON_KRIES, dstXYZ, ILLUMINANT_D50_XYZ) * dst.getRGBtoXYZ());
+        }
+
+        mTransform = xyzToRGB * rgbToXYZ;
+    }
+}
+
 static constexpr float rcpResponse(float x, float g,float a, float b, float c, float d) {
     return x >= d * c ? (std::pow(x, 1.0f / g) - b) / a : x / c;
 }
@@ -112,6 +153,10 @@
     return std::copysign(response(std::abs(x), g, a, b, c, d), x);
 }
 
+static float safePow(float x, float e) {
+    return powf(x < 0.0f ? 0.0f : x, e);
+}
+
 const ColorSpace ColorSpace::sRGB() {
     return {
         "sRGB IEC61966-2.1",
@@ -187,8 +232,8 @@
         "Adobe RGB (1998)",
         {{float2{0.64f, 0.33f}, {0.21f, 0.71f}, {0.15f, 0.06f}}},
         {0.3127f, 0.3290f},
-        std::bind(powf, _1, 1.0f / 2.2f),
-        std::bind(powf, _1, 2.2f)
+        std::bind(safePow, _1, 1.0f / 2.2f),
+        std::bind(safePow, _1, 2.2f)
     };
 }
 
@@ -196,7 +241,7 @@
     return {
         "ROMM RGB ISO 22028-2:2013",
         {{float2{0.7347f, 0.2653f}, {0.1596f, 0.8404f}, {0.0366f, 0.0001f}}},
-        {0.3457f, 0.3585f},
+        {0.34567f, 0.35850f},
         std::bind(rcpResponse, _1, 1.8f, 1.0f, 0.0f, 1 / 16.0f, 0.031248f),
         std::bind(response,    _1, 1.8f, 1.0f, 0.0f, 1 / 16.0f, 0.031248f)
     };
@@ -217,8 +262,8 @@
         "SMPTE RP 431-2-2007 DCI (P3)",
         {{float2{0.680f, 0.320f}, {0.265f, 0.690f}, {0.150f, 0.060f}}},
         {0.314f, 0.351f},
-        std::bind(powf, _1, 1.0f / 2.6f),
-        std::bind(powf, _1, 2.6f)
+        std::bind(safePow, _1, 1.0f / 2.6f),
+        std::bind(safePow, _1, 2.6f)
     };
 }
 
diff --git a/libs/ui/tests/colorspace_test.cpp b/libs/ui/tests/colorspace_test.cpp
index e5c2633..0bfa487 100644
--- a/libs/ui/tests/colorspace_test.cpp
+++ b/libs/ui/tests/colorspace_test.cpp
@@ -150,4 +150,16 @@
     EXPECT_TRUE(extendedSRGB.g > 1.0f);
 }
 
+TEST_F(ColorSpaceTest, Connect) {
+    // No chromatic adaptation
+    auto r = ColorSpace::connect(ColorSpace::sRGB(), ColorSpace::AdobeRGB())
+            .transform({1.0f, 0.5f, 0.0f});
+    EXPECT_TRUE(all(lessThan(abs(r - float3{0.8912f, 0.4962f, 0.1164f}), float3{1e-4f})));
+
+    // Test with chromatic adaptation
+    r = ColorSpace::connect(ColorSpace::sRGB(), ColorSpace::ProPhotoRGB())
+            .transform({1.0f, 0.0f, 0.0f});
+    EXPECT_TRUE(all(lessThan(abs(r - float3{0.70226f, 0.2757f, 0.1036f}), float3{1e-4f})));
+}
+
 }; // namespace android