WIP: Skia support library for ICC tasks
authorMatt Sarett <msarett@google.com>
Fri, 16 Dec 2016 21:28:17 +0000 (16:28 -0500)
committerSkia Commit-Bot <skia-commit-bot@chromium.org>
Fri, 16 Dec 2016 22:13:28 +0000 (22:13 +0000)
As a starting point, this would be mostly trivial to implement using
SkColorSpace.

This also would give us the flexibility to begin to move all of
the ICC related code from SkColorSpace to SkICC.

What are the advantages of moving this away from SkColorSpace?
(1) A long term goal (once Chrome uses SkCodec), might be to
move SkColorSpace::MakeICC() out of the public API.  That way,
we can guarantee that we can draw to/from *any* SkColorSpace.
(2) Keeps SkColorSpace separate from ICC-specific representations
like SkColorSpaceTransferFn etc.

BUG=skia:

Change-Id: Iddeb9903221fb57fbfc01218d8641c928b4a5165
Reviewed-on: https://skia-review.googlesource.com/5676
Commit-Queue: Matt Sarett <msarett@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
Reviewed-by: Mike Reed <reed@google.com>
gn/core.gni
gn/tests.gni
include/core/SkICC.h [new file with mode: 0644]
src/core/SkColorSpacePriv.h
src/core/SkColorSpaceXform_A2B.cpp
src/core/SkColorSpace_A2B.h
src/core/SkColorSpace_Base.h
src/core/SkColorSpace_XYZ.cpp
src/core/SkColorSpace_XYZ.h
src/core/SkICC.cpp [new file with mode: 0644]
tests/ICCTest.cpp [new file with mode: 0644]

