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