skcms: Add PQ and HLG types

The PQ and HLG types differ from PQish and HLGish as follows:
- always use the constants from ITU-R BT.2100 (PQish and HLGish can
  use other constants)
- include an explicit parameter for scaling by HDR reference white
  (PQish and HLGish are capable of this, but the scaling is not an
  explicit parameter)
- for HLG, include parameters for the OOTF, namely, the peak luminance
  and system gamma (absent for HLGish)
- when used with SkColorSpace, will include the OOTF (absent for HLGish)

The skcms_TransferFunction_makePQ and skcms_TransferFunction_makeHLG
functions had no callers, to repurpose them to initialize these
types.

Once all use of PQish and HLGish is removed, we will be able to remove
PQish and HLGish entirely, so that we just have these PQ and HLG.

Bug: 420956739
Change-Id: If7a3fec007cd21825c3533272b865faf91fe5331
Reviewed-on: https://skia-review.googlesource.com/c/skcms/+/1002396
Reviewed-by: Christopher Cameron <ccameron@google.com>
Commit-Queue: Christopher Cameron <ccameron@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
diff --git a/skcms.cc b/skcms.cc
index 703af72..f38209b 100644
--- a/skcms.cc
+++ b/skcms.cc
@@ -161,6 +161,16 @@
                     memcpy(hlg, &tf.a, sizeof(*hlg));
                 }
                 return skcms_TFType_HLGinvish;
+            case skcms_TFType_PQ:
+                if (tf.b != 0.f || tf.c != 0.f || tf.d != 0.f || tf.e != 0.f || tf.f != 0.f) {
+                    return skcms_TFType_Invalid;
+                }
+                return skcms_TFType_PQ;
+            case skcms_TFType_HLG:
+                if (tf.d != 0.f || tf.e != 0.f || tf.f != 0.f) {
+                    return skcms_TFType_Invalid;
+                }
+                return skcms_TFType_HLG;
         }
         return skcms_TFType_Invalid;
     }
@@ -192,6 +202,12 @@
 bool skcms_TransferFunction_isHLGish(const skcms_TransferFunction* tf) {
     return classify(*tf) == skcms_TFType_HLGish;
 }
+bool skcms_TransferFunction_isPQ(const skcms_TransferFunction* tf) {
+    return classify(*tf) == skcms_TFType_PQ;
+}
+bool skcms_TransferFunction_isHLG(const skcms_TransferFunction* tf) {
+    return classify(*tf) == skcms_TFType_HLG;
+}
 
 bool skcms_TransferFunction_makePQish(skcms_TransferFunction* tf,
                                       float A, float B, float C,
@@ -209,6 +225,28 @@
     return true;
 }
 