index d88ac5a299abba26e4eb9f0c30164bcb7b38a844..2e457531c81dc2dee7e208ad320d32ee203b7979 100644 (file)
@@ -153,6 +153,7 @@ skia_core_sources = [
   "$_src/core/SkGraphics.cpp",
   "$_src/core/SkHalf.cpp",
   "$_src/core/SkHalf.h",
+  "$_src/core/SkICC.cpp",
   "$_src/core/SkImageFilter.cpp",
   "$_src/core/SkImageFilterCache.cpp",
   "$_src/core/SkImageFilterCache.h",
index 5f2d8b7931e60a7e28b6d8184829523503dbba6a..79fd4be34c5a54300cf794a69d90a3517761dfb0 100644 (file)
@@ -99,6 +99,7 @@ tests_sources = [
   "$_tests/GrTRecorderTest.cpp",
   "$_tests/HashTest.cpp",
   "$_tests/image-bitmap.cpp",
+  "$_tests/ICCTest.cpp",
   "$_tests/ImageCacheTest.cpp",
   "$_tests/ImageFilterCacheTest.cpp",
   "$_tests/ImageFilterTest.cpp",
diff --git a/include/core/SkICC.h b/include/core/SkICC.h
new file mode 100644 (file)
index 0000000..3780498
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkICC_DEFINED
+#define SkICC_DEFINED
+
+#include "SkRefCnt.h"
+
+struct SkColorSpaceTransferFn;
+class SkColorSpace;
+class SkData;
+class SkMatrix44;
+
+class SK_API SkICC : public SkRefCnt {
+public:
+
+    /**
+     *  Parse an ICC profile.
+     *
+     *  Returns nullptr if the data is not a valid ICC profile or if the profile
+     *  input space is not RGB.
+     */
+    static sk_sp<SkICC> Make(const void*, size_t);
+
+    /**
+     *  If the gamut can be represented as transformation into XYZ D50, returns
+     *  true and sets the proper values in |toXYZD50|.
+     *
+     *  If not, returns false.  This indicates that the ICC data is too complex
+     *  to isolate a simple gamut transformation.
+     */
+    bool toXYZD50(SkMatrix44* toXYZD50) const;
+
+    /**
+     *  If the transfer function can be represented as coefficients to the standard
+     *  equation, returns true and sets |fn| to the proper values.
+     *
+     *  If not, returns false.  This indicates one of the following:
+     *  (1) The R, G, and B transfer functions are not the same.
+     *  (2) The transfer function is represented as a table that we have not managed
+     *      to match to a standard curve.
+     *  (3) The ICC data is too complex to isolate a single transfer function.
+     */
+    bool isNumericalTransferFn(SkColorSpaceTransferFn* fn) const;
+
+    /**
+     *  If the transfer function can be approximated as coefficients to the standard
+     *  equation, returns true and sets |fn| to the proper values.
+     *
+     *  If not, returns false.  This indicates one of the following:
+     *  (1) The R, G, and B transfer functions are not the same.
+     *  (2) The transfer function is represented as a table that is not increasing with
+     *      end points at zero and one.
+     *  (3) The ICC data is too complex to isolate a single transfer function.
+     */
+    bool approximateNumericalTransferFn(SkColorSpaceTransferFn* fn) const;
+
+    /**
+     *  Write an ICC profile with transfer function |fn| and gamut |toXYZD50|.
+     */
+    static sk_sp<SkData> WriteToICC(const SkColorSpaceTransferFn& fn, const SkMatrix44& toXYZD50);
+
+private:
+    SkICC(sk_sp<SkColorSpace> colorSpace);
+
+    sk_sp<SkColorSpace> fColorSpace;
+};
+
+#endif
index 73038738ff60bc024ccf9a6fcbc8754855cb4174..32981ae87549ae1ffd13ea7052755f9d814e4ee9 100644 (file)
@@ -5,14 +5,22 @@
  * found in the LICENSE file.
  */
 
+#include <math.h>
+
 #define SkColorSpacePrintf(...)
 
 static inline bool color_space_almost_equal(float a, float b) {
     return SkTAbs(a - b) < 0.01f;
 }
 
+static inline float add_epsilon(float v) {
+    return v + FLT_MIN;
+}
+
 static inline bool is_zero_to_one(float v) {
-    return (0.0f <= v) && (v <= 1.0f);
+    // Because we allow a value just barely larger than 1, the client can use an
+    // entirely linear transfer function.
+    return (0.0f <= v) && (v <= add_epsilon(1.0f));
 }
 
 static inline bool is_valid_transfer_fn(const SkColorSpaceTransferFn& coeffs) {
@@ -84,3 +92,44 @@ static inline bool is_almost_2dot2(const SkColorSpaceTransferFn& coeffs) {
            color_space_almost_equal(0.0f, coeffs.fF) &&
            color_space_almost_equal(2.2f, coeffs.fG);
 }
+
+static inline void value_to_parametric(SkColorSpaceTransferFn* coeffs, float exponent) {
+    coeffs->fA = 1.0f;
+    coeffs->fB = 0.0f;
+    coeffs->fC = 0.0f;
+    coeffs->fD = 0.0f;
+    coeffs->fE = 0.0f;
+    coeffs->fF = 0.0f;
+    coeffs->fG = exponent;
+}
+
+static inline bool named_to_parametric(SkColorSpaceTransferFn* coeffs,
+                                       SkGammaNamed gammaNamed) {
+    switch (gammaNamed) {
+        case kSRGB_SkGammaNamed:
+            coeffs->fA = 1.0f / 1.055f;
+            coeffs->fB = 0.055f / 1.055f;
+            coeffs->fC = 0.0f;
+            coeffs->fD = 0.04045f;
+            coeffs->fE = 1.0f / 12.92f;
+            coeffs->fF = 0.0f;
+            coeffs->fG = 2.4f;
+            return true;
+        case k2Dot2Curve_SkGammaNamed:
+            value_to_parametric(coeffs, 2.2f);
+            return true;
+        case kLinear_SkGammaNamed:
+            coeffs->fA = 0.0f;
+            coeffs->fB = 0.0f;
+            coeffs->fC = 0.0f;
+            // Make sure that we use the linear segment of the transfer function even
+            // when the x-value is 1.0f.
+            coeffs->fD = add_epsilon(1.0f);
+            coeffs->fE = 1.0f;
+            coeffs->fF = 0.0f;
+            coeffs->fG = 0.0f;
+            return true;
+        default:
+            return false;
+    }
+}
index 83f1d910aeb9fc8b6d71efe55346df244f917f90..cb8dd57f9d1b5bffc3881f367b4ce42784b5017b 100644 (file)
@@ -69,35 +69,19 @@ bool SkColorSpaceXform_A2B::onApply(ColorFormat dstFormat, void* dst, ColorForma
     return true;
 }
 
-static inline SkColorSpaceTransferFn value_to_parametric(float exp) {
-    return {exp, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f};
-}
-
-static inline SkColorSpaceTransferFn gammanamed_to_parametric(SkGammaNamed gammaNamed) {
-    switch (gammaNamed) {
-        case kLinear_SkGammaNamed:
-            return value_to_parametric(1.f);
-        case kSRGB_SkGammaNamed:
-            return {2.4f, (1.f / 1.055f), (0.055f / 1.055f), 0.f, 0.04045f, (1.f / 12.92f), 0.f};
-        case k2Dot2Curve_SkGammaNamed:
-            return value_to_parametric(2.2f);
-        default:
-            SkASSERT(false);
-            return {-1.f, -1.f, -1.f, -1.f, -1.f, -1.f, -1.f};
-    }
-}
-
-static inline SkColorSpaceTransferFn gamma_to_parametric(const SkGammas& gammas, int channel) {
+static inline bool gamma_to_parametric(SkColorSpaceTransferFn* coeffs, const SkGammas& gammas,
+                                       int channel) {
     switch (gammas.type(channel)) {
         case SkGammas::Type::kNamed_Type:
-            return gammanamed_to_parametric(gammas.data(channel).fNamed);
+            return named_to_parametric(coeffs, gammas.data(channel).fNamed);
         case SkGammas::Type::kValue_Type:
-            return value_to_parametric(gammas.data(channel).fValue);
+            value_to_parametric(coeffs, gammas.data(channel).fValue);
+            return true;
         case SkGammas::Type::kParam_Type:
-            return gammas.params(channel);
+            *coeffs = gammas.params(channel);
+            return true;
         default:
-            SkASSERT(false);
-            return {-1.f, -1.f, -1.f, -1.f, -1.f, -1.f, -1.f};
+            return false;
     }
 }
 static inline SkColorSpaceTransferFn invert_parametric(const SkColorSpaceTransferFn& fn) {
@@ -183,6 +167,10 @@ SkColorSpaceXform_A2B::SkColorSpaceXform_A2B(SkColorSpace_A2B* srcSpace,
         currentChannels = e.outputChannels();
         switch (e.type()) {
             case SkColorSpace_A2B::Element::Type::kGammaNamed:
+                if (kLinear_SkGammaNamed == e.gammaNamed()) {
+                    break;
+                }
+
                 // take the fast path for 3-channel named gammas
                 if (3 == currentChannels) {
                     if (k2Dot2Curve_SkGammaNamed == e.gammaNamed()) {
@@ -196,12 +184,11 @@ SkColorSpaceXform_A2B::SkColorSpaceXform_A2B(SkColorSpace_A2B* srcSpace,
                         break;
                     }
                 }
-                if (kLinear_SkGammaNamed != e.gammaNamed()) {
-                    SkCSXformPrintf("Gamma stage added: %s\n",
-                                    debugGammaNamed[(int)e.gammaNamed()]);
-                    SkColorSpaceTransferFn fn = gammanamed_to_parametric(e.gammaNamed());
-                    this->addTransferFns(fn, currentChannels);
-                }
+
+                SkCSXformPrintf("Gamma stage added: %s\n", debugGammaNamed[(int)e.gammaNamed()]);
+                SkColorSpaceTransferFn fn;
+                SkAssertResult(named_to_parametric(&fn, e.gammaNamed()));
+                this->addTransferFns(fn, currentChannels);
                 break;
             case SkColorSpace_A2B::Element::Type::kGammas: {
                 const SkGammas& gammas = e.gammas();
@@ -221,7 +208,8 @@ SkColorSpaceXform_A2B::SkColorSpaceXform_A2B(SkColorSpace_A2B* srcSpace,
                         this->addTableFn(table, channel);
                         gammaNeedsRef = true;
                     } else {
-                        SkColorSpaceTransferFn fn = gamma_to_parametric(gammas, channel);
+                        SkColorSpaceTransferFn fn;
+                        SkAssertResult(gamma_to_parametric(&fn, gammas, channel));
                         this->addTransferFn(fn, channel);
                     }
                 }
@@ -301,9 +289,9 @@ SkColorSpaceXform_A2B::SkColorSpaceXform_A2B(SkColorSpace_A2B* srcSpace,
 
                     this->addTableFn(table, channel);
                 } else {
-                    SkColorSpaceTransferFn fn =
-                            invert_parametric(gamma_to_parametric(gammas, channel));
-                    this->addTransferFn(fn, channel);
+                    SkColorSpaceTransferFn fn;
+                    SkAssertResult(gamma_to_parametric(&fn, gammas, channel));
+                    this->addTransferFn(invert_parametric(fn), channel);
                 }
             }
         }
index ed90713a2c98fe7ff8b6e5d9619da2482ffafee2..084c5934ce27237d65ead89dcdc542ebd4d04450 100644 (file)
@@ -47,15 +47,10 @@ public:
         return nullptr;
     }
 
-    bool onGammaCloseToSRGB() const override {
-        // There is no single gamma curve in an A2B0 profile
-        return false;
-    }
-
-    bool onGammaIsLinear() const override {
-        // There is no single gamma curve in an A2B0 profile
-        return false;
-    }
+    // There is no single gamma curve in an A2B0 profile
+    bool onGammaCloseToSRGB() const override { return false; }
+    bool onGammaIsLinear() const override { return false; }
+    bool onIsNumericalTransferFn(SkColorSpaceTransferFn* coeffs) const override { return false; }
 
     sk_sp<SkColorSpace> makeLinearGamma() override {
         // TODO: Analyze the extrema of our projection into XYZ and use suitable primaries?
index 75a90a6f1bf90e5f50eddcdd912954d66802141c..b3247de5c331b6405e52fcde71d7abbf4fd13d3d 100644 (file)
@@ -55,6 +55,10 @@ struct SkGammas : SkRefCnt {
                    this->fTable.fSize == that.fTable.fSize;
         }
 
+        inline bool operator!=(const Data& that) const {
+            return !(*this == that);
+        }
+
         SkGammaNamed             fNamed;
         float                    fValue;
         Table                    fTable;
@@ -148,11 +152,13 @@ public:
      *  Returns nullptr if color gamut cannot be described in terms of XYZ D50.
      */
     virtual const SkMatrix44* fromXYZD50() const = 0;
-    
+
     virtual bool onGammaCloseToSRGB() const = 0;
-    
+
     virtual bool onGammaIsLinear() const = 0;
-    
+
+    virtual bool onIsNumericalTransferFn(SkColorSpaceTransferFn* coeffs) const = 0;
+
     /**
      *  Returns a color space with the same gamut as this one, but with a linear gamma.
      *  For color spaces whose gamut can not be described in terms of XYZ D50, returns
index b91789614fc1ff8bf6e3bf0f5389abdeb3367219..30f70b6e265556df29454637a001362ffe5204d9 100644 (file)
@@ -5,8 +5,9 @@
  * found in the LICENSE file.
  */
 
-#include "SkColorSpace_XYZ.h"
 #include "SkChecksum.h"
+#include "SkColorSpace_XYZ.h"
+#include "SkColorSpacePriv.h"
 #include "SkColorSpaceXform_Base.h"
 
 static constexpr float gSRGB_toXYZD50[] {
@@ -64,6 +65,29 @@ bool SkColorSpace_XYZ::onGammaIsLinear() const {
     return kLinear_SkGammaNamed == fGammaNamed;
 }
 
+bool SkColorSpace_XYZ::onIsNumericalTransferFn(SkColorSpaceTransferFn* coeffs) const {
+    if (named_to_parametric(coeffs, fGammaNamed)) {
+        return true;
+    }
+
+    SkASSERT(fGammas);
+    if (fGammas->data(0) != fGammas->data(1) || fGammas->data(0) != fGammas->data(2)) {
+        return false;
+    }
+
+    if (fGammas->isValue(0)) {
+        value_to_parametric(coeffs, fGammas->data(0).fValue);
+        return true;
+    }
+
+    if (fGammas->isParametric(0)) {
+        *coeffs = fGammas->params(0);
+        return true;
+    }
+
+    return false;
+}
+
 sk_sp<SkColorSpace> SkColorSpace_XYZ::makeLinearGamma() {
     if (this->gammaIsLinear()) {
         return sk_ref_sp(this);
index 4a32188d534c9071933652dccfa6c00d0b861212..d0ce0f813e37b52c75df466d6f8490eaed37118b 100644 (file)
@@ -23,6 +23,8 @@ public:
 
     bool onGammaIsLinear() const override;
 
+    bool onIsNumericalTransferFn(SkColorSpaceTransferFn* coeffs) const override;
+
     Type type() const override { return Type::kXYZ; }
 
     sk_sp<SkColorSpace> makeLinearGamma() override;
diff --git a/src/core/SkICC.cpp b/src/core/SkICC.cpp
new file mode 100644 (file)
index 0000000..9a1a3c6
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkColorSpace_Base.h"
+#include "SkICC.h"
+
+SkICC::SkICC(sk_sp<SkColorSpace> colorSpace)
+    : fColorSpace(std::move(colorSpace))
+{}
+
+sk_sp<SkICC> SkICC::Make(const void* ptr, size_t len) {
+    sk_sp<SkColorSpace> colorSpace = SkColorSpace::MakeICC(ptr, len);
+    if (!colorSpace) {
+        return nullptr;
+    }
+
+    return sk_sp<SkICC>(new SkICC(std::move(colorSpace)));
+}
+
+bool SkICC::toXYZD50(SkMatrix44* toXYZD50) const {
+    const SkMatrix44* m = as_CSB(fColorSpace)->toXYZD50();
+    if (!m) {
+        return false;
+    }
+
+    *toXYZD50 = *m;
+    return true;
+}
+
+bool SkICC::isNumericalTransferFn(SkColorSpaceTransferFn* coeffs) const {
+    return as_CSB(fColorSpace)->onIsNumericalTransferFn(coeffs);
+}
diff --git a/tests/ICCTest.cpp b/tests/ICCTest.cpp
new file mode 100644 (file)
index 0000000..f4639f0
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "Resources.h"
+#include "SkColorSpace.h"
+#include "SkData.h"
+#include "SkICC.h"
+#include "SkMatrix44.h"
+#include "Test.h"
+
+static bool almost_equal(float a, float b) {
+    return SkTAbs(a - b) < 0.001f;
+}
+
+static inline void test_to_xyz_d50(skiatest::Reporter* r, SkICC* icc, bool shouldSucceed,
+                                   const float* reference) {
+    SkMatrix44 result;
+    REPORTER_ASSERT(r, shouldSucceed == icc->toXYZD50(&result));
+    if (shouldSucceed) {
+        float resultVals[16];
+        result.asColMajorf(resultVals);
+        for (int i = 0; i < 16; i++) {
+            REPORTER_ASSERT(r, almost_equal(resultVals[i], reference[i]));
+        }
+    }
+}
+
+DEF_TEST(ICC_ToXYZD50, r) {
+    const float z30Reference[16] = {
+        0.59825f, 0.27103f, 0.00603f, 0.0f, 0.22243f, 0.67447f, 0.07368f, 0.0f, 0.14352f, 0.05449f,
+        0.74519f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
+    };
+
+    sk_sp<SkData> data = SkData::MakeFromFileName(
+            GetResourcePath("icc_profiles/HP_ZR30w.icc").c_str());
+    sk_sp<SkICC> z30 = SkICC::Make(data->data(), data->size());
+    test_to_xyz_d50(r, z30.get(), true, z30Reference);
+
+    const float z32Reference[16] = {
+        0.61583f, 0.28789f, 0.00513f, 0.0f, 0.20428f, 0.66972f, 0.06609f, 0.0f, 0.14409f, 0.04237f,
+        0.75368f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
+    };
+
+    data = SkData::MakeFromFileName( GetResourcePath("icc_profiles/HP_Z32x.icc").c_str());
+    sk_sp<SkICC> z32 = SkICC::Make(data->data(), data->size());
+    test_to_xyz_d50(r, z32.get(), true, z32Reference);
+
+    data = SkData::MakeFromFileName(GetResourcePath("icc_profiles/upperLeft.icc").c_str());
+    sk_sp<SkICC> upperLeft = SkICC::Make(data->data(), data->size());
+    test_to_xyz_d50(r, upperLeft.get(), false, z32Reference);
+
+    data = SkData::MakeFromFileName(GetResourcePath("icc_profiles/upperRight.icc").c_str());
+    sk_sp<SkICC> upperRight = SkICC::Make(data->data(), data->size());
+    test_to_xyz_d50(r, upperRight.get(), false, z32Reference);
+}
+
+static inline void test_is_numerical_transfer_fn(skiatest::Reporter* r, SkICC* icc,
+                                                 bool shouldSucceed,
+                                                 const SkColorSpaceTransferFn& reference) {
+    SkColorSpaceTransferFn result;
+    REPORTER_ASSERT(r, shouldSucceed == icc->isNumericalTransferFn(&result));
+    if (shouldSucceed) {
+        REPORTER_ASSERT(r, 0 == memcmp(&result, &reference, sizeof(SkColorSpaceTransferFn)));
+    }
+}
+
+DEF_TEST(ICC_IsNumericalTransferFn, r) {
+    SkColorSpaceTransferFn referenceFn;
+    referenceFn.fA = 1.0f;
+    referenceFn.fB = 0.0f;
+    referenceFn.fC = 0.0f;
+    referenceFn.fD = 0.0f;
+    referenceFn.fE = 0.0f;
+    referenceFn.fF = 0.0f;
+    referenceFn.fG = 2.2f;
+
+    sk_sp<SkData> data = SkData::MakeFromFileName(
+            GetResourcePath("icc_profiles/HP_ZR30w.icc").c_str());
+    sk_sp<SkICC> z30 = SkICC::Make(data->data(), data->size());
+    test_is_numerical_transfer_fn(r, z30.get(), true, referenceFn);
+
+    data = SkData::MakeFromFileName( GetResourcePath("icc_profiles/HP_Z32x.icc").c_str());
+    sk_sp<SkICC> z32 = SkICC::Make(data->data(), data->size());
+    test_is_numerical_transfer_fn(r, z32.get(), true, referenceFn);
+
+    data = SkData::MakeFromFileName(GetResourcePath("icc_profiles/upperLeft.icc").c_str());
+    sk_sp<SkICC> upperLeft = SkICC::Make(data->data(), data->size());
+    test_is_numerical_transfer_fn(r, upperLeft.get(), false, referenceFn);
+
+    data = SkData::MakeFromFileName(GetResourcePath("icc_profiles/upperRight.icc").c_str());
+    sk_sp<SkICC> upperRight = SkICC::Make(data->data(), data->size());
+    test_is_numerical_transfer_fn(r, upperRight.get(), false, referenceFn);
+}