Initial implementation of a SkColorSpace_A2B xform

There is support for all features of SkColorSpace_A2B.

Tests for these functionality were adapted from
the XYZ xform, plus a CLUT-specific test was added.

Shared functions used by both SkColorSpaceXform_XYZ and SkColorSpaceXform_A2B
have been moved into a shared header.

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2449243003
CQ_INCLUDE_TRYBOTS=master.client.skia:Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-SKNX_NO_SIMD-Trybot

Review-Url: https://codereview.chromium.org/2449243003
diff --git a/tests/ColorSpaceXformTest.cpp b/tests/ColorSpaceXformTest.cpp
index 477e61a..0e67fe4 100644
--- a/tests/ColorSpaceXformTest.cpp
+++ b/tests/ColorSpaceXformTest.cpp
@@ -10,6 +10,7 @@
 #include "SkCodecPriv.h"
 #include "SkColorPriv.h"
 #include "SkColorSpace.h"
+#include "SkColorSpace_A2B.h"
 #include "SkColorSpace_Base.h"
 #include "SkColorSpace_XYZ.h"
 #include "SkColorSpaceXform_Base.h"
@@ -25,10 +26,42 @@
         // Use special testing entry point, so we don't skip the xform, even though src == dst.
         return SlowIdentityXform(static_cast<SkColorSpace_XYZ*>(space.get()));
     }
+
+    static std::unique_ptr<SkColorSpaceXform> CreateIdentityXform_A2B(
+            SkGammaNamed gammaNamed, const sk_sp<SkGammas>& gammas) {
+        std::vector<SkColorSpace_A2B::Element> srcElements;
+        // sRGB
+        const float values[16] = {
+            0.4358f, 0.3853f, 0.1430f, 0.0f,
+            0.2224f, 0.7170f, 0.0606f, 0.0f,
+            0.0139f, 0.0971f, 0.7139f, 0.0f,
+            0.0000f, 0.0000f, 0.0000f, 1.0f
+        };
+        SkMatrix44 arbitraryMatrix{SkMatrix44::kUninitialized_Constructor};
+        arbitraryMatrix.setRowMajorf(values);
+        if (kNonStandard_SkGammaNamed == gammaNamed) {
+            srcElements.push_back(SkColorSpace_A2B::Element(gammas));
+        } else {
+            srcElements.push_back(SkColorSpace_A2B::Element(gammaNamed));
+        }
+        srcElements.push_back(SkColorSpace_A2B::Element(arbitraryMatrix));
+        auto srcSpace = ColorSpaceXformTest::CreateA2BSpace(SkColorSpace_A2B::PCS::kXYZ,
+                                                            std::move(srcElements));
+        sk_sp<SkColorSpace> dstSpace(new SkColorSpace_XYZ(gammaNamed, gammas, arbitraryMatrix,
+                                                          nullptr));
+
+        return SkColorSpaceXform::New(static_cast<SkColorSpace_A2B*>(srcSpace.get()),
+                                      static_cast<SkColorSpace_XYZ*>(dstSpace.get()));
+    }
+
+    static sk_sp<SkColorSpace> CreateA2BSpace(SkColorSpace_A2B::PCS pcs,
+                                              std::vector<SkColorSpace_A2B::Element> elements) {
+        return sk_sp<SkColorSpace>(new SkColorSpace_A2B(pcs, nullptr, std::move(elements)));
+    }
 };
 
 static bool almost_equal(int x, int y) {
-    return SkTAbs(x - y) <= 1;
+    return SkTAbs(x - y) <= 1 ;
 }
 
 static void test_identity_xform(skiatest::Reporter* r, const sk_sp<SkGammas>& gammas,
@@ -67,6 +100,42 @@
     }
 }
 