+void skcms_TransferFunction_makePQ(
+    skcms_TransferFunction* tf,
+    float hdr_reference_white_luminance) {
+    *tf = { TFKind_marker(skcms_TFType_PQ),
+            hdr_reference_white_luminance,
+            0.f,0.f,0.f,0.f,0.f };
+    assert(skcms_TransferFunction_isPQ(tf));
+}
+
+void skcms_TransferFunction_makeHLG(
+    skcms_TransferFunction* tf,
+    float hdr_reference_white_luminance,
+    float peak_luminance,
+    float system_gamma) {
+    *tf = { TFKind_marker(skcms_TFType_HLG),
+            hdr_reference_white_luminance,
+            peak_luminance,
+            system_gamma,
+            0.f, 0.f, 0.f };
+    assert(skcms_TransferFunction_isHLG(tf));
+}
+
 float skcms_TransferFunction_eval(const skcms_TransferFunction* tf, float x) {
     float sign = x < 0 ? -1.0f : 1.0f;
     x *= sign;
@@ -218,6 +256,13 @@
     switch (classify(*tf, &pq, &hlg)) {
         case skcms_TFType_Invalid: break;
 
+        case skcms_TFType_HLG: {
+            const float a = 0.17883277f;
+            const float b = 0.28466892f;
+            const float c = 0.55991073f;
+            return sign * (x <= 0.5f ? x*x/3.f : (expf_((x-c)/a) + b) / 12.f);
+        }
+
         case skcms_TFType_HLGish: {
             const float K = hlg.K_minus_1 + 1.0f;
             return K * sign * (x*hlg.R <= 1 ? powf_(x*hlg.R, hlg.G)
@@ -236,6 +281,16 @@
             return sign * (x < tf->d ?       tf->c * x + tf->f
                                      : powf_(tf->a * x + tf->b, tf->g) + tf->e);
 
+        case skcms_TFType_PQ: {
+            const float c1 =  107 / 128.f;
+            const float c2 = 2413 / 128.f;
+            const float c3 = 2392 / 128.f;
+            const float m1 = 1305 / 8192.f;
+            const float m2 = 2523 / 32.f;
+            const float p = powf_(x, 1.f / m2);
+            return powf_((p - c1) / (c2 - c3 * p), 1.f / m1);
+        }
+
         case skcms_TFType_PQish:
             return sign *
                    powf_((pq.A + pq.B * powf_(x, pq.C)) / (pq.D + pq.E * powf_(x, pq.C)), pq.F);
@@ -1930,6 +1985,8 @@
     TF_HLGish hlg;
     switch (classify(*src, &pq, &hlg)) {
         case skcms_TFType_Invalid: return false;
+        case skcms_TFType_PQ:      return false;
+        case skcms_TFType_HLG:     return false;
         case skcms_TFType_sRGBish: break;  // handled below
 
         case skcms_TFType_PQish:
@@ -2458,6 +2515,11 @@
 
         switch (classify(tf)) {
             case skcms_TFType_Invalid:    return noop;
+            // TODO(https://issues.skia.org/issues/420956739): Consider adding
+            // support for PQ and HLG. Generally any code that goes through this
+            // path would also want tone mapping too.
+            case skcms_TFType_PQ:         return noop;
+            case skcms_TFType_HLG:        return noop;
             case skcms_TFType_sRGBish:    return OpAndArg{op.sRGBish,   &tf};
             case skcms_TFType_PQish:      return OpAndArg{op.PQish,     &tf};
             case skcms_TFType_HLGish:     return OpAndArg{op.HLGish,    &tf};
diff --git a/src/skcms_public.h b/src/skcms_public.h
index 82ac5a2..b95e777 100644
--- a/src/skcms_public.h
+++ b/src/skcms_public.h
@@ -57,6 +57,8 @@
     skcms_TFType_PQish,
     skcms_TFType_HLGish,
     skcms_TFType_HLGinvish,
+    skcms_TFType_PQ,
+    skcms_TFType_HLG,
 } skcms_TFType;
 
 // Identify which kind of transfer function is encoded in an skcms_TransferFunction
@@ -86,21 +88,48 @@
     return skcms_TransferFunction_makeScaledHLGish(fn, 1.0f, R,G, a,b,c);
 }
 
-// PQ mapping encoded [0,1] to linear [0,1].
-static inline bool skcms_TransferFunction_makePQ(skcms_TransferFunction* tf) {
-    return skcms_TransferFunction_makePQish(tf, -107/128.0f,         1.0f,   32/2523.0f
-                                              , 2413/128.0f, -2392/128.0f, 8192/1305.0f);
-}
-// HLG mapping encoded [0,1] to linear [0,12].
-static inline bool skcms_TransferFunction_makeHLG(skcms_TransferFunction* tf) {
-    return skcms_TransferFunction_makeHLGish(tf, 2.0f, 2.0f
-                                               , 1/0.17883277f, 0.28466892f, 0.55991073f);
-}
+// The PQ transfer function. The function skcms_TransferFunction_eval will always evaluate to the
+// unit PQ EOTF, which maps [0, 1] to [0, 1], regardless of the other parameters.
+// This is stored differently from PQish transfer functions. In particular:
+//   - the constant -5 is stored in g
+//   - the hdr_reference_white_luminance parameter is stored in a
+//   - all other parameters are set to 0
+// When this is used as an SkColorSpace, the transformation to XYZD50 will be as follows:
+//   1. Apply the unit PQ EOTF to each channel
+//   2. Multiply by 10,000 nits
+//   3. Divide by hdr_reference_white_luminance nits (default is 203)
+//   4. Transform primaries to XYZD50
+SKCMS_API void skcms_TransferFunction_makePQ(
+    skcms_TransferFunction*,
+    float hdr_reference_white_luminance);
+
+// The HLG transfer function. The function skcms_TransferFunction_eval will always evaluate to the
+// HLG inverse OETF, which maps [0, 1] to [0, 1], regardless of the other parameters.
+// This is stored differently from PQish transfer functions. In particular:
+//   - the constant -6 is stored in g
+//   - the hdr_reference_white_luminance parameter is stored in a
+//   - the peak_white_luminance parameter is stored in b
+//   - the system_gamma parameter is stored in c
+//   - all other parameters are set to 0
+// When this is used as an SkColorSpace, the transformation to XYZD50 will be as follows:
+//   1. Apply the HLG inverse OETF to each channel
+//   2. Transform primaries to Rec2020
+//   3. Apply the channel-mixing HLG OOTF using system_gamma (default is 1.2)
+//   4. Multiply by peak_luminance nits (default is 1,000)
+//   5. Divide by hdr_reference_white nits (default is 203)
+//   6. Transform primaries to XYZD50
+SKCMS_API void skcms_TransferFunction_makeHLG(
+    skcms_TransferFunction*,
+    float hdr_reference_white_luminance,
+    float peak_luminance,
+    float system_gamma);
 
 // Is this an ordinary sRGB-ish transfer function, or one of the HDR forms we support?
 SKCMS_API bool skcms_TransferFunction_isSRGBish(const skcms_TransferFunction*);
 SKCMS_API bool skcms_TransferFunction_isPQish  (const skcms_TransferFunction*);
 SKCMS_API bool skcms_TransferFunction_isHLGish (const skcms_TransferFunction*);
+SKCMS_API bool skcms_TransferFunction_isPQ     (const skcms_TransferFunction*);
+SKCMS_API bool skcms_TransferFunction_isHLG    (const skcms_TransferFunction*);
 
 // Unified representation of 'curv' or 'para' tag data, or a 1D table from 'mft1' or 'mft2'
 typedef union skcms_Curve {
diff --git a/tests.c b/tests.c
index eac8692..b052b06 100644
--- a/tests.c
+++ b/tests.c
@@ -1537,7 +1537,8 @@
     {
         // This PQ function maps [0,1] to [0,1].
         skcms_TransferFunction pq;
-        expect(skcms_TransferFunction_makePQ(&pq));
+        expect(skcms_TransferFunction_makePQish(&pq, -107/128.0f,         1.0f,   32/2523.0f
+                                                   , 2413/128.0f, -2392/128.0f, 8192/1305.0f));
 
         expect(0.0000f == skcms_TransferFunction_eval(&pq, 0.0f));
         expect(1.0000f == skcms_TransferFunction_eval(&pq, 1.0f));
@@ -1599,7 +1600,8 @@
 
 static void test_HLG(void) {
     skcms_TransferFunction enc, dec;
-    expect(skcms_TransferFunction_makeHLG(&dec));
+    expect(skcms_TransferFunction_makeHLGish(&dec, 2.0f, 2.0f
+                                                 , 1/0.17883277f, 0.28466892f, 0.55991073f));
     expect(skcms_TransferFunction_invert(&dec, &enc));
 
     // Spot check the lower half of the curve.
@@ -1740,7 +1742,8 @@
 static void test_PQ_invert(void) {
     skcms_TransferFunction pqA, invA, invB;
 
-    expect(skcms_TransferFunction_makePQ(&pqA));
+    expect(skcms_TransferFunction_makePQish(&pqA, -107/128.0f,         1.0f,   32/2523.0f
+                                                , 2413/128.0f, -2392/128.0f, 8192/1305.0f));
     // PQ's inverse is actually also PQish, so we can write out its expected value here.
     expect(skcms_TransferFunction_makePQish(&invA, 107/128.0f, 2413/128.0f, 1305/8192.0f
                                                  ,       1.0f, 2392/128.0f, 2523/  32.0f));
@@ -1788,7 +1791,8 @@
 static void test_HLG_invert(void) {
     skcms_TransferFunction hlg, inv;
 
-    expect(skcms_TransferFunction_makeHLG(&hlg));
+    expect(skcms_TransferFunction_makeHLGish(&hlg, 2.0f, 2.0f
+                                                 , 1/0.17883277f, 0.28466892f, 0.55991073f));
     // Unlike PQ, we can't create HLG's inverse directly, only via _invert().
     expect(skcms_TransferFunction_invert(&hlg, &inv));
 
@@ -1813,6 +1817,85 @@
     expect(skcms_AreApproximateInverses(&inv_curve, &hlg));
 }
 
+static void test_PQ_v2(void) {
+    const float signal_100_nits = 0.508078421517399f;
+    const float value_100_nits = 0.0100f;
+    const float signal_203_nits = 0.580688881041611f;
+    const float value_203_nits = 0.0203f;
+    skcms_TransferFunction tf;
+    const float tol = 0.01f;
+    float y_expected = 0.f;
+    float y_actual = 0.f;
+
+    skcms_TransferFunction_makePQ(&tf, 203.f);
+    y_expected = value_100_nits;
+    y_actual = skcms_TransferFunction_eval(&tf, signal_100_nits);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    y_expected = value_203_nits;
+    y_actual = skcms_TransferFunction_eval(&tf, signal_203_nits);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    skcms_TransferFunction_makePQ(&tf, 100.f);
+    y_expected = value_100_nits;
+    y_actual = skcms_TransferFunction_eval(&tf, signal_100_nits);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    y_expected = value_203_nits;
+    y_actual = skcms_TransferFunction_eval(&tf, signal_203_nits);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    expect(!skcms_TransferFunction_isPQish(&tf));
+    expect(skcms_TransferFunction_isPQ(&tf));
+    expect(skcms_TransferFunction_getType(&tf) == skcms_TFType_PQ);
+}
+
+static void test_HLG_v2(void) {
+    const float white_before_ootf = 0.26479718562407867f;
+    const float tol = 0.01f;
+    float y_expected = 0.f;
+    float y_actual = 0.f;
+
+    skcms_TransferFunction tf;
+    skcms_TransferFunction_makeHLG(&tf, 203.f, 1000.f, 1.2f);
+    expect(tf.a == 203.f);
+    expect(tf.b == 1000.f);
+    expect(tf.c == 1.2f);
+
+    y_expected = 1/48.f;
+    y_actual = skcms_TransferFunction_eval(&tf, 0.25f);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    y_expected = white_before_ootf;
+    y_actual = skcms_TransferFunction_eval(&tf, 0.75f);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    skcms_TransferFunction_makeHLG(&tf, 314.f, 314.f, 1.f);
+    expect(tf.a == 314.f);
+    expect(tf.b == 314.f);
+    expect(tf.c == 1.f);
+
+    y_expected = 1/48.f;
+    y_actual = skcms_TransferFunction_eval(&tf, 0.25f);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    y_expected = white_before_ootf;
+    y_actual = skcms_TransferFunction_eval(&tf, 0.75f);
+    expect(y_actual <= y_expected + tol);
+    expect(y_actual >= y_expected - tol);
+
+    expect(!skcms_TransferFunction_isHLGish(&tf));
+    expect(skcms_TransferFunction_isHLG(&tf));
+    expect(skcms_TransferFunction_getType(&tf) == skcms_TFType_HLG);
+}
+
 static void test_RGBA_8888_sRGB(void) {
     // We'll convert sRGB to Display P3 two ways and test they're equivalent.
 
@@ -2000,6 +2083,8 @@
     test_scaled_HLG();
     test_PQ_invert();
     test_HLG_invert();
+    test_PQ_v2();
+    test_HLG_v2();
     test_RGBA_8888_sRGB();
     test_ParseWithA2BPriority();
     test_B2A();