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