1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
18 
19 package androidx.compose.ui.graphics.colorspace
20 
21 import androidx.annotation.Size
22 import androidx.compose.ui.graphics.Color
23 import androidx.compose.ui.graphics.colorspace.ColorSpaces.transferHlgEotf
24 import androidx.compose.ui.graphics.colorspace.ColorSpaces.transferHlgOetf
25 import androidx.compose.ui.graphics.colorspace.ColorSpaces.transferSt2048Eotf
26 import androidx.compose.ui.graphics.colorspace.ColorSpaces.transferSt2048Oetf
27 import androidx.compose.ui.util.packFloats
28 import kotlin.math.abs
29 import kotlin.math.pow
30 
31 /**
32  * An RGB color space is an additive color space using the [RGB][ColorModel.Rgb] color model (a
33  * color is therefore represented by a tuple of 3 numbers).
34  *
35  * A specific RGB color space is defined by the following properties:
36  * * Three chromaticities of the red, green and blue primaries, which define the gamut of the color
37  *   space.
38  * * A white point chromaticity that defines the stimulus to which color space values are normalized
39  *   (also just called "white").
40  * * An opto-electronic transfer function, also called opto-electronic conversion function or often,
41  *   and approximately, gamma function.
42  * * An electro-optical transfer function, also called electo-optical conversion function or often,
43  *   and approximately, gamma function.
44  * * A range of valid RGB values (most commonly `[0..1]`).
45  *
46  * The most commonly used RGB color space is [sRGB][ColorSpaces.Srgb].
47  *
48  * ### Primaries and white point chromaticities
49  *
50  * In this implementation, the chromaticity of the primaries and the white point of an RGB color
51  * space is defined in the CIE xyY color space. This color space separates the chromaticity of a
52  * color, the x and y components, and its luminance, the Y component. Since the primaries and the
53  * white point have full brightness, the Y component is assumed to be 1 and only the x and y
54  * components are needed to encode them.
55  *
56  * For convenience, this implementation also allows to define the primaries and white point in the
57  * CIE XYZ space. The tristimulus XYZ values are internally converted to xyY.
58  *
59  * [sRGB primaries and white
60  * point](https://developer.android.com/reference/android/images/graphics/colorspace_srgb.png)
61  *
62  * ### Transfer functions
63  *
64  * A transfer function is a color component conversion function, defined as a single variable,
65  * monotonic mathematical function. It is applied to each individual component of a color. They are
66  * used to perform the mapping between linear tristimulus values and non-linear electronic signal
67  * value.
68  *
69  * The *opto-electronic transfer function* (OETF or OECF) encodes tristimulus values in a scene to a
70  * non-linear electronic signal value. An OETF is often expressed as a power function with an
71  * exponent between 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).
72  *
73  * The *electro-optical transfer function* (EOTF or EOCF) decodes a non-linear electronic signal
74  * value to a tristimulus value at the display. An EOTF is often expressed as a power function with
75  * an exponent between 1.8 and 2.6.
76  *
77  * Transfer functions are used as a compression scheme. For instance, linear sRGB values would
78  * normally require 11 to 12 bits of precision to store all values that can be perceived by the
79  * human eye. When encoding sRGB values using the appropriate OETF (see [sRGB][ColorSpaces.Srgb] for
80  * an exact mathematical description of that OETF), the values can be compressed to only 8 bits
81  * precision.
82  *
83  * When manipulating RGB values, particularly sRGB values, it is safe to assume that these values
84  * have been encoded with the appropriate OETF (unless noted otherwise). Encoded values are often
85  * said to be in "gamma space". They are therefore defined in a non-linear space. This in turns
86  * means that any linear operation applied to these values is going to yield mathematically
87  * incorrect results (any linear interpolation such as gradient generation for instance, most image
88  * processing functions such as blurs, etc.).
89  *
90  * To properly process encoded RGB values you must first apply the EOTF to decode the value into
91  * linear space. After processing, the RGB value must be encoded back to non-linear ("gamma") space.
92  * Here is a formal description of the process, where `f` is the processing function to apply:
93  *
94  * [See RGB equation](https://developer.android.com/reference/android/graphics/ColorSpace.Rgb)
95  *
96  * If the transfer functions of the color space can be expressed as an ICC parametric curve as
97  * defined in ICC.1:2004-10, the numeric parameters can be retrieved from [transferParameters]. This
98  * can be useful to match color spaces for instance.
99  *
100  * Some RGB color spaces, such as [ColorSpaces.Aces] and [scRGB][ColorSpaces.LinearExtendedSrgb],
101  * are said to be linear because their transfer functions are the identity function: `f(x) = x`. If
102  * the source and/or destination are known to be linear, it is not necessary to invoke the transfer
103  * functions.
104  *
105  * ### Range
106  *
107  * Most RGB color spaces allow RGB values in the range `[0..1]`. There are however a few RGB color
108  * spaces that allow much larger ranges. For instance, [scRGB][ColorSpaces.ExtendedSrgb] is used to
109  * manipulate the range `[-0.5..7.5]` while [ACES][ColorSpaces.Aces] can be used throughout the
110  * range `[-65504, 65504]`.
111  *
112  * [Extended sRGB and its large
113  * range](https://developer.android.com/reference/android/images/graphics/colorspace_scrgb.png)
114  *
115  * ### Converting between RGB color spaces
116  *
117  * Conversion between two color spaces is achieved by using an intermediate color space called the
118  * profile connection space (PCS). The PCS used by this implementation is CIE XYZ. The conversion
119  * operation is defined as such:
120  *
121  * [See RGB equation](https://developer.android.com/reference/android/graphics/ColorSpace.Rgb)
122  *
123  * Where `Tsrc` is the [RGB to XYZ transform][getTransform] of the source color space and `Tdst^-1`
124  * the [XYZ to RGB transform][getInverseTransform] of the destination color space.
125  *
126  * Many RGB color spaces commonly used with electronic devices use the standard illuminant
127  * [D65][Illuminant.D65]. Care must be take however when converting between two RGB color spaces if
128  * their white points do not match. This can be achieved by either calling [adapt] to adapt one or
129  * both color spaces to a single common white point. This can be achieved automatically by calling
130  * [ColorSpace.connect], which also handles non-RGB color spaces.
131  *
132  * To learn more about the white point adaptation process, refer to the documentation of
133  * [Adaptation].
134  */
135 class Rgb
136 /**
137  * Creates a new RGB color space using a specified set of primaries and a specified white point.
138  *
139  * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length of
140  * the arrays depends on the chosen space:
141  * ```
142  * | Spaces | Primaries length | White point length |
143  * |--------|------------------|--------------------|
144  * | xyY    | 6                | 2                  |
145  * | XYZ    | 9                | 3                  |
146  * ```
147  *
148  * When the primaries and/or white point are specified in xyY, the Y component does not need to be
149  * specified and is assumed to be 1.0. Only the xy components are required.
150  *
151  * @param name Name of the color space, cannot be null, its length must be >= 1
152  * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
153  * @param whitePoint Reference white as a [WhitePoint]
154  * @param transform Computed transform matrix that converts from RGB to XYZ, or `null` to compute it
155  *   from `primaries` and `whitePoint`.
156  * @param oetf Opto-electronic transfer function, cannot be null
157  * @param eotf Electro-optical transfer function, cannot be null
158  * @param min The minimum valid value in this color space's RGB range
159  * @param max The maximum valid value in this color space's RGB range
160  * @param transferParameters Parameters for the transfer functions
161  * @param id ID of this color space as an integer between [ColorSpace.MinId] and [ColorSpace.MaxId]
162  * @throws IllegalArgumentException If any of the following conditions is met:
163  *     * The name is null or has a length of 0.
164  *     * The primaries array is null or has a length that is neither 6 or 9.
165  *     * The white point array is null or has a length that is neither 2 or 3.
166  *     * The OETF is null or the EOTF is null.
167  *     * The minimum valid value is >= the maximum valid value.
168  *     * The ID is not between [ColorSpace.MinId] and [ColorSpace.MaxId].
169  */
170 internal constructor(
171     name: String,
172     primaries: FloatArray,
173     val whitePoint: WhitePoint,
174     transform: FloatArray?,
175     oetf: DoubleFunction,
176     eotf: DoubleFunction,
177     private val min: Float,
178     private val max: Float,
179     /**
180      * Returns the parameters used by the [electro-optical][eotf] and [opto-electronic][oetf]
181      * transfer functions. If the transfer functions do not match the ICC parametric curves defined
182      * in ICC.1:2004-10 (section 10.15), this method returns null.
183      *
184      * See [TransferParameters] for a full description of the transfer functions.
185      *
186      * @return An instance of [TransferParameters] or null if this color space's transfer functions
187      *   do not match the equation defined in [TransferParameters]
188      */
189     val transferParameters: TransferParameters?,
190     id: Int
191 ) : ColorSpace(name, ColorModel.Rgb, id) {
192 
193     internal val primaries: FloatArray
194     internal val transform: FloatArray
195     internal val inverseTransform: FloatArray
196     internal val oetfOrig = oetf
197 
198     /**
199      * Returns the opto-electronic transfer function (OETF) of this color space. The inverse
200      * function is the electro-optical transfer function (EOTF) returned by [eotf]. These functions
201      * are defined to satisfy the following equality for x ∈ `[0..1]`:
202      *
203      *     OETF(EOTF(x) = EOTF(OETF(x)) = x
204      *
205      * For RGB colors, this function can be used to convert from linear space to "gamma space"
206      * (gamma encoded). The terms gamma space and gamma encoded are frequently used because many
207      * OETFs can be closely approximated using a simple power function of the form x^γ (the
208      * approximation of the [sRGB][ColorSpaces.Srgb] OETF uses γ = 2.2 for instance).
209      *
210      * @return A transfer function that converts from linear space to "gamma space"
211      * @see eotf
212      * @see Rgb.transferParameters
213      */
214     val oetf: (Double) -> Double = { x -> oetfOrig(x).coerceIn(min.toDouble(), max.toDouble()) }
215 
216     internal val oetfFunc: DoubleFunction = DoubleFunction { x ->
217         oetfOrig(x).coerceIn(min.toDouble(), max.toDouble())
218     }
219 
220     internal val eotfOrig = eotf
221 
222     /**
223      * Returns the electro-optical transfer function (EOTF) of this color space. The inverse
224      * function is the opto-electronic transfer function (OETF) returned by [oetf]. These functions
225      * are defined to satisfy the following equality for x in `[0..1]`:
226      *
227      *     OETF(EOTF(x) = EOTF(OETF(x)) = x
228      *
229      * For RGB colors, this function can be used to convert from "gamma space" (gamma encoded) to
230      * linear space. The terms gamma space and gamma encoded are frequently used because many EOTFs
231      * can be closely approximated using a simple power function of the form x^γ (the approximation
232      * of the [sRGB][ColorSpaces.Srgb] EOTF uses γ = 2.2 for instance).
233      *
234      * @return A transfer function that converts from "gamma space" to linear space
235      * @see oetf
236      * @see Rgb.transferParameters
237      */
238     val eotf: (Double) -> Double = { x -> eotfOrig(x.coerceIn(min.toDouble(), max.toDouble())) }
239 
240     internal val eotfFunc = DoubleFunction { x ->
241         eotfOrig(x.coerceIn(min.toDouble(), max.toDouble()))
242     }
243 
244     override val isWideGamut: Boolean
245     override val isSrgb: Boolean
246 
247     init {
248         if (primaries.size != 6 && primaries.size != 9) {
249             throw IllegalArgumentException(
250                 ("The color space's primaries must be " +
251                     "defined as an array of 6 floats in xyY or 9 floats in XYZ")
252             )
253         }
254 
255         if (min >= max) {
256             throw IllegalArgumentException(
257                 "Invalid range: min=$min, max=$max; min must be strictly < max"
258             )
259         }
260         this.primaries = xyPrimaries(primaries)
261 
262         if (transform == null) {
263             this.transform = computeXYZMatrix(this.primaries, this.whitePoint)
264         } else {
265             if (transform.size != 9) {
266                 throw IllegalArgumentException(
267                     "Transform must have 9 entries! Has ${transform.size}"
268                 )
269             }
270             this.transform = transform
271         }
272         inverseTransform = inverse3x3(this.transform)
273 
274         // A color space is wide-gamut if its area is >90% of NTSC 1953 and
275         // if it entirely contains the Color space definition in xyY
276         isWideGamut = isWideGamut(this.primaries, min, max)
277         isSrgb = isSrgb(this.primaries, this.whitePoint, oetf, eotf, min, max, id)
278     }
279 
280     /**
281      * Returns the primaries of this color space as a new array of 6 floats. The Y component is
282      * assumed to be 1 and is therefore not copied into the destination. The x and y components of
283      * the first primary are written in the array at positions 0 and 1 respectively.
284      *
285      * @return A new non-null array of 2 floats
286      * @see whitePoint
287      */
288     @Size(6) fun getPrimaries(): FloatArray = primaries.copyOf()
289 
290     /**
291      * Returns the transform of this color space as a new array. The transform is used to convert
292      * from RGB to XYZ (with the same white point as this color space). To connect color spaces, you
293      * must first [adapt][ColorSpace.adapt] them to the same white point.
294      *
295      * It is recommended to use [ColorSpace.connect] to convert between color spaces.
296      *
297      * @return A new array of 9 floats
298      * @see getInverseTransform
299      */
300     @Size(9) fun getTransform(): FloatArray = transform.copyOf()
301 
302     /**
303      * Returns the inverse transform of this color space as a new array. The inverse transform is
304      * used to convert from XYZ to RGB (with the same white point as this color space). To connect
305      * color spaces, you must first [adapt][ColorSpace.adapt] them to the same white point.
306      *
307      * It is recommended to use [ColorSpace.connect] to convert between color spaces.
308      *
309      * @return A new array of 9 floats
310      * @see getTransform
311      */
312     @Size(9) fun getInverseTransform(): FloatArray = inverseTransform.copyOf()
313 
314     /**
315      * Creates a new RGB color space using a 3x3 column-major transform matrix. The transform matrix
316      * must convert from the RGB space to the profile connection space CIE XYZ.
317      *
318      * The range of the color space is imposed to be `[0..1]`.
319      *
320      * @param name Name of the color space, cannot be null, its length must be >= 1
321      * @param toXYZ 3x3 column-major transform matrix from RGB to the profile connection space CIE
322      *   XYZ as an array of 9 floats, cannot be null
323      * @param oetf Opto-electronic transfer function, cannot be null
324      * @param eotf Electro-optical transfer function, cannot be null
325      * @throws IllegalArgumentException If any of the following conditions is met:
326      *     * The name is null or has a length of 0.
327      *     * The OETF is null or the EOTF is null.
328      *     * The minimum valid value is >= the maximum valid value.
329      */
330     constructor(
331         @Size(min = 1) name: String,
332         @Size(9) toXYZ: FloatArray,
333         oetf: (Double) -> Double,
334         eotf: (Double) -> Double
335     ) : this(
336         name,
337         computePrimaries(toXYZ),
338         computeWhitePoint(toXYZ),
339         null,
340         DoubleFunction { x -> oetf(x) },
341         DoubleFunction { x -> eotf(x) },
342         0.0f,
343         1.0f,
344         null,
345         MinId
346     )
347 
348     /**
349      * Creates a new RGB color space using a specified set of primaries and a specified white point.
350      *
351      * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length
352      * of the arrays depends on the chosen space:
353      * ```
354      * | Spaces | Primaries length | White point length |
355      * |--------|------------------|--------------------|
356      * | xyY    | 6                | 2                  |
357      * | XYZ    | 9                | 3                  |
358      * ```
359      *
360      * When the primaries and/or white point are specified in xyY, the Y component does not need to
361      * be specified and is assumed to be 1.0. Only the xy components are required.
362      *
363      * @param name Name of the color space, cannot be null, its length must be >= 1
364      * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
365      * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
366      * @param oetf Opto-electronic transfer function, cannot be null
367      * @param eotf Electro-optical transfer function, cannot be null
368      * @param min The minimum valid value in this color space's RGB range
369      * @param max The maximum valid value in this color space's RGB range
370      * @throws IllegalArgumentException If any of the following conditions is met:
371      *     * The name is null or has a length of 0.
372      *     * The primaries array is null or has a length that is neither 6 or 9.
373      *     * The white point array is null or has a length that is neither 2 or 3.
374      *     * The OETF is null or the EOTF is null.
375      *     * The minimum valid value is >= the maximum valid value.
376      */
377     constructor(
378         @Size(min = 1) name: String,
379         @Size(min = 6, max = 9) primaries: FloatArray,
380         whitePoint: WhitePoint,
381         oetf: (Double) -> Double,
382         eotf: (Double) -> Double,
383         min: Float,
384         max: Float
385     ) : this(
386         name,
387         primaries,
388         whitePoint,
389         null,
390         DoubleFunction { x -> oetf(x) },
391         DoubleFunction { x -> eotf(x) },
392         min,
393         max,
394         null,
395         MinId
396     )
397 
398     /**
399      * Creates a new RGB color space using a 3x3 column-major transform matrix. The transform matrix
400      * must convert from the RGB space to the profile connection space CIE XYZ.
401      *
402      * The range of the color space is imposed to be `[0..1]`.
403      *
404      * @param name Name of the color space, cannot be null, its length must be >= 1
405      * @param toXYZ 3x3 column-major transform matrix from RGB to the profile connection space CIE
406      *   XYZ as an array of 9 floats, cannot be null
407      * @param function Parameters for the transfer functions
408      * @throws IllegalArgumentException If any of the following conditions is met:
409      *     * The name is null or has a length of 0.
410      *     * Gamma is negative.
411      */
412     constructor(
413         @Size(min = 1) name: String,
414         @Size(9) toXYZ: FloatArray,
415         function: TransferParameters
416     ) : this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), function, MinId)
417 
418     /**
419      * Creates a new RGB color space using a specified set of primaries and a specified white point.
420      *
421      * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length
422      * of the arrays depends on the chosen space:
423      * ```
424      * | Spaces | Primaries length | White point length |
425      * |--------|------------------|--------------------|
426      * | xyY    | 6                | 2                  |
427      * | XYZ    | 9                | 3                  |
428      * ```
429      *
430      * When the primaries and/or white point are specified in xyY, the Y component does not need to
431      * be specified and is assumed to be 1.0. Only the xy components are required.
432      *
433      * @param name Name of the color space, cannot be null, its length must be >= 1
434      * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
435      * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
436      * @param function Parameters for the transfer functions
437      * @throws IllegalArgumentException If any of the following conditions is met:
438      *     * The name is null or has a length of 0.
439      *     * The primaries array is null or has a length that is neither 6 or 9.
440      *     * The white point array is null or has a length that is neither 2 or 3.
441      *     * The transfer parameters are invalid.
442      */
443     constructor(
444         @Size(min = 1) name: String,
445         @Size(min = 6, max = 9) primaries: FloatArray,
446         whitePoint: WhitePoint,
447         function: TransferParameters
448     ) : this(name, primaries, whitePoint, function, MinId)
449 
450     /**
451      * Creates a new RGB color space using a specified set of primaries and a specified white point.
452      *
453      * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length
454      * of the arrays depends on the chosen space:
455      * ```
456      * | Spaces | Primaries length | White point length |
457      * |--------|------------------|--------------------|
458      * | xyY    | 6                | 2                  |
459      * | XYZ    | 9                | 3                  |
460      * ```
461      *
462      * When the primaries and/or white point are specified in xyY, the Y component does not need to
463      * be specified and is assumed to be 1.0. Only the xy components are required.
464      *
465      * @param name Name of the color space, cannot be null, its length must be >= 1
466      * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
467      * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
468      * @param function Parameters for the transfer functions
469      * @param id ID of this color space as an integer between [ColorSpace.MinId] and
470      *   [ColorSpace.MaxId]
471      * @throws IllegalArgumentException If any of the following conditions is met:
472      *     * The name is null or has a length of 0.
473      *     * The primaries array is null or has a length that is neither 6 or 9.
474      *     * The white point array is null or has a length that is neither 2 or 3.
475      *     * The ID is not between [ColorSpace.MinId] and [ColorSpace.MaxId].
476      *     * The transfer parameters are invalid.
477      *
478      * @see get
479      */
480     internal constructor(
481         name: String,
482         primaries: FloatArray,
483         whitePoint: WhitePoint,
484         function: TransferParameters,
485         id: Int
486     ) : this(
487         name,
488         primaries,
489         whitePoint,
490         null,
491         generateOetf(function),
492         generateEotf(function),
493         0.0f,
494         1.0f,
495         function,
496         id
497     )
498 
499     /**
500      * Creates a new RGB color space using a 3x3 column-major transform matrix. The transform matrix
501      * must convert from the RGB space to the profile connection space CIE XYZ.
502      *
503      * The range of the color space is imposed to be `[0..1]`.
504      *
505      * @param name Name of the color space, cannot be null, its length must be >= 1
506      * @param toXYZ 3x3 column-major transform matrix from RGB to the profile connection space CIE
507      *   XYZ as an array of 9 floats, cannot be null
508      * @param gamma Gamma to use as the transfer function
509      * @throws IllegalArgumentException If any of the following conditions is met:
510      *     * The name is null or has a length of 0.
511      *     * Gamma is negative.
512      *
513      * @see get
514      */
515     constructor(
516         @Size(min = 1) name: String,
517         @Size(9) toXYZ: FloatArray,
518         gamma: Double
519     ) : this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MinId)
520 
521     /**
522      * Creates a new RGB color space using a specified set of primaries and a specified white point.
523      *
524      * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length
525      * of the arrays depends on the chosen space:
526      * ```
527      * | Spaces | Primaries length | White point length |
528      * |--------|------------------|--------------------|
529      * | xyY    | 6                | 2                  |
530      * | XYZ    | 9                | 3                  |
531      * ```
532      *
533      * When the primaries and/or white point are specified in xyY, the Y component does not need to
534      * be specified and is assumed to be 1.0. Only the xy components are required.
535      *
536      * @param name Name of the color space, cannot be null, its length must be >= 1
537      * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
538      * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
539      * @param gamma Gamma to use as the transfer function
540      * @throws IllegalArgumentException If any of the following conditions is met:
541      *     * The name is null or has a length of 0.
542      *     * The primaries array is null or has a length that is neither 6 or 9.
543      *     * The white point array is null or has a length that is neither 2 or 3.
544      *     * Gamma is negative.
545      *
546      * @see get
547      */
548     constructor(
549         @Size(min = 1) name: String,
550         @Size(min = 6, max = 9) primaries: FloatArray,
551         whitePoint: WhitePoint,
552         gamma: Double
553     ) : this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MinId)
554 
555     /**
556      * Creates a new RGB color space using a specified set of primaries and a specified white point.
557      *
558      * The primaries and white point can be specified in the CIE xyY space or in CIE XYZ. The length
559      * of the arrays depends on the chosen space:
560      * ```
561      * | Spaces | Primaries length | White point length |
562      * |--------|------------------|--------------------|
563      * | xyY    | 6                | 2                  |
564      * | XYZ    | 9                | 3                  |
565      * ```
566      *
567      * When the primaries and/or white point are specified in xyY, the Y component does not need to
568      * be specified and is assumed to be 1.0. Only the xy components are required.
569      *
570      * @param name Name of the color space, cannot be null, its length must be >= 1
571      * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
572      * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
573      * @param gamma Gamma to use as the transfer function
574      * @param min The minimum valid value in this color space's RGB range
575      * @param max The maximum valid value in this color space's RGB range
576      * @param id ID of this color space as an integer between [ColorSpace.MinId] and
577      *   [ColorSpace.MaxId]
578      * @throws IllegalArgumentException If any of the following conditions is met:
579      *     * The name is null or has a length of 0.
580      *     * The primaries array is null or has a length that is neither 6 or 9.
581      *     * The white point array is null or has a length that is neither 2 or 3.
582      *     * The minimum valid value is >= the maximum valid value.
583      *     * The ID is not between [ColorSpace.MinId] and [ColorSpace.MaxId].
584      *     * Gamma is negative.
585      *
586      * @see get
587      */
588     internal constructor(
589         name: String,
590         primaries: FloatArray,
591         whitePoint: WhitePoint,
592         gamma: Double,
593         min: Float,
594         max: Float,
595         id: Int
596     ) : this(
597         name,
598         primaries,
599         whitePoint,
600         null,
601         if (gamma == 1.0) DoubleIdentity
602         else DoubleFunction { x -> (if (x < 0.0) 0.0 else x).pow(1.0 / gamma) },
603         if (gamma == 1.0) DoubleIdentity
604         else DoubleFunction { x -> (if (x < 0.0) 0.0 else x).pow(gamma) },
605         min,
606         max,
607         TransferParameters(gamma, 1.0, 0.0, 0.0, 0.0),
608         id
609     )
610 
611     /**
612      * Creates a copy of the specified color space with a new transform.
613      *
614      * @param colorSpace The color space to create a copy of
615      */
616     internal constructor(
617         colorSpace: Rgb,
618         transform: FloatArray,
619         whitePoint: WhitePoint
620     ) : this(
621         colorSpace.name,
622         colorSpace.primaries,
623         whitePoint,
624         transform,
625         colorSpace.oetfOrig,
626         colorSpace.eotfOrig,
627         colorSpace.min,
628         colorSpace.max,
629         colorSpace.transferParameters,
630         MinId
631     )
632 
633     /**
634      * Copies the primaries of this color space in specified array. The Y component is assumed to be
635      * 1 and is therefore not copied into the destination. The x and y components of the first
636      * primary are written in the array at positions 0 and 1 respectively.
637      *
638      * @param primaries The destination array, cannot be null, its length must be >= 6
639      * @return [primaries] array, modified to contain the primaries of this color space.
640      * @see getPrimaries
641      */
642     @Size(min = 6)
643     fun getPrimaries(@Size(min = 6) primaries: FloatArray): FloatArray {
644         return this.primaries.copyInto(primaries)
645     }
646 
647     /**
648      * Copies the transform of this color space in specified array. The transform is used to convert
649      * from RGB to XYZ (with the same white point as this color space). To connect color spaces, you
650      * must first [adapt][ColorSpace.adapt] them to the same white point.
651      *
652      * It is recommended to use [ColorSpace.connect] to convert between color spaces.
653      *
654      * @param transform The destination array, cannot be null, its length must be >= 9
655      * @return [transform], modified to contain the transform for this color space.
656      * @see getInverseTransform
657      */
658     @Size(min = 9)
659     fun getTransform(@Size(min = 9) transform: FloatArray): FloatArray {
660         return this.transform.copyInto(transform)
661     }
662 
663     /**
664      * Copies the inverse transform of this color space in specified array. The inverse transform is
665      * used to convert from XYZ to RGB (with the same white point as this color space). To connect
666      * color spaces, you must first [adapt][ColorSpace.adapt] them to the same white point.
667      *
668      * It is recommended to use [ColorSpace.connect] to convert between color spaces.
669      *
670      * @param inverseTransform The destination array, cannot be null, its length must be >= 9
671      * @return The [inverseTransform] array passed as a parameter, modified to contain the inverse
672      *   transform of this color space.
673      * @see getTransform
674      */
675     @Size(min = 9)
676     fun getInverseTransform(@Size(min = 9) inverseTransform: FloatArray): FloatArray {
677         return this.inverseTransform.copyInto(inverseTransform)
678     }
679 
680     override fun getMinValue(component: Int): Float {
681         return min
682     }
683 
684     override fun getMaxValue(component: Int): Float {
685         return max
686     }
687 
688     /**
689      * Decodes an RGB value to linear space. This is achieved by applying this color space's
690      * electro-optical transfer function to the supplied values.
691      *
692      * Refer to the documentation of [Rgb] for more information about transfer functions and their
693      * use for encoding and decoding RGB values.
694      *
695      * @param r The red component to decode to linear space
696      * @param g The green component to decode to linear space
697      * @param b The blue component to decode to linear space
698      * @return A new array of 3 floats containing linear RGB values
699      * @see toLinear
700      * @see fromLinear
701      */
702     @Size(3)
703     fun toLinear(r: Float, g: Float, b: Float): FloatArray {
704         return toLinear(floatArrayOf(r, g, b))
705     }
706 
707     /**
708      * Decodes an RGB value to linear space. This is achieved by applying this color space's
709      * electro-optical transfer function to the first 3 values of the supplied array. The result is
710      * stored back in the input array.
711      *
712      * Refer to the documentation of [Rgb] for more information about transfer functions and their
713      * use for encoding and decoding RGB values.
714      *
715      * @param v A non-null array of non-linear RGB values, its length must be at least 3
716      * @return [v], containing linear RGB values
717      * @see toLinear
718      * @see fromLinear
719      */
720     @Size(min = 3)
721     fun toLinear(@Size(min = 3) v: FloatArray): FloatArray {
722         // Compiler hint to avoid extra bounds checks
723         if (v.size < 3) return v
724         v[0] = eotfFunc(v[0].toDouble()).toFloat()
725         v[1] = eotfFunc(v[1].toDouble()).toFloat()
726         v[2] = eotfFunc(v[2].toDouble()).toFloat()
727         return v
728     }
729 
730     /**
731      * Encodes an RGB value from linear space to this color space's "gamma space". This is achieved
732      * by applying this color space's opto-electronic transfer function to the supplied values.
733      *
734      * Refer to the documentation of [Rgb] for more information about transfer functions and their
735      * use for encoding and decoding RGB values.
736      *
737      * @param r The red component to encode from linear space
738      * @param g The green component to encode from linear space
739      * @param b The blue component to encode from linear space
740      * @return A new array of 3 floats containing non-linear RGB values
741      * @see fromLinear
742      * @see toLinear
743      */
744     @Size(3)
745     fun fromLinear(r: Float, g: Float, b: Float): FloatArray {
746         return fromLinear(floatArrayOf(r, g, b))
747     }
748 
749     /**
750      * Encodes an RGB value from linear space to this color space's "gamma space". This is achieved
751      * by applying this color space's opto-electronic transfer function to the first 3 values of the
752      * supplied array. The result is stored back in the input array.
753      *
754      * Refer to the documentation of [Rgb] for more information about transfer functions and their
755      * use for encoding and decoding RGB values.
756      *
757      * @param v A non-null array of linear RGB values, its length must be at least 3
758      * @return [v], containing non-linear RGB values
759      * @see fromLinear
760      * @see toLinear
761      */
762     @Size(min = 3)
763     fun fromLinear(@Size(min = 3) v: FloatArray): FloatArray {
764         // Compiler hint to avoid extra bounds checks
765         if (v.size < 3) return v
766         v[0] = oetfFunc(v[0].toDouble()).toFloat()
767         v[1] = oetfFunc(v[1].toDouble()).toFloat()
768         v[2] = oetfFunc(v[2].toDouble()).toFloat()
769         return v
770     }
771 
772     override fun toXyz(v: FloatArray): FloatArray {
773         // Compiler hint to avoid extra bounds checks
774         if (v.size < 3) return v
775         v[0] = eotfFunc(v[0].toDouble()).toFloat()
776         v[1] = eotfFunc(v[1].toDouble()).toFloat()
777         v[2] = eotfFunc(v[2].toDouble()).toFloat()
778         return mul3x3Float3(transform, v)
779     }
780 
781     override fun toXy(v0: Float, v1: Float, v2: Float): Long {
782         val v00 = eotfFunc(v0.toDouble()).toFloat()
783         val v10 = eotfFunc(v1.toDouble()).toFloat()
784         val v20 = eotfFunc(v2.toDouble()).toFloat()
785 
786         // Compiler hint to skip bounds checks
787         if (transform.size < 9) return 0L
788         val x = mul3x3Float3_0(transform, v00, v10, v20)
789         val y = mul3x3Float3_1(transform, v00, v10, v20)
790 
791         return packFloats(x, y)
792     }
793 
794     override fun toZ(v0: Float, v1: Float, v2: Float): Float {
795         val v00 = eotfFunc(v0.toDouble()).toFloat()
796         val v10 = eotfFunc(v1.toDouble()).toFloat()
797         val v20 = eotfFunc(v2.toDouble()).toFloat()
798 
799         val z = mul3x3Float3_2(transform, v00, v10, v20)
800 
801         return z
802     }
803 
804     override fun xyzaToColor(
805         x: Float,
806         y: Float,
807         z: Float,
808         a: Float,
809         colorSpace: ColorSpace
810     ): Color {
811         var v0 = mul3x3Float3_0(inverseTransform, x, y, z)
812         var v1 = mul3x3Float3_1(inverseTransform, x, y, z)
813         var v2 = mul3x3Float3_2(inverseTransform, x, y, z)
814 
815         v0 = oetfFunc(v0.toDouble()).toFloat()
816         v1 = oetfFunc(v1.toDouble()).toFloat()
817         v2 = oetfFunc(v2.toDouble()).toFloat()
818 
819         return Color(v0, v1, v2, a, colorSpace)
820     }
821 
822     override fun fromXyz(v: FloatArray): FloatArray {
823         mul3x3Float3(inverseTransform, v)
824         // Compiler hint to avoid extra bounds checks
825         if (v.size < 3) return v
826         v[0] = oetfFunc(v[0].toDouble()).toFloat()
827         v[1] = oetfFunc(v[1].toDouble()).toFloat()
828         v[2] = oetfFunc(v[2].toDouble()).toFloat()
829         return v
830     }
831 
832     override fun equals(other: Any?): Boolean {
833         if (this === other) return true
834         if (other == null || this::class != other::class) return false
835         if (!super.equals(other)) return false
836 
837         val rgb = other as Rgb
838 
839         if (rgb.min.compareTo(min) != 0) return false
840         if (rgb.max.compareTo(max) != 0) return false
841         if (whitePoint != rgb.whitePoint) return false
842         if (!(primaries contentEquals rgb.primaries)) return false
843         if (transferParameters != null) {
844             return transferParameters == rgb.transferParameters
845         } else if (rgb.transferParameters == null) {
846             return true
847         }
848 
849         return if (oetfOrig != rgb.oetfOrig) false else eotfOrig == rgb.eotfOrig
850     }
851 
852     override fun hashCode(): Int {
853         var result = super.hashCode()
854         result = 31 * result + whitePoint.hashCode()
855         result = 31 * result + primaries.contentHashCode()
856         result = 31 * result + (if (min != 0.0f) min.toBits() else 0)
857         result = 31 * result + (if (max != 0.0f) max.toBits() else 0)
858         result = (31 * result + (transferParameters?.hashCode() ?: 0))
859         if (transferParameters == null) {
860             result = 31 * result + oetfOrig.hashCode()
861             result = 31 * result + eotfOrig.hashCode()
862         }
863         return result
864     }
865 
866     // internal so that current.txt doesn't expose it: a 'private' companion object
867     // is marked deprecated
868     internal companion object {
869         private val DoubleIdentity = DoubleFunction { d -> d }
870 
871         /**
872          * Computes whether a color space is the sRGB color space or at least a close approximation.
873          *
874          * @param primaries The set of RGB primaries in xyY as an array of 6 floats
875          * @param whitePoint The white point in xyY as an array of 2 floats
876          * @param OETF The opto-electronic transfer function
877          * @param EOTF The electro-optical transfer function
878          * @param min The minimum value of the color space's range
879          * @param max The minimum value of the color space's range
880          * @param id The ID of the color space
881          * @return True if the color space can be considered as the sRGB color space
882          * @see isSrgb
883          */
884         private fun isSrgb(
885             primaries: FloatArray,
886             whitePoint: WhitePoint,
887             OETF: DoubleFunction,
888             EOTF: DoubleFunction,
889             min: Float,
890             max: Float,
891             id: Int
892         ): Boolean {
893             if (id == 0) return true
894             if (!compare(primaries, ColorSpaces.SrgbPrimaries)) {
895                 return false
896             }
897             if (!compare(whitePoint, Illuminant.D65)) {
898                 return false
899             }
900 
901             if (min != 0.0f) return false
902             if (max != 1.0f) return false
903 
904             // We would have already returned true if this was SRGB itself, so
905             // it is safe to reference it here.
906             val srgb = ColorSpaces.Srgb
907 
908             var x = 0.0
909             while (x <= 1.0) {
910                 if (!compare(x, OETF, srgb.oetfOrig)) return false
911                 if (!compare(x, EOTF, srgb.eotfOrig)) return false
912                 x += 1 / 255.0
913             }
914 
915             return true
916         }
917 
918         private fun compare(point: Double, a: DoubleFunction, b: DoubleFunction): Boolean {
919             val rA = a(point)
920             val rB = b(point)
921             return abs(rA - rB) <= 1e-3
922         }
923 
924         /**
925          * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form a wide
926          * color gamut. A color gamut is considered wide if its area is &gt; 90% of the area of NTSC
927          * 1953 and if it contains the sRGB color gamut entirely. If the conditions above are not
928          * met, the color space is considered as having a wide color gamut if its range is larger
929          * than [0..1].
930          *
931          * @param primaries RGB primaries in CIE xyY as an array of 6 floats
932          * @param min The minimum value of the color space's range
933          * @param max The minimum value of the color space's range
934          * @return True if the color space has a wide gamut, false otherwise
935          * @see isWideGamut
936          * @see area
937          */
938         private fun isWideGamut(primaries: FloatArray, min: Float, max: Float): Boolean {
939             return (((area(primaries) / area(ColorSpaces.Ntsc1953Primaries) > 0.9f &&
940                 contains(primaries, ColorSpaces.SrgbPrimaries))) || (min < 0.0f && max > 1.0f))
941         }
942 
943         /**
944          * Computes the area of the triangle represented by a set of RGB primaries in the CIE xyY
945          * space.
946          *
947          * If [primaries] does not contain at least 6 elements, returns 0.0.
948          *
949          * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats
950          * @return The area of the triangle
951          * @see isWideGamut
952          */
953         private fun area(primaries: FloatArray): Float {
954             // Compiler hint to remove bound checks
955             if (primaries.size < 6) return 0.0f
956             val rx = primaries[0]
957             val ry = primaries[1]
958             val gx = primaries[2]
959             val gy = primaries[3]
960             val bx = primaries[4]
961             val by = primaries[5]
962             val det = rx * gy + ry * bx + gx * by - gy * bx - ry * gx - rx * by
963             val r = 0.5f * det
964             return if (r < 0.0f) -r else r
965         }
966 
967         /**
968          * Computes the cross product of two 2D vectors.
969          *
970          * @param ax The x coordinate of the first vector
971          * @param ay The y coordinate of the first vector
972          * @param bx The x coordinate of the second vector
973          * @param by The y coordinate of the second vector
974          * @return The result of a x b
975          */
976         private inline fun cross(ax: Float, ay: Float, bx: Float, by: Float): Float {
977             return ax * by - ay * bx
978         }
979 
980         /**
981          * Decides whether a 2D triangle, identified by the 6 coordinates of its 3 vertices, is
982          * contained within another 2D triangle, also identified by the 6 coordinates of its 3
983          * vertices.
984          *
985          * In the illustration below, we want to test whether the RGB triangle is contained within
986          * the triangle XYZ formed by the 3 vertices at the "+" locations.
987          *
988          *                                     Y     .
989          *                                 .   +    .
990          *                                  .     ..
991          *                                   .   .
992          *                                    . .
993          *                                     .  G
994          *                                     *
995          *                                    * *
996          *                                  **   *
997          *                                 *      **
998          *                                *         *
999          *                              **           *
1000          *                             *              *
1001          *                            *                *
1002          *                          **                  *
1003          *                         *                     *
1004          *                        *                       **
1005          *                      **                          *   R    ...
1006          *                     *                             *  .....
1007          *                    *                         ***** ..
1008          *                  **              ************       .   +
1009          *              B  *    ************                    .   X
1010          *           ......*****                                 .
1011          *     ......    .                                        .
1012          *             ..
1013          *        +   .
1014          *      Z    .
1015          *
1016          * RGB is contained within XYZ if all the following conditions are true (with "x" the cross
1017          * product operator):
1018          * -->  -->
1019          * GR x RX >= 0
1020          * -->  -->
1021          * RX x BR >= 0
1022          * -->  -->
1023          * RG x GY >= 0
1024          * -->  -->
1025          * GY x RG >= 0
1026          * -->  -->
1027          * RB x BZ >= 0
1028          * -->  -->
1029          * BZ x GB >= 0
1030          *
1031          * @param p1 The enclosing triangle as 6 floats
1032          * @param p2 The enclosed triangle as 6 floats
1033          * @return True if the triangle p1 contains the triangle p2
1034          * @see isWideGamut
1035          */
1036         private fun contains(p1: FloatArray, p2: FloatArray): Boolean {
1037             // Translate the vertices p1 in the coordinates system
1038             // with the vertices p2 as the origin
1039             val p0 =
1040                 floatArrayOf(
1041                     p1[0] - p2[0],
1042                     p1[1] - p2[1],
1043                     p1[2] - p2[2],
1044                     p1[3] - p2[3],
1045                     p1[4] - p2[4],
1046                     p1[5] - p2[5]
1047                 )
1048             // Check the first vertex of p1
1049             if (
1050                 cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 ||
1051                     cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0
1052             ) {
1053                 return false
1054             }
1055             // Check the second vertex of p1
1056             if (
1057                 cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 ||
1058                     cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0
1059             ) {
1060                 return false
1061             }
1062             // Check the third vertex of p1
1063             return !(cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 ||
1064                 cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0)
1065         }
1066 
1067         /**
1068          * Computes the primaries of a color space identified only by its RGB->XYZ transform matrix.
1069          * This method assumes that the range of the color space is [0..1].
1070          *
1071          * @param toXYZ The color space's 3x3 transform matrix to XYZ
1072          * @return A new array of 6 floats containing the color space's primaries in CIE xyY
1073          */
1074         internal fun computePrimaries(toXYZ: FloatArray): FloatArray {
1075             val r = mul3x3Float3(toXYZ, floatArrayOf(1.0f, 0.0f, 0.0f))
1076             val g = mul3x3Float3(toXYZ, floatArrayOf(0.0f, 1.0f, 0.0f))
1077             val b = mul3x3Float3(toXYZ, floatArrayOf(0.0f, 0.0f, 1.0f))
1078 
1079             val rSum = r[0] + r[1] + r[2]
1080             val gSum = g[0] + g[1] + g[2]
1081             val bSum = b[0] + b[1] + b[2]
1082 
1083             return floatArrayOf(
1084                 r[0] / rSum,
1085                 r[1] / rSum,
1086                 g[0] / gSum,
1087                 g[1] / gSum,
1088                 b[0] / bSum,
1089                 b[1] / bSum
1090             )
1091         }
1092 
1093         /**
1094          * Computes the white point of a color space identified only by its RGB->XYZ transform
1095          * matrix. This method assumes that the range of the color space is [0..1].
1096          *
1097          * @param toXYZ The color space's 3x3 transform matrix to XYZ
1098          * @return A new array of 2 floats containing the color space's white point in CIE xyY
1099          */
1100         private fun computeWhitePoint(toXYZ: FloatArray): WhitePoint {
1101             val w = mul3x3Float3(toXYZ, floatArrayOf(1.0f, 1.0f, 1.0f))
1102             val sum = w[0] + w[1] + w[2]
1103             return WhitePoint(w[0] / sum, w[1] / sum)
1104         }
1105 
1106         /**
1107          * Converts the specified RGB primaries point to xyY if needed. The primaries can be
1108          * specified as an array of 6 floats (in CIE xyY) or 9 floats (in CIE XYZ). If no conversion
1109          * is needed, the input array is copied.
1110          *
1111          * @param primaries The primaries in xyY or XYZ, in an array of 6 floats.
1112          * @return A new array of 6 floats containing the primaries in xyY
1113          */
1114         private fun xyPrimaries(primaries: FloatArray): FloatArray {
1115             val xyPrimaries = FloatArray(6)
1116 
1117             // XYZ to xyY
1118             if (primaries.size == 9) {
1119                 var sum: Float = primaries[0] + primaries[1] + primaries[2]
1120                 xyPrimaries[0] = primaries[0] / sum
1121                 xyPrimaries[1] = primaries[1] / sum
1122 
1123                 sum = primaries[3] + primaries[4] + primaries[5]
1124                 xyPrimaries[2] = primaries[3] / sum
1125                 xyPrimaries[3] = primaries[4] / sum
1126 
1127                 sum = primaries[6] + primaries[7] + primaries[8]
1128                 xyPrimaries[4] = primaries[6] / sum
1129                 xyPrimaries[5] = primaries[7] / sum
1130             } else {
1131                 primaries.copyInto(xyPrimaries, endIndex = 6)
1132             }
1133 
1134             return xyPrimaries
1135         }
1136 
1137         /**
1138          * Computes the matrix that converts from RGB to XYZ based on RGB primaries and a white
1139          * point, both specified in the CIE xyY space. The Y component of the primaries and white
1140          * point is implied to be 1.
1141          *
1142          * @param primaries The RGB primaries in xyY, as an array of 6 floats
1143          * @param whitePoint The white point in xyY, as an array of 2 floats
1144          * @return A 3x3 matrix as a new array of 9 floats
1145          */
1146         private fun computeXYZMatrix(primaries: FloatArray, whitePoint: WhitePoint): FloatArray {
1147             val rx = primaries[0]
1148             val ry = primaries[1]
1149             val gx = primaries[2]
1150             val gy = primaries[3]
1151             val bx = primaries[4]
1152             val by = primaries[5]
1153             val wx = whitePoint.x
1154             val wy = whitePoint.y
1155 
1156             val oneRxRy = (1 - rx) / ry
1157             val oneGxGy = (1 - gx) / gy
1158             val oneBxBy = (1 - bx) / by
1159             val oneWxWy = (1 - wx) / wy
1160 
1161             val rxRy = rx / ry
1162             val gxGy = gx / gy
1163             val bxBy = bx / by
1164             val wxWy = wx / wy
1165 
1166             val byNumerator =
1167                 (oneWxWy - oneRxRy) * (gxGy - rxRy) - (wxWy - rxRy) * (oneGxGy - oneRxRy)
1168             val byDenominator =
1169                 (oneBxBy - oneRxRy) * (gxGy - rxRy) - (bxBy - rxRy) * (oneGxGy - oneRxRy)
1170             val bY = byNumerator / byDenominator
1171             val gY = (wxWy - rxRy - bY * (bxBy - rxRy)) / (gxGy - rxRy)
1172             val rY = 1f - gY - bY
1173 
1174             val rYRy = rY / ry
1175             val gYGy = gY / gy
1176             val bYBy = bY / by
1177 
1178             return floatArrayOf(
1179                 rYRy * rx,
1180                 rY,
1181                 rYRy * (1f - rx - ry),
1182                 gYGy * gx,
1183                 gY,
1184                 gYGy * (1f - gx - gy),
1185                 bYBy * bx,
1186                 bY,
1187                 bYBy * (1f - bx - by)
1188             )
1189         }
1190 
1191         private fun generateOetf(function: TransferParameters): DoubleFunction {
1192             return if (function.isHLGish) {
1193                 DoubleFunction { x: Double -> transferHlgOetf(function, x) }
1194             } else if (function.isPQish) {
1195                 DoubleFunction { x: Double -> transferSt2048Oetf(function, x) }
1196             } else {
1197                 if (function.e == 0.0 && function.f == 0.0)
1198                     DoubleFunction { x: Double ->
1199                         rcpResponse(
1200                             x,
1201                             function.a,
1202                             function.b,
1203                             function.c,
1204                             function.d,
1205                             function.gamma
1206                         )
1207                     }
1208                 else
1209                     DoubleFunction { x: Double ->
1210                         rcpResponse(
1211                             x,
1212                             function.a,
1213                             function.b,
1214                             function.c,
1215                             function.d,
1216                             function.e,
1217                             function.f,
1218                             function.gamma
1219                         )
1220                     }
1221             }
1222         }
1223 
1224         private fun generateEotf(function: TransferParameters): DoubleFunction {
1225             return if (function.isHLGish) {
1226                 DoubleFunction { x: Double -> transferHlgEotf(function, x) }
1227             } else if (function.isPQish) {
1228                 DoubleFunction { x: Double -> transferSt2048Eotf(function, x) }
1229             } else {
1230                 if (function.e == 0.0 && function.f == 0.0)
1231                     DoubleFunction { x: Double ->
1232                         response(x, function.a, function.b, function.c, function.d, function.gamma)
1233                     }
1234                 else
1235                     DoubleFunction { x: Double ->
1236                         response(
1237                             x,
1238                             function.a,
1239                             function.b,
1240                             function.c,
1241                             function.d,
1242                             function.e,
1243                             function.f,
1244                             function.gamma
1245                         )
1246                     }
1247             }
1248         }
1249     }
1250 }
1251 
1252 /**
1253  * Java's DoubleUnaryOperator isn't available until API 24, so we'll use a substitute. When we bump
1254  * minimum SDK versions, this should be removed and we should use Java's version.
1255  */
1256 internal fun interface DoubleFunction {
invokenull1257     operator fun invoke(double: Double): Double
1258 }
1259