1 /*
2  * 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 package androidx.compose.ui.graphics.colorspace
17 
18 import androidx.annotation.IntRange
19 import androidx.annotation.Size
20 import androidx.compose.ui.graphics.Color
21 import androidx.compose.ui.util.packFloats
22 import kotlin.math.abs
23 import kotlin.math.pow
24 import kotlin.math.withSign
25 
26 /**
27  * A [ColorSpace] is used to identify a specific organization of colors. Each color space is
28  * characterized by a [color model][ColorModel] that defines how a color value is represented (for
29  * instance the [RGB][ColorModel.Rgb] color model defines a color value as a triplet of numbers).
30  *
31  * Each component of a color must fall within a valid range, specific to each color space, defined
32  * by [getMinValue] and [getMaxValue] This range is commonly `[0..1]`. While it is recommended to
33  * use values in the valid range, a color space always clamps input and output values when
34  * performing operations such as converting to a different color space.
35  *
36  * ### Using color spaces
37  *
38  * This implementation provides a pre-defined set of common color spaces described in the
39  * [ColorSpaces] object.
40  *
41  * The documentation of [ColorSpaces] provides a detailed description of the various characteristics
42  * of each available color space.
43  *
44  * ### Color space conversions
45  *
46  * To allow conversion between color spaces, this implementation uses the CIE XYZ profile connection
47  * space (PCS). Color values can be converted to and from this PCS using [toXyz] and [fromXyz].
48  *
49  * For color space with a non-RGB color model, the white point of the PCS *must be* the CIE standard
50  * illuminant D50. RGB color spaces use their native white point (D65 for [sRGB][ColorSpaces.Srgb]
51  * for instance and must undergo [chromatic adaptation][Adaptation] as necessary.
52  *
53  * Since the white point of the PCS is not defined for RGB color space, it is highly recommended to
54  * use the [connect] method to perform conversions between color spaces. A color space can be
55  * manually adapted to a specific white point using [adapt]. Please refer to the documentation of
56  * [RGB color spaces][Rgb] for more information. Several common CIE standard illuminants are
57  * provided in this class as reference (see [Illuminant.D65] or [Illuminant.D50] for instance).
58  *
59  * Here is an example of how to convert from a color space to another:
60  *
61  *     // Convert from DCI-P3 to Rec.2020
62  *     val connector = ColorSpaces.DciP3.connect(ColorSpaces.BT2020)
63  *     val bt2020Values = connector.transform(p3r, p3g, p3b);
64  *
65  * You can easily convert to [sRGB][ColorSpaces.Srgb] by omitting the color space parameter:
66  *
67  *     // Convert from DCI-P3 to sRGB
68  *     val connector = ColorSpaces.DciP3.connect()
69  *     val sRGBValues = connector.transform(p3r, p3g, p3b);
70  *
71  * Conversions also work between color spaces with different color models:
72  *
73  *     // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB)
74  *     val connector = ColorSpaces.CieLab.connect(ColorSpaces.Bt709)
75  *
76  * ### Color spaces and multi-threading
77  *
78  * Color spaces and other related classes ([Connector] for instance) are immutable and stateless.
79  * They can be safely used from multiple concurrent threads.
80  *
81  * @see ColorSpaces
82  * @see ColorModel
83  * @see Connector
84  * @see Adaptation
85  */
86 abstract class ColorSpace
87 internal constructor(
88     /**
89      * Returns the name of this color space. The name is never null and contains always at least 1
90      * character.
91      *
92      * Color space names are recommended to be unique but are not guaranteed to be. There is no
93      * defined format but the name usually falls in one of the following categories:
94      * * Generic names used to identify color spaces in non-RGB color models. For instance:
95      *   [Generic L*a*b*][ColorSpaces.CieLab].
96      * * Names tied to a particular specification. For instance:
97      *   [sRGB IEC61966-2.1][ColorSpaces.Srgb] or [SMPTE ST 2065-1:2012 ACES][ColorSpaces.Aces].
98      * * Ad-hoc names, often generated procedurally or by the user during a calibration workflow.
99      *   These names often contain the make and model of the display.
100      *
101      * Because the format of color space names is not defined, it is not recommended to
102      * programmatically identify a color space by its name alone. Names can be used as a first
103      * approximation.
104      *
105      * It is however perfectly acceptable to display color space names to users in a UI, or in
106      * debuggers and logs. When displaying a color space name to the user, it is recommended to add
107      * extra information to avoid ambiguities: color model, a representation of the color space's
108      * gamut, white point, etc.
109      *
110      * @return A non-null String of length >= 1
111      */
112     val name: String,
113 
114     /**
115      * The color model of this color space.
116      *
117      * @see ColorModel
118      * @see componentCount
119      */
120     val model: ColorModel,
121 
122     /**
123      * The ID of this color space. Positive IDs match the color spaces enumerated in [ColorSpaces].
124      * A negative ID indicates a color space created by calling one of the public constructors.
125      */
126     internal val id: Int
127 ) {
128     constructor(name: String, model: ColorModel) : this(name, model, MinId)
129 
130     /**
131      * Returns the number of components that form a color value according to this color space's
132      * color model.
133      *
134      * @return An integer between 1 and 4
135      * @see ColorModel
136      * @see model
137      */
138     val componentCount: Int
139         @IntRange(from = 1, to = 4) get() = model.componentCount
140 
141     /**
142      * Returns whether this color space is a wide-gamut color space. An RGB color space is
143      * wide-gamut if its gamut entirely contains the [sRGB][ColorSpaces.Srgb] gamut and if the area
144      * of its gamut is 90% of greater than the area of the [NTSC][ColorSpaces.Ntsc1953] gamut.
145      *
146      * @return True if this color space is a wide-gamut color space, false otherwise
147      */
148     abstract val isWideGamut: Boolean
149 
150     /**
151      * Indicates whether this color space is the sRGB color space or equivalent to the sRGB color
152      * space.
153      *
154      * A color space is considered sRGB if it meets all the following conditions:
155      * * Its color model is [ColorModel.Rgb]. * Its primaries are within 1e-3 of the true
156      *   [sRGB][ColorSpaces.Srgb] primaries.
157      *     * Its white point is within 1e-3 of the CIE standard illuminant [D65][Illuminant.D65].
158      * * Its opto-electronic transfer function is not linear.
159      * * Its electro-optical transfer function is not linear.
160      * * Its transfer functions yield values within 1e-3 of [ColorSpaces.Srgb].
161      * * Its range is `[0..1]`.
162      *
163      * This method always returns true for [ColorSpaces.Srgb].
164      *
165      * @return True if this color space is the sRGB color space (or a close approximation), false
166      *   otherwise
167      */
168     open val isSrgb: Boolean
169         get() = false
170 
171     init { // ColorSpace init
172         if (name.isEmpty()) {
173             throw IllegalArgumentException(
174                 "The name of a color space cannot be null and " +
175                     "must contain at least 1 character"
176             )
177         }
178 
179         if (id < MinId || id > MaxId) {
180             throw IllegalArgumentException("The id must be between $MinId and $MaxId")
181         }
182     }
183 
184     /**
185      * Returns the minimum valid value for the specified component of this color space's color
186      * model.
187      *
188      * @param component The index of the component, from `0` to `3`, inclusive.
189      * @return A floating point value less than [getMaxValue]
190      * @see getMaxValue
191      * @see ColorModel.componentCount
192      */
getMinValuenull193     abstract fun getMinValue(@IntRange(from = 0, to = 3) component: Int): Float
194 
195     /**
196      * Returns the maximum valid value for the specified component of this color space's color
197      * model.
198      *
199      * @param component The index of the component, from `0` to `3`, inclusive
200      * @return A floating point value greater than [getMinValue]
201      * @see getMinValue
202      * @see ColorModel.componentCount
203      */
204     abstract fun getMaxValue(@IntRange(from = 0, to = 3) component: Int): Float
205 
206     /**
207      * Converts a color value from this color space's model to tristimulus CIE XYZ values. If the
208      * color model of this color space is not [RGB][ColorModel.Rgb], it is assumed that the target
209      * CIE XYZ space uses a [D50][Illuminant.D50] standard illuminant.
210      *
211      * This method is a convenience for color spaces with a model of 3 components
212      * ([RGB][ColorModel.Rgb] or [ColorModel.Lab] for instance). With color spaces using fewer or
213      * more components, use [toXyz] instead.
214      *
215      * @param r The first component of the value to convert from (typically R in RGB)
216      * @param g The second component of the value to convert from (typically G in RGB)
217      * @param b The third component of the value to convert from (typically B in RGB)
218      * @return A new array of 3 floats, containing tristimulus XYZ values
219      * @see toXyz
220      * @see fromXyz
221      */
222     @Size(3)
223     fun toXyz(r: Float, g: Float, b: Float): FloatArray {
224         return toXyz(floatArrayOf(r, g, b))
225     }
226 
227     /**
228      * Converts a color value from this color space's model to tristimulus CIE XYZ values. If the
229      * color model of this color space is not [RGB][ColorModel.Rgb], it is assumed that the target
230      * CIE XYZ space uses a [D50][Illuminant.D50] standard illuminant.
231      *
232      * The specified array's length must be at least equal to to the number of color components as
233      * returned by [ColorModel.componentCount].
234      *
235      * @param v An array of color components containing the color space's color value to convert to
236      *   XYZ, and large enough to hold the resulting tristimulus XYZ values, at least 3 values.
237      * @return The array passed in parameter [v].
238      * @see toXyz
239      * @see fromXyz
240      */
toXyznull241     @Size(min = 3) abstract fun toXyz(@Size(min = 3) v: FloatArray): FloatArray
242 
243     /** Same as [toXyz], but returns only the x and y components packed into a long. */
244     internal open fun toXy(v0: Float, v1: Float, v2: Float): Long {
245         val xyz = toXyz(v0, v1, v2)
246         return packFloats(xyz[0], xyz[1])
247     }
248 
249     /** Same as [toXyz], but returns only the z component. */
toZnull250     internal open fun toZ(v0: Float, v1: Float, v2: Float): Float {
251         val xyz = toXyz(v0, v1, v2)
252         return xyz[2]
253     }
254 
255     /**
256      * Converts [x], [y], [z] components to this ColorSpace and returns a color with the resulting
257      * values, using [a] for alpha, and [colorSpace] for the color space.
258      */
xyzaToColornull259     internal open fun xyzaToColor(
260         x: Float,
261         y: Float,
262         z: Float,
263         a: Float,
264         colorSpace: ColorSpace
265     ): Color {
266         val colors = fromXyz(x, y, z)
267         return Color(colors[0], colors[1], colors[2], a, colorSpace)
268     }
269 
270     /**
271      * Converts tristimulus values from the CIE XYZ space to this color space's color model.
272      *
273      * @param x The X component of the color value
274      * @param y The Y component of the color value
275      * @param z The Z component of the color value
276      * @return A new array whose size is equal to the number of color components as returned by
277      *   [ColorModel.componentCount].
278      * @see fromXyz
279      * @see toXyz
280      */
281     @Size(min = 3)
fromXyznull282     fun fromXyz(x: Float, y: Float, z: Float): FloatArray {
283         val xyz = FloatArray(model.componentCount)
284         xyz[0] = x
285         xyz[1] = y
286         xyz[2] = z
287         return fromXyz(xyz)
288     }
289 
290     /**
291      * Converts tristimulus values from the CIE XYZ space to this color space's color model. The
292      * resulting value is passed back in the specified array.
293      *
294      * The specified array's length must be at least equal to to the number of color components as
295      * returned by [ColorModel.componentCount], and its first 3 values must be the XYZ components to
296      * convert from.
297      *
298      * @param v An array of color components containing the XYZ values to convert from, and large
299      *   enough to hold the number of components of this color space's model. The minimum size is 3,
300      *   but most color spaces have 4 components.
301      * @return The array passed in parameter [v].
302      * @see fromXyz
303      * @see toXyz
304      */
fromXyznull305     @Size(min = 3) abstract fun fromXyz(@Size(min = 3) v: FloatArray): FloatArray
306 
307     /**
308      * Returns a string representation of the object. This method returns a string equal to the
309      * value of:
310      *
311      *     "$name "(id=$id, model=$model)"
312      *
313      * For instance, the string representation of the [sRGB][ColorSpaces.Srgb] color space is equal
314      * to the following value:
315      *
316      *     sRGB IEC61966-2.1 (id=0, model=RGB)
317      *
318      * @return A string representation of the object
319      */
320     override fun toString(): String {
321         return "$name (id=$id, model=$model)"
322     }
323 
equalsnull324     override fun equals(other: Any?): Boolean {
325         if (this === other) {
326             return true
327         }
328 
329         if (other == null || this::class != other::class) {
330             return false
331         }
332 
333         val that = other as ColorSpace
334 
335         if (id != that.id) return false
336 
337         return if (name != that.name) false else model == that.model
338     }
339 
hashCodenull340     override fun hashCode(): Int {
341         var result = name.hashCode()
342         result = 31 * result + model.hashCode()
343         result = 31 * result + id
344         return result
345     }
346 
347     internal companion object { // ColorSpace companion object
348 
349         /**
350          * The minimum ID value a color space can have.
351          *
352          * @see id
353          */
354         internal const val MinId = -1 // Do not change
355 
356         /**
357          * The maximum ID value a color space can have.
358          *
359          * @see id
360          */
361         internal const val MaxId = 63 // Do not change, used to encode in longs
362     }
363 }
364 
createConnectornull365 private fun createConnector(
366     source: ColorSpace,
367     destination: ColorSpace,
368     intent: RenderIntent
369 ): Connector {
370     return if (source === destination) {
371         Connector.identity(source)
372     } else if (source.model == ColorModel.Rgb && destination.model == ColorModel.Rgb) {
373         Connector.RgbConnector(source as Rgb, destination as Rgb, intent)
374     } else {
375         Connector(source, destination, intent)
376     }
377 }
378 
379 /**
380  * Connects two color spaces to allow conversion from the source color space to the destination
381  * color space. If the source and destination color spaces do not have the same profile connection
382  * space (CIE XYZ with the same white point), they are chromatically adapted to use the CIE standard
383  * illuminant [D50][Illuminant.D50] as needed.
384  *
385  * If the source and destination are the same, an optimized connector is returned to avoid
386  * unnecessary computations and loss of precision.
387  *
388  * @param destination The color space to convert colors to
389  * @param intent The render intent to map colors from the source to the destination
390  * @return A non-null connector between the two specified color spaces
391  */
ColorSpacenull392 fun ColorSpace.connect(
393     destination: ColorSpace = ColorSpaces.Srgb,
394     intent: RenderIntent = RenderIntent.Perceptual
395 ): Connector {
396     val srcId = id
397     val dstId = destination.id
398     return if ((srcId or dstId) < 0) { // User-supplied color spaces, don't cache
399         createConnector(this, destination, intent)
400     } else {
401         Connectors.getOrPut(connectorKey(srcId, dstId, intent)) {
402             createConnector(this, destination, intent)
403         }
404     }
405 }
406 
407 /**
408  * Performs the chromatic adaptation of a color space from its native white point to the specified
409  * white point. If the specified color space does not have an [RGB][ColorModel.Rgb] color model, or
410  * if the color space already has the target white point, the color space is returned unmodified.
411  *
412  * The chromatic adaptation is performed using the von Kries method described in the documentation
413  * of [Adaptation].
414  *
415  * @param whitePoint The new white point
416  * @param adaptation The adaptation matrix
417  * @return A new color space if the specified color space has an RGB model and a white point
418  *   different from the specified white point; the specified color space otherwise
419  * @see Adaptation
420  */
421 @kotlin.jvm.JvmOverloads
ColorSpacenull422 fun ColorSpace.adapt(
423     whitePoint: WhitePoint,
424     adaptation: Adaptation = Adaptation.Bradford
425 ): ColorSpace {
426     if (this.model == ColorModel.Rgb) {
427         val rgb = this as Rgb
428         if (compare(rgb.whitePoint, whitePoint)) {
429             return this
430         }
431 
432         val xyz = whitePoint.toXyz()
433         val adaptationTransform =
434             chromaticAdaptation(adaptation.transform, rgb.whitePoint.toXyz(), xyz)
435         val transform = mul3x3(adaptationTransform, rgb.transform)
436 
437         return Rgb(rgb, transform, whitePoint)
438     }
439     return this
440 }
441 
442 // Reciprocal piecewise gamma response
rcpResponsenull443 internal fun rcpResponse(x: Double, a: Double, b: Double, c: Double, d: Double, g: Double): Double {
444     return if (x >= d * c) (x.pow(1.0 / g) - b) / a else x / c
445 }
446 
447 // Piecewise gamma response
responsenull448 internal fun response(x: Double, a: Double, b: Double, c: Double, d: Double, g: Double): Double {
449     return if (x >= d) (a * x + b).pow(g) else c * x
450 }
451 
452 // Reciprocal piecewise gamma response
rcpResponsenull453 internal fun rcpResponse(
454     x: Double,
455     a: Double,
456     b: Double,
457     c: Double,
458     d: Double,
459     e: Double,
460     f: Double,
461     g: Double
462 ): Double {
463     return if (x >= d * c) ((x - e).pow(1.0 / g) - b) / a else (x - f) / c
464 }
465 
466 // Piecewise gamma response
responsenull467 internal fun response(
468     x: Double,
469     a: Double,
470     b: Double,
471     c: Double,
472     d: Double,
473     e: Double,
474     f: Double,
475     g: Double
476 ): Double {
477     return if (x >= d) (a * x + b).pow(g) + e else c * x + f
478 }
479 
480 // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color
481 // spaces that allow negative values
absRcpResponsenull482 internal fun absRcpResponse(
483     x: Double,
484     a: Double,
485     b: Double,
486     c: Double,
487     d: Double,
488     g: Double
489 ): Double {
490     return rcpResponse(if (x < 0.0) -x else x, a, b, c, d, g).withSign(x)
491 }
492 
493 // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that
494 // allow negative values
absResponsenull495 internal fun absResponse(x: Double, a: Double, b: Double, c: Double, d: Double, g: Double): Double {
496     return response(if (x < 0.0) -x else x, a, b, c, d, g).withSign(x)
497 }
498 
499 /**
500  * Compares two sets of parametric transfer functions parameters with a precision of 1e-3.
501  *
502  * @param a The first set of parameters to compare
503  * @param b The second set of parameters to compare
504  * @return True if the two sets are equal, false otherwise
505  */
comparenull506 internal fun compare(a: TransferParameters, b: TransferParameters?): Boolean {
507     return (b != null &&
508         abs(a.a - b.a) < 1e-3 &&
509         abs(a.b - b.b) < 1e-3 &&
510         abs(a.c - b.c) < 1e-3 &&
511         abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF
512         abs(a.e - b.e) < 1e-3 &&
513         abs(a.f - b.f) < 1e-3 &&
514         abs(a.gamma - b.gamma) < 1e-3)
515 }
516 
517 /**
518  * Compares two WhitePoints with a precision of 1e-3.
519  *
520  * @param a The first WhitePoint to compare
521  * @param b The second WhitePoint to compare
522  * @return True if the two WhitePoints are equal, false otherwise
523  */
comparenull524 internal fun compare(a: WhitePoint, b: WhitePoint): Boolean {
525     if (a === b) return true
526     return abs(a.x - b.x) < 1e-3f && abs(a.y - b.y) < 1e-3f
527 }
528 
529 /**
530  * Compares two arrays of float with a precision of 1e-3.
531  *
532  * @param a The first array to compare
533  * @param b The second array to compare
534  * @return True if the two arrays are equal, false otherwise
535  */
comparenull536 internal fun compare(a: FloatArray, b: FloatArray): Boolean {
537     if (a === b) return true
538     for (i in a.indices) {
539         // TODO: do we need the compareTo() here? Isn't the abs sufficient?
540         if (a[i].compareTo(b[i]) != 0 && abs(a[i] - b[i]) > 1e-3f) return false
541     }
542     return true
543 }
544 
545 /**
546  * Inverts a 3x3 matrix. This method assumes the matrix is invertible.
547  *
548  * @param m A 3x3 matrix as a non-null array of 9 floats
549  * @return A new array of 9 floats containing the inverse of the input matrix
550  */
inverse3x3null551 internal fun inverse3x3(m: FloatArray): FloatArray {
552     val a = m[0]
553     val b = m[3]
554     val c = m[6]
555     val d = m[1]
556     val e = m[4]
557     val f = m[7]
558     val g = m[2]
559     val h = m[5]
560     val i = m[8]
561 
562     val xA = e * i - f * h
563     val xB = f * g - d * i
564     val xC = d * h - e * g
565 
566     val det = a * xA + b * xB + c * xC
567 
568     val inverted = FloatArray(m.size)
569     inverted[0] = xA / det
570     inverted[1] = xB / det
571     inverted[2] = xC / det
572     inverted[3] = (c * h - b * i) / det
573     inverted[4] = (a * i - c * g) / det
574     inverted[5] = (b * g - a * h) / det
575     inverted[6] = (b * f - c * e) / det
576     inverted[7] = (c * d - a * f) / det
577     inverted[8] = (a * e - b * d) / det
578     return inverted
579 }
580 
581 /**
582  * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats.
583  *
584  * @param lhs 3x3 matrix, as a non-null array of 9 floats
585  * @param rhs 3x3 matrix, as a non-null array of 9 floats
586  * @return A new array of 9 floats containing the result of the multiplication of rhs by lhs
587  */
mul3x3null588 internal fun mul3x3(lhs: FloatArray, rhs: FloatArray): FloatArray {
589     val r = FloatArray(9)
590     // Compiler hint to bypass extra bound checks
591     if (lhs.size < 9) return r
592     if (rhs.size < 9) return r
593     r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2]
594     r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2]
595     r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2]
596     r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5]
597     r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5]
598     r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5]
599     r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8]
600     r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8]
601     r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8]
602     return r
603 }
604 
605 /**
606  * Multiplies a vector of 3 components by a 3x3 matrix and stores the result in the input vector.
607  *
608  * @param lhs 3x3 matrix, as a non-null array of 9 floats
609  * @param rhs Vector of 3 components, as a non-null array of 3 floats
610  * @return The array of 3 passed as the [rhs] parameter
611  */
mul3x3Float3null612 internal fun mul3x3Float3(lhs: FloatArray, rhs: FloatArray): FloatArray {
613     // Compiler hint to bypass extra bounds checks
614     if (lhs.size < 9) return rhs
615     if (rhs.size < 3) return rhs
616 
617     val r0 = rhs[0]
618     val r1 = rhs[1]
619     val r2 = rhs[2]
620     rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2
621     rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2
622     rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2
623     return rhs
624 }
625 
626 /**
627  * Multiplies a vector of 3 components by a 3x3 matrix and returns the first element. If [lhs] does
628  * not contain at least 9 elements, returns [r0].
629  *
630  * @param lhs 3x3 matrix, as a non-null array of 9 floats
631  * @param r0: The first element of the vector
632  * @param r1: The second element of the vector
633  * @param r2: The third element of the vector
634  * @return The first element of the resulting multiplication.
635  */
636 @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
mul3x3Float3_0null637 internal inline fun mul3x3Float3_0(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
638     return lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2
639 }
640 
641 /**
642  * Multiplies a vector of 3 components by a 3x3 matrix and returns the second element. If [lhs] does
643  * not contain at least 9 elements, returns [r0].
644  *
645  * @param lhs 3x3 matrix, as a non-null array of 9 floats
646  * @param r0: The first element of the vector
647  * @param r1: The second element of the vector
648  * @param r2: The third element of the vector
649  * @return The second element of the resulting multiplication.
650  */
651 @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
mul3x3Float3_1null652 internal inline fun mul3x3Float3_1(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
653     return lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2
654 }
655 
656 /**
657  * Multiplies a vector of 3 components by a 3x3 matrix and returns the third element. If [lhs] does
658  * not contain at least 9 elements, returns [r0].
659  *
660  * @param lhs 3x3 matrix, as a non-null array of 9 floats
661  * @param r0: The first element of the vector
662  * @param r1: The second element of the vector
663  * @param r2: The third element of the vector
664  * @return The third element of the resulting multiplication.
665  */
666 @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
mul3x3Float3_2null667 internal inline fun mul3x3Float3_2(lhs: FloatArray, r0: Float, r1: Float, r2: Float): Float {
668     return lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2
669 }
670 
671 /**
672  * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats, by a 3x3 matrix
673  * represented as an array of 9 floats.
674  *
675  * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats
676  * @param rhs 3x3 matrix, as a non-null array of 9 floats
677  * @return A new array of 9 floats containing the result of the multiplication of [rhs] by [lhs].
678  */
mul3x3Diagnull679 internal fun mul3x3Diag(lhs: FloatArray, rhs: FloatArray): FloatArray {
680     return floatArrayOf(
681         lhs[0] * rhs[0],
682         lhs[1] * rhs[1],
683         lhs[2] * rhs[2],
684         lhs[0] * rhs[3],
685         lhs[1] * rhs[4],
686         lhs[2] * rhs[5],
687         lhs[0] * rhs[6],
688         lhs[1] * rhs[7],
689         lhs[2] * rhs[8]
690     )
691 }
692 
693 /**
694  * Computes the chromatic adaptation transform from the specified source white point to the
695  * specified destination white point.
696  *
697  * The transform is computed using the von Kries method, described in more details in the
698  * documentation of [Adaptation]. The [Adaptation] enum provides different matrices that can be used
699  * to perform the adaptation.
700  *
701  * @param matrix The adaptation matrix
702  * @param srcWhitePoint The white point to adapt from, *will be modified*
703  * @param dstWhitePoint The white point to adapt to, *will be modified*
704  * @return A 3x3 matrix as a non-null array of 9 floats
705  */
chromaticAdaptationnull706 internal fun chromaticAdaptation(
707     matrix: FloatArray,
708     srcWhitePoint: FloatArray,
709     dstWhitePoint: FloatArray
710 ): FloatArray {
711     val srcLMS = mul3x3Float3(matrix, srcWhitePoint)
712     val dstLMS = mul3x3Float3(matrix, dstWhitePoint)
713     // LMS is a diagonal matrix stored as a float[3]
714     val LMS = floatArrayOf(dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2])
715     return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix))
716 }
717