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