+static void test_identity_xform_A2B(skiatest::Reporter* r, SkGammaNamed gammaNamed,
+                                    const sk_sp<SkGammas>& gammas, bool repeat) {
+    // Arbitrary set of 10 pixels
+    constexpr int width = 10;
+    constexpr uint32_t srcPixels[width] = {
+            0xFFABCDEF, 0xFF146829, 0xFF382759, 0xFF184968, 0xFFDE8271,
+            0xFF32AB52, 0xFF0383BC, 0xFF000102, 0xFFFFFFFF, 0xFFDDEEFF, };
+    uint32_t dstPixels[width];
+
+    // Create and perform an identity xform.
+    auto xform = ColorSpaceXformTest::CreateIdentityXform_A2B(gammaNamed, gammas);
+    bool result = xform->apply(select_xform_format(kN32_SkColorType), dstPixels,
+                               SkColorSpaceXform::kBGRA_8888_ColorFormat, srcPixels, width,
+                               kOpaque_SkAlphaType);
+    REPORTER_ASSERT(r, result);
+
+    // Since the src->dst matrix is the identity, and the gamma curves match,
+    // the pixels should be unchanged.
+    for (int i = 0; i < width; i++) {
+        REPORTER_ASSERT(r, almost_equal(((srcPixels[i] >>  0) & 0xFF),
+                                        SkGetPackedB32(dstPixels[i])));
+        REPORTER_ASSERT(r, almost_equal(((srcPixels[i] >>  8) & 0xFF),
+                                        SkGetPackedG32(dstPixels[i])));
+        REPORTER_ASSERT(r, almost_equal(((srcPixels[i] >> 16) & 0xFF),
+                                        SkGetPackedR32(dstPixels[i])));
+        REPORTER_ASSERT(r, almost_equal(((srcPixels[i] >> 24) & 0xFF),
+                                        SkGetPackedA32(dstPixels[i])));
+    }
+
+    if (repeat) {
+        // We should cache part of the transform after the run.  So it is interesting
+        // to make sure it still runs correctly the second time.
+        test_identity_xform_A2B(r, gammaNamed, gammas, false);
+    }
+}
+
 DEF_TEST(ColorSpaceXform_TableGamma, r) {
     // Lookup-table based gamma curves
     constexpr size_t tableSize = 10;
@@ -90,6 +159,7 @@
     table[8] = 0.75f;
     table[9] = 1.00f;
     test_identity_xform(r, gammas, true);
+    test_identity_xform_A2B(r, kNonStandard_SkGammaNamed, gammas, true);
 }
 
 DEF_TEST(ColorSpaceXform_ParametricGamma, r) {
@@ -116,6 +186,7 @@
     params->fC = 0.0f;
     params->fG = 2.4f;
     test_identity_xform(r, gammas, true);
+    test_identity_xform_A2B(r, kNonStandard_SkGammaNamed, gammas, true);
 }
 
 DEF_TEST(ColorSpaceXform_ExponentialGamma, r) {
@@ -124,6 +195,7 @@
     gammas->fRedType = gammas->fGreenType = gammas->fBlueType = SkGammas::Type::kValue_Type;
     gammas->fRedData.fValue = gammas->fGreenData.fValue = gammas->fBlueData.fValue = 1.4f;
     test_identity_xform(r, gammas, true);
+    test_identity_xform_A2B(r, kNonStandard_SkGammaNamed, gammas, true);
 }
 
 DEF_TEST(ColorSpaceXform_NamedGamma, r) {
@@ -133,6 +205,10 @@
     gammas->fGreenData.fNamed = k2Dot2Curve_SkGammaNamed;
     gammas->fBlueData.fNamed = kLinear_SkGammaNamed;
     test_identity_xform(r, gammas, true);
+    test_identity_xform_A2B(r, kNonStandard_SkGammaNamed, gammas, true);
+    test_identity_xform_A2B(r, kSRGB_SkGammaNamed, nullptr, true);
+    test_identity_xform_A2B(r, k2Dot2Curve_SkGammaNamed, nullptr, true);
+    test_identity_xform_A2B(r, kLinear_SkGammaNamed, nullptr, true);
 }
 
 DEF_TEST(ColorSpaceXform_NonMatchingGamma, r) {
@@ -174,5 +250,68 @@
     gammas->fBlueData.fParamOffset = sizeof(float) * tableSize;
 
     test_identity_xform(r, gammas, true);
+    test_identity_xform_A2B(r, kNonStandard_SkGammaNamed, gammas, true);
+}
+
+DEF_TEST(ColorSpaceXform_A2BCLUT, r) {
+    constexpr int inputChannels = 3;
+    constexpr int gp            = 4; // # grid points
+
+    constexpr int numEntries    = gp*gp*gp*3;
+    uint8_t gridPoints[3] = {gp, gp, gp};
+    void* memory = sk_malloc_throw(sizeof(SkColorLookUpTable) + sizeof(float) * numEntries);
+    sk_sp<SkColorLookUpTable> colorLUT(new (memory) SkColorLookUpTable(inputChannels, gridPoints));
+    // make a CLUT that rotates R, G, and B ie R->G, G->B, B->R
+    float* table = SkTAddOffset<float>(memory, sizeof(SkColorLookUpTable));
+    for (int r = 0; r < gp; ++r) {
+        for (int g = 0; g < gp; ++g) {
+            for (int b = 0; b < gp; ++b) {
+                table[3*(gp*gp*r + gp*g + b) + 0] = g * (1.f / (gp - 1.f));
+                table[3*(gp*gp*r + gp*g + b) + 1] = b * (1.f / (gp - 1.f));
+                table[3*(gp*gp*r + gp*g + b) + 2] = r * (1.f / (gp - 1.f));
+            }
+        }
+    }
+
+    // build an even distribution of pixels every (7 / 255) steps
+    // to test the xform on
+    constexpr int pixelgp   = 7;
+    constexpr int numPixels = pixelgp*pixelgp*pixelgp;
+    SkAutoTMalloc<uint32_t> srcPixels(numPixels);
+    int srcIndex = 0;
+    for (int r = 0; r < pixelgp; ++r) {
+        for (int g = 0; g < pixelgp; ++g) {
+            for (int b = 0; b < pixelgp; ++b) {
+                const int red   = (int) (r * (255.f / (pixelgp - 1.f)));
+                const int green = (int) (g * (255.f / (pixelgp - 1.f)));
+                const int blue  = (int) (b * (255.f / (pixelgp - 1.f)));
+                srcPixels[srcIndex] = SkColorSetRGB(red, green, blue);
+                ++srcIndex;
+            }
+        }
+    }
+    SkAutoTMalloc<uint32_t> dstPixels(numPixels);
+
+    // src space is identity besides CLUT
+    std::vector<SkColorSpace_A2B::Element> srcElements;
+    srcElements.push_back(SkColorSpace_A2B::Element(std::move(colorLUT)));
+    auto srcSpace = ColorSpaceXformTest::CreateA2BSpace(SkColorSpace_A2B::PCS::kXYZ,
+                                                        std::move(srcElements));
+    // dst space is entirely identity
+    auto dstSpace = SkColorSpace::MakeRGB(SkColorSpace::kLinear_RenderTargetGamma, SkMatrix44::I());
+    auto xform = SkColorSpaceXform::New(srcSpace.get(), dstSpace.get());
+    bool result = xform->apply(SkColorSpaceXform::kRGBA_8888_ColorFormat, dstPixels.get(),
+                               SkColorSpaceXform::kRGBA_8888_ColorFormat, srcPixels.get(),
+                               numPixels, kOpaque_SkAlphaType);
+    REPORTER_ASSERT(r, result);
+
+    for (int i = 0; i < numPixels; ++i) {
+        REPORTER_ASSERT(r, almost_equal(SkColorGetR(srcPixels[i]),
+                                        SkColorGetG(dstPixels[i])));
+        REPORTER_ASSERT(r, almost_equal(SkColorGetG(srcPixels[i]),
+                                        SkColorGetB(dstPixels[i])));
+        REPORTER_ASSERT(r, almost_equal(SkColorGetB(srcPixels[i]),
+                                        SkColorGetR(dstPixels[i])));
+    }
 }