1 /*
2  * Copyright 2021 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.core.content.res;
18 
19 import android.graphics.Color;
20 
21 import androidx.core.graphics.ColorUtils;
22 
23 import org.jspecify.annotations.NonNull;
24 
25 /**
26  * Collection of methods for transforming between color spaces.
27  *
28  * <p>Methods are named $xFrom$Y. For example, lstarFromInt() returns L* from an ARGB integer.
29  *
30  * <p>These methods, generally, convert colors between the L*a*b*, XYZ, and sRGB spaces.
31  *
32  * <p>L*a*b* is a perceptually accurate color space. This is particularly important in the L*
33  * dimension: it measures luminance and unlike lightness measures traditionally used in UI work via
34  * RGB or HSL, this luminance transitions smoothly, permitting creation of pleasing shades of a
35  * color, and more pleasing transitions between colors.
36  *
37  * <p>XYZ is commonly used as an intermediate color space for converting between one color space to
38  * another. For example, to convert RGB to L*a*b*, first RGB is converted to XYZ, then XYZ is
39  * convered to L*a*b*.
40  *
41  * <p>sRGB is a "specification originated from work in 1990s through cooperation by Hewlett-Packard
42  * and Microsoft, and it was designed to be a standard definition of RGB for the internet, which it
43  * indeed became...The standard is based on a sampling of computer monitors at the time...The whole
44  * idea of sRGB is that if everyone assumed that RGB meant the same thing, then the results would be
45  * consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of
46  * Color Psychology, 2015
47  */
48 final class CamUtils {
CamUtils()49     private CamUtils() {
50     }
51 
52     // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
53     static final float[][] XYZ_TO_CAM16RGB = {
54             {0.401288f, 0.650173f, -0.051461f},
55             {-0.250268f, 1.204414f, 0.045854f},
56             {-0.002079f, 0.048952f, 0.953127f}
57     };
58 
59     // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
60     static final float[][] CAM16RGB_TO_XYZ = {
61             {1.8620678f, -1.0112547f, 0.14918678f},
62             {0.38752654f, 0.62144744f, -0.00897398f},
63             {-0.01584150f, -0.03412294f, 1.0499644f}
64     };
65 
66     // sRGB specification has D65 whitepoint - Stokes, Anderson, Chandrasekar, Motta - A Standard
67     // Default Color Space for the Internet: sRGB, 1996
68     static final float[] WHITE_POINT_D65 = {95.047f, 100.0f, 108.883f};
69 
70     // This is a more precise sRGB to XYZ transformation matrix than traditionally
71     // used. It was derived using Schlomer's technique of transforming the xyY
72     // primaries to XYZ, then applying a correction to ensure mapping from sRGB
73     // 1, 1, 1 to the reference white point, D65.
74     static final float[][] SRGB_TO_XYZ = {
75             {0.41233894f, 0.35762063f, 0.18051042f},
76             {0.2126f, 0.7152f, 0.0722f},
77             {0.01932141f, 0.11916382f, 0.9503448f}
78     };
79 
intFromLStar(float lStar)80     static int intFromLStar(float lStar) {
81         if (lStar < 1) {
82             return 0xff000000;
83         } else if (lStar > 99) {
84             return 0xffffffff;
85         }
86 
87         // XYZ to LAB conversion routine, assume a and b are 0.
88         float fy = (lStar + 16.0f) / 116.0f;
89 
90         // fz = fx = fy because a and b are 0
91         float fz = fy;
92         float fx = fy;
93 
94         float kappa = 24389f / 27f;
95         float epsilon = 216f / 24389f;
96         boolean lExceedsEpsilonKappa = (lStar > 8.0f);
97         float yT = lExceedsEpsilonKappa ? fy * fy * fy : lStar / kappa;
98         boolean cubeExceedEpsilon = (fy * fy * fy) > epsilon;
99         float xT = cubeExceedEpsilon ? fx * fx * fx : (116f * fx - 16f) / kappa;
100         float zT = cubeExceedEpsilon ? fz * fz * fz : (116f * fx - 16f) / kappa;
101 
102         return ColorUtils.XYZToColor(xT * CamUtils.WHITE_POINT_D65[0],
103                 yT * CamUtils.WHITE_POINT_D65[1], zT * CamUtils.WHITE_POINT_D65[2]);
104     }
105 
lerp(float start, float stop, float amount)106     static float lerp(float start, float stop, float amount) {
107         return start + (stop - start) * amount;
108     }
109 
110     /** Returns L* from L*a*b*, perceptual luminance, from an ARGB integer (ColorInt). */
lStarFromInt(int argb)111     static float lStarFromInt(int argb) {
112         return lStarFromY(yFromInt(argb));
113     }
114 
lStarFromY(float y)115     static float lStarFromY(float y) {
116         y = y / 100.0f;
117         final float e = 216.f / 24389.f;
118         float yIntermediate;
119         if (y <= e) {
120             return ((24389.f / 27.f) * y);
121         } else {
122             yIntermediate = (float) Math.cbrt(y);
123         }
124         return 116.f * yIntermediate - 16.f;
125     }
126 
yFromInt(int argb)127     static float yFromInt(int argb) {
128         final float r = linearized(Color.red(argb));
129         final float g = linearized(Color.green(argb));
130         final float b = linearized(Color.blue(argb));
131         float[][] matrix = SRGB_TO_XYZ;
132         float y = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]);
133         return y;
134     }
135 
xyzFromInt(int argb, float @NonNull [] outXYZ)136     static void xyzFromInt(int argb, float @NonNull [] outXYZ) {
137         final float r = linearized(Color.red(argb));
138         final float g = linearized(Color.green(argb));
139         final float b = linearized(Color.blue(argb));
140 
141         float[][] matrix = SRGB_TO_XYZ;
142         outXYZ[0] = (r * matrix[0][0]) + (g * matrix[0][1]) + (b * matrix[0][2]);
143         outXYZ[1] = (r * matrix[1][0]) + (g * matrix[1][1]) + (b * matrix[1][2]);
144         outXYZ[2] = (r * matrix[2][0]) + (g * matrix[2][1]) + (b * matrix[2][2]);
145     }
146 
yFromLStar(float lstar)147     static float yFromLStar(float lstar) {
148         float ke = 8.0f;
149         if (lstar > ke) {
150             return (float) Math.pow(((lstar + 16.0) / 116.0), 3) * 100f;
151         } else {
152             return lstar / (24389f / 27f) * 100f;
153         }
154     }
155 
linearized(int rgbComponent)156     static float linearized(int rgbComponent) {
157         float normalized = (float) rgbComponent / 255.0f;
158 
159         if (normalized <= 0.04045f) {
160             return (normalized / 12.92f) * 100.0f;
161         } else {
162             return (float) Math.pow(((normalized + 0.055f) / 1.055f), 2.4f) * 100.0f;
163         }
164     }
165 }
166