rewrite skcms_TransferFunction_invert

This is mostly me rewriting the code and comments as a way to make sure
I understand the math.

I'm somewhat puzzled that the BenQ_GL2450 profile approximation changes
to skip the linear segment.  I think that's from how I'm now checking
inv.d > 0 for when the linear segment matters?

Change-Id: I0d6ad1093e0c709a2d15fbd509c48f2c312f4cc6
Reviewed-on: https://skia-review.googlesource.com/c/181725
Auto-Submit: Mike Klein <mtklein@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/profiles/misc/BenQ_GL2450.icc.txt b/profiles/misc/BenQ_GL2450.icc.txt
index c0a7c3a..4a9a316 100644
--- a/profiles/misc/BenQ_GL2450.icc.txt
+++ b/profiles/misc/BenQ_GL2450.icc.txt
@@ -21,13 +21,13 @@
  'bTRC' : 'curv' :   2060 : 4936
 
 rTRC : 16-bit table with 1024 entries
-  ~= : 1.998019, 1.000748, -0.0009201114, 0.08681955, 0.08699902, 9.568995e-05, 0 (Max error: 0.0222163) (D-gap: -8.0606e-07)
+  ~= : 1.993112, 1.001823, -0, 0, 0, 0, 0 (Max error: 0.0127077)
 gTRC : 16-bit table with 1024 entries
-  ~= : 1.998019, 1.000748, -0.0009201114, 0.08681955, 0.08699902, 9.568995e-05, 0 (Max error: 0.0222163) (D-gap: -8.0606e-07)
+  ~= : 1.993112, 1.001823, -0, 0, 0, 0, 0 (Max error: 0.0127077)
 bTRC : 16-bit table with 1024 entries
-  ~= : 1.998019, 1.000748, -0.0009201114, 0.08681955, 0.08699902, 9.568995e-05, 0 (Max error: 0.0222163) (D-gap: -8.0606e-07)
-Best : 1.998019, 1.000748, -0.0009201114, 0.08681955, 0.08699902, 9.568995e-05, 0 (D-gap: -8.0606e-07)
-Inv  : 0.5004959, 0.9985085, -9.554722e-05, 11.51814, 0.007553216, 0.000919424, -0 (D-gap: 9.53674e-07)
+  ~= : 1.993112, 1.001823, -0, 0, 0, 0, 0 (Max error: 0.0127077)
+Best : 1.993112, 1.001823, -0, 0, 0, 0, 0
+Inv  : 0.5017281, 0.9963722, -0, 0, 0, 0, 0
  XYZ : | 0.388397217 0.402297974 0.201400757 |
        | 0.193893433 0.741104126 0.065093994 |
        | 0.010299683 0.061203003 1.014602661 |
diff --git a/skcms.cc b/skcms.cc
index 4231527..cfc22bb 100644
--- a/skcms.cc
+++ b/skcms.cc
@@ -1403,80 +1403,62 @@
                              : powf_(tf->a * x + tf->b, tf->g) + tf->e);
 }
 
