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 
17 package androidx.compose.ui.graphics.colorspace
18 
19 import androidx.annotation.Size
20 import androidx.collection.mutableIntObjectMapOf
21 import androidx.compose.ui.graphics.Color
22 import androidx.compose.ui.util.unpackFloat1
23 import androidx.compose.ui.util.unpackFloat2
24 
25 /**
26  * A connector transforms colors from a source color space to a destination color space.
27  *
28  * A source color space is connected to a destination color space using the color transform `C`
29  * computed from their respective transforms noted `Tsrc` and `Tdst` in the following equation:
30  *
31  * [See equation](https://developer.android.com/reference/android/graphics/ColorSpace.Connector)
32  *
33  * The transform `C` shown above is only valid when the source and destination color spaces have the
34  * same profile connection space (PCS). We know that instances of [ColorSpace] always use CIE XYZ as
35  * their PCS but their white points might differ. When they do, we must perform a chromatic
36  * adaptation of the color spaces' transforms. To do so, we use the von Kries method described in
37  * the documentation of [Adaptation], using the CIE standard illuminant [D50][Illuminant.D50] as the
38  * target white point.
39  *
40  * Example of conversion from [sRGB][ColorSpaces.Srgb] to [DCI-P3][ColorSpaces.DciP3]:
41  *
42  *     val connector = ColorSpaces.Srgb.connect(ColorSpaces.DciP3);
43  *     val p3 = connector.transform(1.0f, 0.0f, 0.0f);
44  *     // p3 contains { 0.9473, 0.2740, 0.2076 }
45  *
46  * @see Adaptation
47  * @see ColorSpace.adapt
48  * @see ColorSpace.connect
49  */
50 open class Connector
51 /**
52  * To connect between color spaces, we might need to use adapted transforms. This should be
53  * transparent to the user so this constructor takes the original source and destinations (returned
54  * by the getters), as well as possibly adapted color spaces used by transform().
55  */
56 internal constructor(
57     /**
58      * Returns the source color space this connector will convert from.
59      *
60      * @return A non-null instance of [ColorSpace]
61      * @see destination
62      */
63     val source: ColorSpace,
64     /**
65      * Returns the destination color space this connector will convert to.
66      *
67      * @return A non-null instance of [ColorSpace]
68      * @see source
69      */
70     val destination: ColorSpace,
71     private val transformSource: ColorSpace,
72     private val transformDestination: ColorSpace,
73     /**
74      * Returns the render intent this connector will use when mapping the source color space to the
75      * destination color space.
76      *
77      * @return A non-null [RenderIntent]
78      * @see RenderIntent
79      */
80     val renderIntent: RenderIntent,
81     private val transform: FloatArray?
82 ) {
83     /**
84      * Creates a new connector between a source and a destination color space.
85      *
86      * @param source The source color space, cannot be null
87      * @param destination The destination color space, cannot be null
88      * @param intent The render intent to use when compressing gamuts
89      */
90     internal constructor(
91         source: ColorSpace,
92         destination: ColorSpace,
93         intent: RenderIntent
94     ) : this(
95         source,
96         destination,
97         if (source.model == ColorModel.Rgb) source.adapt(Illuminant.D50) else source,
98         if (destination.model == ColorModel.Rgb) {
99             destination.adapt(Illuminant.D50)
100         } else {
101             destination
102         },
103         intent,
104         computeTransform(source, destination, intent)
105     )
106 
107     /**
108      * Transforms the specified color from the source color space to a color in the destination
109      * color space. This convenience method assumes a source color model with 3 components
110      * (typically RGB). To transform from color models with more than 3 components, such as
111      * [CMYK][ColorModel.Cmyk], use [transform] instead.
112      *
113      * @param r The red component of the color to transform
114      * @param g The green component of the color to transform
115      * @param b The blue component of the color to transform
116      * @return A new array of 3 floats containing the specified color transformed from the source
117      *   space to the destination space
118      * @see transform
119      */
120     @Size(3)
transformnull121     fun transform(r: Float, g: Float, b: Float): FloatArray {
122         return transform(floatArrayOf(r, g, b))
123     }
124 
125     /**
126      * Transforms the specified color from the source color space to a color in the destination
127      * color space.
128      *
129      * @param v A non-null array of 3 floats containing the value to transform and that will hold
130      *   the result of the transform
131      * @return The [v] array passed as a parameter, containing the specified color transformed from
132      *   the source space to the destination space
133      * @see transform
134      */
135     @Size(min = 3)
transformnull136     open fun transform(@Size(min = 3) v: FloatArray): FloatArray {
137         val xyz = transformSource.toXyz(v)
138         if (transform != null) {
139             xyz[0] *= transform[0]
140             xyz[1] *= transform[1]
141             xyz[2] *= transform[2]
142         }
143         return transformDestination.fromXyz(xyz)
144     }
145 
transformToColornull146     internal open fun transformToColor(color: Color): Color {
147         val (r, g, b, a) = color
148         val packed = transformSource.toXy(r, g, b)
149         var x = unpackFloat1(packed)
150         var y = unpackFloat2(packed)
151         var z = transformSource.toZ(r, g, b)
152         if (transform != null) {
153             x *= transform[0]
154             y *= transform[1]
155             z *= transform[2]
156         }
157         return transformDestination.xyzaToColor(x, y, z, a, destination)
158     }
159 
160     /** Optimized connector for RGB->RGB conversions. */
161     internal class RgbConnector
162     internal constructor(
163         private val mSource: Rgb,
164         private val mDestination: Rgb,
165         intent: RenderIntent
166     ) : Connector(mSource, mDestination, mSource, mDestination, intent, null) {
167         private val mTransform: FloatArray
168 
169         init {
170             mTransform = computeTransform(mSource, mDestination, intent)
171         }
172 
transformnull173         override fun transform(v: FloatArray): FloatArray {
174             v[0] = mSource.eotfFunc(v[0].toDouble()).toFloat()
175             v[1] = mSource.eotfFunc(v[1].toDouble()).toFloat()
176             v[2] = mSource.eotfFunc(v[2].toDouble()).toFloat()
177             mul3x3Float3(mTransform, v)
178             v[0] = mDestination.oetfFunc(v[0].toDouble()).toFloat()
179             v[1] = mDestination.oetfFunc(v[1].toDouble()).toFloat()
180             v[2] = mDestination.oetfFunc(v[2].toDouble()).toFloat()
181             return v
182         }
183 
transformToColornull184         override fun transformToColor(color: Color): Color {
185             val (r, g, b, a) = color
186             val v0 = mSource.eotfFunc(r.toDouble()).toFloat()
187             val v1 = mSource.eotfFunc(g.toDouble()).toFloat()
188             val v2 = mSource.eotfFunc(b.toDouble()).toFloat()
189             val v01 = mul3x3Float3_0(mTransform, v0, v1, v2)
190             val v11 = mul3x3Float3_1(mTransform, v0, v1, v2)
191             val v21 = mul3x3Float3_2(mTransform, v0, v1, v2)
192             val v02 = mDestination.oetfFunc(v01.toDouble()).toFloat()
193             val v12 = mDestination.oetfFunc(v11.toDouble()).toFloat()
194             val v22 = mDestination.oetfFunc(v21.toDouble()).toFloat()
195             return Color(v02, v12, v22, a, mDestination)
196         }
197 
198         /**
199          * Computes the color transform that connects two RGB color spaces.
200          *
201          * We can only connect color spaces if they use the same profile connection space. We assume
202          * the connection space is always CIE XYZ but we maye need to perform a chromatic adaptation
203          * to match the white points. If an adaptation is needed, we use the CIE standard illuminant
204          * D50. The unmatched color space is adapted using the von Kries transform and the
205          * [Adaptation.Bradford] matrix.
206          *
207          * @param source The source color space, cannot be null
208          * @param destination The destination color space, cannot be null
209          * @param intent The render intent to use when compressing gamuts
210          * @return An array of 9 floats containing the 3x3 matrix transform
211          */
computeTransformnull212         private fun computeTransform(
213             source: Rgb,
214             destination: Rgb,
215             intent: RenderIntent
216         ): FloatArray {
217             if (compare(source.whitePoint, destination.whitePoint)) {
218                 // RGB->RGB using the PCS of both color spaces since they have the same
219                 return mul3x3(destination.inverseTransform, source.transform)
220             } else {
221                 // RGB->RGB using CIE XYZ D50 as the PCS
222                 var transform = source.transform
223                 var inverseTransform = destination.inverseTransform
224 
225                 val srcXYZ = source.whitePoint.toXyz()
226                 val dstXYZ = destination.whitePoint.toXyz()
227 
228                 if (!compare(source.whitePoint, Illuminant.D50)) {
229                     val srcAdaptation =
230                         chromaticAdaptation(
231                             Adaptation.Bradford.transform,
232                             srcXYZ,
233                             Illuminant.newD50Xyz()
234                         )
235                     transform = mul3x3(srcAdaptation, source.transform)
236                 }
237 
238                 if (!compare(destination.whitePoint, Illuminant.D50)) {
239                     val dstAdaptation =
240                         chromaticAdaptation(
241                             Adaptation.Bradford.transform,
242                             dstXYZ,
243                             Illuminant.newD50Xyz()
244                         )
245                     inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.transform))
246                 }
247 
248                 if (intent == RenderIntent.Absolute) {
249                     transform =
250                         mul3x3Diag(
251                             floatArrayOf(
252                                 srcXYZ[0] / dstXYZ[0],
253                                 srcXYZ[1] / dstXYZ[1],
254                                 srcXYZ[2] / dstXYZ[2]
255                             ),
256                             transform
257                         )
258                 }
259 
260                 return mul3x3(inverseTransform, transform)
261             }
262         }
263     }
264 
265     internal companion object {
266         /**
267          * Computes an extra transform to apply in XYZ space depending on the selected rendering
268          * intent.
269          */
computeTransformnull270         private fun computeTransform(
271             source: ColorSpace,
272             destination: ColorSpace,
273             intent: RenderIntent
274         ): FloatArray? {
275             if (intent != RenderIntent.Absolute) return null
276 
277             val srcRGB = source.model == ColorModel.Rgb
278             val dstRGB = destination.model == ColorModel.Rgb
279 
280             if (srcRGB && dstRGB) return null
281 
282             if (srcRGB || dstRGB) {
283                 val rgb = (if (srcRGB) source else destination) as Rgb
284                 val srcXYZ = if (srcRGB) rgb.whitePoint.toXyz() else Illuminant.D50Xyz
285                 val dstXYZ = if (dstRGB) rgb.whitePoint.toXyz() else Illuminant.D50Xyz
286                 return floatArrayOf(
287                     srcXYZ[0] / dstXYZ[0],
288                     srcXYZ[1] / dstXYZ[1],
289                     srcXYZ[2] / dstXYZ[2]
290                 )
291             }
292 
293             return null
294         }
295 
296         /**
297          * Returns the identity connector for a given color space.
298          *
299          * @param source The source and destination color space
300          * @return A non-null connector that does not perform any transform
301          * @see ColorSpace.connect
302          */
identitynull303         internal fun identity(source: ColorSpace): Connector {
304             return object : Connector(source, source, RenderIntent.Relative) {
305                 override fun transform(v: FloatArray): FloatArray = v
306 
307                 override fun transformToColor(color: Color): Color = color
308             }
309         }
310     }
311 }
312 
313 internal val Connectors =
314     mutableIntObjectMapOf(
315         connectorKey(ColorSpaces.Srgb.id, ColorSpaces.Srgb.id, RenderIntent.Perceptual),
316         Connector.identity(ColorSpaces.Srgb),
317         connectorKey(ColorSpaces.Srgb.id, ColorSpaces.Oklab.id, RenderIntent.Perceptual),
318         Connector(ColorSpaces.Srgb, ColorSpaces.Oklab, RenderIntent.Perceptual),
319         connectorKey(ColorSpaces.Oklab.id, ColorSpaces.Srgb.id, RenderIntent.Perceptual),
320         Connector(ColorSpaces.Oklab, ColorSpaces.Srgb, RenderIntent.Perceptual)
321     )
322 
323 // See [ColorSpace.MaxId], the id is encoded on 6 bits
324 @Suppress("NOTHING_TO_INLINE")
connectorKeynull325 internal inline fun connectorKey(src: Int, dst: Int, renderIntent: RenderIntent): Int {
326     return src or (dst shl 6) or (renderIntent.value shl 12)
327 }
328