-// TODO: Adjust logic here? This still assumes that purely linear inputs will have D > 1, which
-// we never generate. It also emits inverted linear using the same formulation. Standardize on
-// G == 1 here, too?
 bool skcms_TransferFunction_invert(const skcms_TransferFunction* src, skcms_TransferFunction* dst) {
-    // Original equation is:       y = (ax + b)^g + e   for x >= d
-    //                             y = cx + f           otherwise
-    //
-    // so 1st inverse is:          (y - e)^(1/g) = ax + b
-    //                             x = ((y - e)^(1/g) - b) / a
-    //
-    // which can be re-written as: x = (1/a)(y - e)^(1/g) - b/a
-    //                             x = ((1/a)^g)^(1/g) * (y - e)^(1/g) - b/a
-    //                             x = ([(1/a)^g]y + [-((1/a)^g)e]) ^ [1/g] + [-b/a]
-    //
-    // and 2nd inverse is:         x = (y - f) / c
-    // which can be re-written as: x = [1/c]y + [-f/c]
-    //
-    // and now both can be expressed in terms of the same parametric form as the
-    // original - parameters are enclosed in square brackets.
-    skcms_TransferFunction tf_inv = { 0, 0, 0, 0, 0, 0, 0 };
-
-    // This rejects obviously malformed inputs, as well as decreasing functions
     if (!tf_is_valid(src)) {
         return false;
     }
 
-    // There are additional constraints to be invertible
-    bool has_nonlinear = (src->d <= 1);
-    bool has_linear = (src->d > 0);
+    // We're inverting this function, solving for x in terms of y.
+    //   y = (cx + f)         x < d
+    //       (ax + b)^g + e   x ≥ d
+    // The inverse of this function can be expressed in the same piecewise form.
+    skcms_TransferFunction inv = {0,0,0,0,0,0,0};
 
-    // Is the linear section not invertible?
-    if (has_linear && src->c == 0) {
+    // We'll start by finding the new threshold inv.d.
+    // In principle we should be able to find that by solving for y at x=d from either side.
+    // (If those two d values aren't the same, it's a discontinuous transfer function.)
+    float d_l =       src->c * src->d + src->f,
+          d_r = powf_(src->a * src->d + src->b, src->g) + src->e;
+    if (fabsf_(d_l - d_r) > 1/512.0f) {
         return false;
     }
+    inv.d = d_l;  // TODO(mtklein): better in practice to choose d_r?
 
-    // Is the nonlinear section not invertible?
-    if (has_nonlinear && (src->a == 0 || src->g == 0)) {
-        return false;
+    // When d=0, the linear section collapses to a point.  We leave c,d,f all zero in that case.
+    if (inv.d > 0) {
+        // Inverting the linear section is pretty straightfoward:
+        //        y       = cx + f
+        //        y - f   = cx
+        //   (1/c)y - f/c = x
+        inv.c =    1.0f/src->c;
+        inv.f = -src->f/src->c;
     }
 
-    // If both segments are present, they need to line up
-    if (has_linear && has_nonlinear) {
-        float l_at_d = src->c * src->d + src->f;
-        float n_at_d = powf_(src->a * src->d + src->b, src->g) + src->e;
-        if (fabsf_(l_at_d - n_at_d) > (1 / 512.0f)) {
-            return false;
-        }
-    }
+    // The interesting part is inverting the nonlinear section:
+    //         y                = (ax + b)^g + e.
+    //         y - e            = (ax + b)^g
+    //        (y - e)^1/g       =  ax + b
+    //        (y - e)^1/g - b   =  ax
+    //   (1/a)(y - e)^1/g - b/a =   x
+    //
+    // To make that fit our form, we need to move the (1/a) term inside the exponentiation:
+    //   let k = (1/a)^g
+    //   (1/a)( y -  e)^1/g - b/a = x
+    //        (ky - ke)^1/g - b/a = x
 
-    // Invert linear segment
-    if (has_linear) {
-        tf_inv.c = 1.0f / src->c;
-        tf_inv.f = -src->f / src->c;
-    }
+    float k = powf_(1.0f / src->a, src->g);  // TODO(mtklein): evaluate as 1 / powf(src->a, src->g)?
+    inv.g = 1.0f / src->g;
+    inv.a = k;
+    inv.b = -k * src->e;
+    inv.e = -src->b / src->a;
 
-    // Invert nonlinear segment
-    if (has_nonlinear) {
-        tf_inv.g = 1.0f / src->g;
-        tf_inv.a = powf_(1.0f / src->a, src->g);
-        tf_inv.b = -tf_inv.a * src->e;
-        tf_inv.e = -src->b / src->a;
-    }
+    // TODO(mtklein): we'd like to guarantee the edge cases more strongly:
+    //    inv(src(0)) = 0
+    //    inv(src(d)) = d
+    //    inv(src(1)) = 1
 
-    if (!has_linear) {
-        tf_inv.d = 0;
-    } else if (!has_nonlinear) {
-        // Any value larger than 1 works
-        tf_inv.d = 2.0f;
-    } else {
-        tf_inv.d = src->c * src->d + src->f;
-    }
-
-    *dst = tf_inv;
-    return true;
+    *dst = inv;
+    return tf_is_valid(dst);
 }
 
 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //