ColorSpace.java revision 9505a6552764461c22ce48f1ac13d025d23e1579
1/* 2 * Copyright (C) 2016 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 17package android.graphics; 18 19import android.annotation.ColorInt; 20import android.annotation.IntRange; 21import android.annotation.NonNull; 22import android.annotation.Size; 23import android.annotation.Nullable; 24import android.util.Pair; 25 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.List; 29import java.util.function.DoubleUnaryOperator; 30 31/** 32 * {@usesMathJax} 33 * 34 * <p>A {@link ColorSpace} is used to identify a specific organization of colors. 35 * Each color space is characterized by a {@link Model color model} that defines 36 * how a color value is represented (for instance the {@link Model#RGB RGB} color 37 * model defines a color value as a triplet of numbers).</p> 38 * 39 * <p>Each component of a color must fall within a valid range, specific to each 40 * color space, defined by {@link #getMinValue(int)} and {@link #getMaxValue(int)} 41 * This range is commonly \([0..1]\). While it is recommended to use values in the 42 * valid range, a color space always clamps input and output values when performing 43 * operations such as converting to a different color space.</p> 44 * 45 * <h3>Using color spaces</h3> 46 * 47 * <p>This implementation provides a pre-defined set of common color spaces 48 * described in the {@link Named} enum. To obtain an instance of one of the 49 * pre-defined color spaces, simply invoke {@link #get(Named)}:</p> 50 * 51 * <pre class="prettyprint"> 52 * ColorSpace sRgb = ColorSpace.get(ColorSpace.Named.SRGB); 53 * </pre> 54 * 55 * <p>The {@link #get(Named)} method always returns the same instance for a given 56 * name. Color spaces with an {@link Model#RGB RGB} color model can be safely 57 * cast to {@link Rgb}. Doing so gives you access to more APIs to query various 58 * properties of RGB color models: color gamut primaries, transfer functions, 59 * conversions to and from linear space, etc. Please refer to {@link Rgb} for 60 * more information.</p> 61 * 62 * <p>The documentation of {@link Named} provides a detailed description of the 63 * various characteristics of each available color space.</p> 64 * 65 * <h3>Color space conversions</h3> 66 67 * <p>To allow conversion between color spaces, this implementation uses the CIE 68 * XYZ profile connection space (PCS). Color values can be converted to and from 69 * this PCS using {@link #toXyz(float[])} and {@link #fromXyz(float[])}.</p> 70 * 71 * <p>For color space with a non-RGB color model, the white point of the PCS 72 * <em>must be</em> the CIE standard illuminant D50. RGB color spaces use their 73 * native white point (D65 for {@link Named#SRGB sRGB} for instance and must 74 * undergo {@link Adaptation chromatic adaptation} as necessary.</p> 75 * 76 * <p>Since the white point of the PCS is not defined for RGB color space, it is 77 * highly recommended to use the variants of the {@link #connect(ColorSpace, ColorSpace)} 78 * method to perform conversions between color spaces. A color space can be 79 * manually adapted to a specific white point using {@link #adapt(ColorSpace, float[])}. 80 * Please refer to the documentation of {@link Rgb RGB color spaces} for more 81 * information. Several common CIE standard illuminants are provided in this 82 * class as reference (see {@link #ILLUMINANT_D65} or {@link #ILLUMINANT_D50} 83 * for instance).</p> 84 * 85 * <p>Here is an example of how to convert from a color space to another:</p> 86 * 87 * <pre class="prettyprint"> 88 * // Convert from DCI-P3 to Rec.2020 89 * ColorSpace.Connector connector = ColorSpace.connect( 90 * ColorSpace.get(ColorSpace.Named.DCI_P3), 91 * ColorSpace.get(ColorSpace.Named.BT2020)); 92 * 93 * float[] bt2020 = connector.transform(p3r, p3g, p3b); 94 * </pre> 95 * 96 * <p>You can easily convert to {@link Named#SRGB sRGB} by omitting the second 97 * parameter:</p> 98 * 99 * <pre class="prettyprint"> 100 * // Convert from DCI-P3 to sRGB 101 * ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3)); 102 * 103 * float[] sRGB = connector.transform(p3r, p3g, p3b); 104 * </pre> 105 * 106 * <p>Conversions also work between color spaces with different color models:</p> 107 * 108 * <pre class="prettyprint"> 109 * // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB) 110 * ColorSpace.Connector connector = ColorSpace.connect( 111 * ColorSpace.get(ColorSpace.Named.CIE_LAB), 112 * ColorSpace.get(ColorSpace.Named.BT709)); 113 * </pre> 114 * 115 * <h3>Color spaces and multi-threading</h3> 116 * 117 * <p>Color spaces and other related classes ({@link Connector} for instance) 118 * are immutable and stateless. They can be safely used from multiple concurrent 119 * threads.</p> 120 * 121 * <p>Public static methods provided by this class, such as {@link #get(Named)} 122 * and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be 123 * thread-safe.</p> 124 * 125 * <h3>Visualization and debugging</h3> 126 * 127 * <p>To visualize and debug color spaces, you can call {@link #createRenderer()}. 128 * The {@link Renderer} created by calling this method can be used to compare 129 * color spaces and locate specific colors on a CIE 1931 chromaticity diagram.</p> 130 * 131 * <p>The following code snippet shows how to render a bitmap that compares 132 * the color gamuts and white points of {@link Named#DCI_P3} and 133 * {@link Named#PRO_PHOTO_RGB}:</p> 134 * 135 * <pre class="prettyprint"> 136 * Bitmap bitmap = ColorSpace.createRenderer() 137 * .size(768) 138 * .clip(true) 139 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 140 * .add(ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB), 0xff097ae9) 141 * .render(); 142 * </pre> 143 * <p> 144 * <img src="{@docRoot}reference/android/images/graphics/colorspace_renderer.png" /> 145 * <figcaption style="text-align: center;">DCI-P3 vs ProPhoto RGB</figcaption> 146 * </p> 147 * 148 * <p>Please refer to the documentation of the {@link Renderer} class for more 149 * information about its options and capabilities.</p> 150 * 151 * @see #get(Named) 152 * @see Named 153 * @see Model 154 * @see Connector 155 * @see Adaptation 156 * @see Renderer 157 */ 158@SuppressWarnings("StaticInitializerReferencesSubClass") 159public abstract class ColorSpace { 160 /** 161 * Standard CIE 1931 2° illuminant A, encoded in xyY. 162 * This illuminant has a color temperature of 2856K. 163 */ 164 public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f }; 165 /** 166 * Standard CIE 1931 2° illuminant B, encoded in xyY. 167 * This illuminant has a color temperature of 4874K. 168 */ 169 public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f }; 170 /** 171 * Standard CIE 1931 2° illuminant C, encoded in xyY. 172 * This illuminant has a color temperature of 6774K. 173 */ 174 public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f }; 175 /** 176 * Standard CIE 1931 2° illuminant D50, encoded in xyY. 177 * This illuminant has a color temperature of 5003K. This illuminant 178 * is used by the profile connection space in ICC profiles. 179 */ 180 public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f }; 181 /** 182 * Standard CIE 1931 2° illuminant D55, encoded in xyY. 183 * This illuminant has a color temperature of 5503K. 184 */ 185 public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f }; 186 /** 187 * Standard CIE 1931 2° illuminant D60, encoded in xyY. 188 * This illuminant has a color temperature of 6004K. 189 */ 190 public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f }; 191 /** 192 * Standard CIE 1931 2° illuminant D65, encoded in xyY. 193 * This illuminant has a color temperature of 6504K. This illuminant 194 * is commonly used in RGB color spaces such as sRGB, BT.209, etc. 195 */ 196 public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f }; 197 /** 198 * Standard CIE 1931 2° illuminant D75, encoded in xyY. 199 * This illuminant has a color temperature of 7504K. 200 */ 201 public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f }; 202 /** 203 * Standard CIE 1931 2° illuminant E, encoded in xyY. 204 * This illuminant has a color temperature of 5454K. 205 */ 206 public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f }; 207 208 /** 209 * The minimum ID value a color space can have. 210 * 211 * @see #getId() 212 */ 213 public static final int MIN_ID = -1; // Do not change 214 /** 215 * The maximum ID value a color space can have. 216 * 217 * @see #getId() 218 */ 219 public static final int MAX_ID = 64; // Do not change, used to encode in longs 220 221 private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }; 222 private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f }; 223 private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f }; 224 225 // See static initialization block next to #get(Named) 226 private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; 227 228 @NonNull private final String mName; 229 @NonNull private final Model mModel; 230 @IntRange(from = MIN_ID, to = MAX_ID) private final int mId; 231 232 /** 233 * {@usesMathJax} 234 * 235 * <p>List of common, named color spaces. A corresponding instance of 236 * {@link ColorSpace} can be obtained by calling {@link ColorSpace#get(Named)}:</p> 237 * 238 * <pre class="prettyprint"> 239 * ColorSpace cs = ColorSpace.get(ColorSpace.Named.DCI_P3); 240 * </pre> 241 * 242 * <p>The properties of each color space are described below (see {@link #SRGB sRGB} 243 * for instance). When applicable, the color gamut of each color space is compared 244 * to the color gamut of sRGB using a CIE 1931 xy chromaticity diagram. This diagram 245 * shows the location of the color space's primaries and white point.</p> 246 * 247 * @see ColorSpace#get(Named) 248 */ 249 public enum Named { 250 // NOTE: Do NOT change the order of the enum 251 /** 252 * <p>{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.</p> 253 * <table summary="Color space definition"> 254 * <tr> 255 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 256 * </tr> 257 * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr> 258 * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr> 259 * <tr><th>Property</th><th colspan="4">Value</th></tr> 260 * <tr><td>Name</td><td colspan="4">sRGB IEC61966-2.1</td></tr> 261 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 262 * <tr> 263 * <td>Opto-electronic transfer function</td> 264 * <td colspan="4">\(\begin{equation} 265 * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \le 0.0031308 \\ 266 * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \gt 0.0031308 \end{cases} 267 * \end{equation}\) 268 * </td> 269 * </tr> 270 * <tr> 271 * <td>Electro-optical transfer function</td> 272 * <td colspan="4">\(\begin{equation} 273 * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \le 0.04045 \\ 274 * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \gt 0.04045 \end{cases} 275 * \end{equation}\) 276 * </td> 277 * </tr> 278 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 279 * </table> 280 * <p> 281 * <img src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" /> 282 * <figcaption style="text-align: center;">sRGB</figcaption> 283 * </p> 284 */ 285 SRGB, 286 /** 287 * <p>{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.</p> 288 * <table summary="Color space definition"> 289 * <tr> 290 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 291 * </tr> 292 * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr> 293 * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr> 294 * <tr><th>Property</th><th colspan="4">Value</th></tr> 295 * <tr><td>Name</td><td colspan="4">sRGB IEC61966-2.1 (Linear)</td></tr> 296 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 297 * <tr> 298 * <td>Opto-electronic transfer function</td> 299 * <td colspan="4">\(C_{sRGB} = C_{linear}\)</td> 300 * </tr> 301 * <tr> 302 * <td>Electro-optical transfer function</td> 303 * <td colspan="4">\(C_{linear} = C_{sRGB}\)</td> 304 * </tr> 305 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 306 * </table> 307 * <p> 308 * <img src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" /> 309 * <figcaption style="text-align: center;">sRGB</figcaption> 310 * </p> 311 */ 312 LINEAR_SRGB, 313 /** 314 * <p>{@link ColorSpace.Rgb RGB} color space scRGB-nl standardized as IEC 61966-2-2:2003.</p> 315 * <table summary="Color space definition"> 316 * <tr> 317 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 318 * </tr> 319 * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr> 320 * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr> 321 * <tr><th>Property</th><th colspan="4">Value</th></tr> 322 * <tr><td>Name</td><td colspan="4">scRGB-nl IEC 61966-2-2:2003</td></tr> 323 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 324 * <tr> 325 * <td>Opto-electronic transfer function</td> 326 * <td colspan="4">\(\begin{equation} 327 * C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| & 328 * \left| C_{linear} \right| \le 0.0031308 \\ 329 * sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 & 330 * \left| C_{linear} \right| \gt 0.0031308 \end{cases} 331 * \end{equation}\) 332 * </td> 333 * </tr> 334 * <tr> 335 * <td>Electro-optical transfer function</td> 336 * <td colspan="4">\(\begin{equation} 337 * C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} & 338 * \left| C_{scRGB} \right| \le 0.04045 \\ 339 * sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} & 340 * \left| C_{scRGB} \right| \gt 0.04045 \end{cases} 341 * \end{equation}\) 342 * </td> 343 * </tr> 344 * <tr><td>Range</td><td colspan="4">\([-0.5..7.5[\)</td></tr> 345 * </table> 346 * <p> 347 * <img src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" /> 348 * <figcaption style="text-align: center;">Extended RGB (orange) vs sRGB (white)</figcaption> 349 * </p> 350 */ 351 EXTENDED_SRGB, 352 /** 353 * <p>{@link ColorSpace.Rgb RGB} color space scRGB standardized as IEC 61966-2-2:2003.</p> 354 * <table summary="Color space definition"> 355 * <tr> 356 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 357 * </tr> 358 * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr> 359 * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr> 360 * <tr><th>Property</th><th colspan="4">Value</th></tr> 361 * <tr><td>Name</td><td colspan="4">scRGB IEC 61966-2-2:2003</td></tr> 362 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 363 * <tr> 364 * <td>Opto-electronic transfer function</td> 365 * <td colspan="4">\(C_{scRGB} = C_{linear}\)</td> 366 * </tr> 367 * <tr> 368 * <td>Electro-optical transfer function</td> 369 * <td colspan="4">\(C_{linear} = C_{scRGB}\)</td> 370 * </tr> 371 * <tr><td>Range</td><td colspan="4">\([-0.5..7.5[\)</td></tr> 372 * </table> 373 * <p> 374 * <img src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" /> 375 * <figcaption style="text-align: center;">Extended RGB (orange) vs sRGB (white)</figcaption> 376 * </p> 377 */ 378 LINEAR_EXTENDED_SRGB, 379 /** 380 * <p>{@link ColorSpace.Rgb RGB} color space BT.709 standardized as Rec. ITU-R BT.709-5.</p> 381 * <table summary="Color space definition"> 382 * <tr> 383 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 384 * </tr> 385 * <tr><td>x</td><td>0.640</td><td>0.300</td><td>0.150</td><td>0.3127</td></tr> 386 * <tr><td>y</td><td>0.330</td><td>0.600</td><td>0.060</td><td>0.3290</td></tr> 387 * <tr><th>Property</th><th colspan="4">Value</th></tr> 388 * <tr><td>Name</td><td colspan="4">Rec. ITU-R BT.709-5</td></tr> 389 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 390 * <tr> 391 * <td>Opto-electronic transfer function</td> 392 * <td colspan="4">\(\begin{equation} 393 * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ 394 * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} 395 * \end{equation}\) 396 * </td> 397 * </tr> 398 * <tr> 399 * <td>Electro-optical transfer function</td> 400 * <td colspan="4">\(\begin{equation} 401 * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ 402 * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} 403 * \end{equation}\) 404 * </td> 405 * </tr> 406 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 407 * </table> 408 * <p> 409 * <img src="{@docRoot}reference/android/images/graphics/colorspace_bt709.png" /> 410 * <figcaption style="text-align: center;">BT.709</figcaption> 411 * </p> 412 */ 413 BT709, 414 /** 415 * <p>{@link ColorSpace.Rgb RGB} color space BT.2020 standardized as Rec. ITU-R BT.2020-1.</p> 416 * <table summary="Color space definition"> 417 * <tr> 418 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 419 * </tr> 420 * <tr><td>x</td><td>0.708</td><td>0.170</td><td>0.131</td><td>0.3127</td></tr> 421 * <tr><td>y</td><td>0.292</td><td>0.797</td><td>0.046</td><td>0.3290</td></tr> 422 * <tr><th>Property</th><th colspan="4">Value</th></tr> 423 * <tr><td>Name</td><td colspan="4">Rec. ITU-R BT.2020-1</td></tr> 424 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 425 * <tr> 426 * <td>Opto-electronic transfer function</td> 427 * <td colspan="4">\(\begin{equation} 428 * C_{BT2020} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.0181 \\ 429 * 1.0993 \times C_{linear}^{\frac{1}{2.2}} - 0.0993 & C_{linear} \ge 0.0181 \end{cases} 430 * \end{equation}\) 431 * </td> 432 * </tr> 433 * <tr> 434 * <td>Electro-optical transfer function</td> 435 * <td colspan="4">\(\begin{equation} 436 * C_{linear} = \begin{cases}\frac{C_{BT2020}}{4.5} & C_{BT2020} \lt 0.08145 \\ 437 * \left( \frac{C_{BT2020} + 0.0993}{1.0993} \right) ^{2.2} & C_{BT2020} \ge 0.08145 \end{cases} 438 * \end{equation}\) 439 * </td> 440 * </tr> 441 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 442 * </table> 443 * <p> 444 * <img src="{@docRoot}reference/android/images/graphics/colorspace_bt2020.png" /> 445 * <figcaption style="text-align: center;">BT.2020 (orange) vs sRGB (white)</figcaption> 446 * </p> 447 */ 448 BT2020, 449 /** 450 * <p>{@link ColorSpace.Rgb RGB} color space DCI-P3 standardized as SMPTE RP 431-2-2007.</p> 451 * <table summary="Color space definition"> 452 * <tr> 453 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 454 * </tr> 455 * <tr><td>x</td><td>0.680</td><td>0.265</td><td>0.150</td><td>0.314</td></tr> 456 * <tr><td>y</td><td>0.320</td><td>0.690</td><td>0.060</td><td>0.351</td></tr> 457 * <tr><th>Property</th><th colspan="4">Value</th></tr> 458 * <tr><td>Name</td><td colspan="4">SMPTE RP 431-2-2007 DCI (P3)</td></tr> 459 * <tr><td>CIE standard illuminant</td><td colspan="4">N/A</td></tr> 460 * <tr> 461 * <td>Opto-electronic transfer function</td> 462 * <td colspan="4">\(C_{P3} = C_{linear}^{\frac{1}{2.6}}\)</td> 463 * </tr> 464 * <tr> 465 * <td>Electro-optical transfer function</td> 466 * <td colspan="4">\(C_{linear} = C_{P3}^{2.6}\)</td> 467 * </tr> 468 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 469 * </table> 470 * <p> 471 * <img src="{@docRoot}reference/android/images/graphics/colorspace_dci_p3.png" /> 472 * <figcaption style="text-align: center;">DCI-P3 (orange) vs sRGB (white)</figcaption> 473 * </p> 474 */ 475 DCI_P3, 476 /** 477 * <p>{@link ColorSpace.Rgb RGB} color space Display P3 based on SMPTE RP 431-2-2007 and IEC 61966-2.1:1999.</p> 478 * <table summary="Color space definition"> 479 * <tr> 480 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 481 * </tr> 482 * <tr><td>x</td><td>0.680</td><td>0.265</td><td>0.150</td><td>0.3127</td></tr> 483 * <tr><td>y</td><td>0.320</td><td>0.690</td><td>0.060</td><td>0.3290</td></tr> 484 * <tr><th>Property</th><th colspan="4">Value</th></tr> 485 * <tr><td>Name</td><td colspan="4">Display P3</td></tr> 486 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 487 * <tr> 488 * <td>Opto-electronic transfer function</td> 489 * <td colspan="4">\(\begin{equation} 490 * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \le 0.0031308 \\ 491 * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \gt 0.0031308 \end{cases} 492 * \end{equation}\) 493 * </td> 494 * </tr> 495 * <tr> 496 * <td>Electro-optical transfer function</td> 497 * <td colspan="4">\(\begin{equation} 498 * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \le 0.04045 \\ 499 * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \gt 0.04045 \end{cases} 500 * \end{equation}\) 501 * </td> 502 * </tr> 503 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 504 * </table> 505 * <p> 506 * <img src="{@docRoot}reference/android/images/graphics/colorspace_display_p3.png" /> 507 * <figcaption style="text-align: center;">Display P3 (orange) vs sRGB (white)</figcaption> 508 * </p> 509 */ 510 DISPLAY_P3, 511 /** 512 * <p>{@link ColorSpace.Rgb RGB} color space NTSC, 1953 standard.</p> 513 * <table summary="Color space definition"> 514 * <tr> 515 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 516 * </tr> 517 * <tr><td>x</td><td>0.67</td><td>0.21</td><td>0.14</td><td>0.310</td></tr> 518 * <tr><td>y</td><td>0.33</td><td>0.71</td><td>0.08</td><td>0.316</td></tr> 519 * <tr><th>Property</th><th colspan="4">Value</th></tr> 520 * <tr><td>Name</td><td colspan="4">NTSC (1953)</td></tr> 521 * <tr><td>CIE standard illuminant</td><td colspan="4">C</td></tr> 522 * <tr> 523 * <td>Opto-electronic transfer function</td> 524 * <td colspan="4">\(\begin{equation} 525 * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ 526 * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} 527 * \end{equation}\) 528 * </td> 529 * </tr> 530 * <tr> 531 * <td>Electro-optical transfer function</td> 532 * <td colspan="4">\(\begin{equation} 533 * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ 534 * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} 535 * \end{equation}\) 536 * </td> 537 * </tr> 538 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 539 * </table> 540 * <p> 541 * <img src="{@docRoot}reference/android/images/graphics/colorspace_ntsc_1953.png" /> 542 * <figcaption style="text-align: center;">NTSC 1953 (orange) vs sRGB (white)</figcaption> 543 * </p> 544 */ 545 NTSC_1953, 546 /** 547 * <p>{@link ColorSpace.Rgb RGB} color space SMPTE C.</p> 548 * <table summary="Color space definition"> 549 * <tr> 550 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 551 * </tr> 552 * <tr><td>x</td><td>0.630</td><td>0.310</td><td>0.155</td><td>0.3127</td></tr> 553 * <tr><td>y</td><td>0.340</td><td>0.595</td><td>0.070</td><td>0.3290</td></tr> 554 * <tr><th>Property</th><th colspan="4">Value</th></tr> 555 * <tr><td>Name</td><td colspan="4">SMPTE-C RGB</td></tr> 556 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 557 * <tr> 558 * <td>Opto-electronic transfer function</td> 559 * <td colspan="4">\(\begin{equation} 560 * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ 561 * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} 562 * \end{equation}\) 563 * </td> 564 * </tr> 565 * <tr> 566 * <td>Electro-optical transfer function</td> 567 * <td colspan="4">\(\begin{equation} 568 * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ 569 * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} 570 * \end{equation}\) 571 * </td> 572 * </tr> 573 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 574 * </table> 575 * <p> 576 * <img src="{@docRoot}reference/android/images/graphics/colorspace_smpte_c.png" /> 577 * <figcaption style="text-align: center;">SMPTE-C (orange) vs sRGB (white)</figcaption> 578 * </p> 579 */ 580 SMPTE_C, 581 /** 582 * <p>{@link ColorSpace.Rgb RGB} color space Adobe RGB (1998).</p> 583 * <table summary="Color space definition"> 584 * <tr> 585 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 586 * </tr> 587 * <tr><td>x</td><td>0.64</td><td>0.21</td><td>0.15</td><td>0.3127</td></tr> 588 * <tr><td>y</td><td>0.33</td><td>0.71</td><td>0.06</td><td>0.3290</td></tr> 589 * <tr><th>Property</th><th colspan="4">Value</th></tr> 590 * <tr><td>Name</td><td colspan="4">Adobe RGB (1998)</td></tr> 591 * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> 592 * <tr> 593 * <td>Opto-electronic transfer function</td> 594 * <td colspan="4">\(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\)</td> 595 * </tr> 596 * <tr> 597 * <td>Electro-optical transfer function</td> 598 * <td colspan="4">\(C_{linear} = C_{RGB}^{2.2}\)</td> 599 * </tr> 600 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 601 * </table> 602 * <p> 603 * <img src="{@docRoot}reference/android/images/graphics/colorspace_adobe_rgb.png" /> 604 * <figcaption style="text-align: center;">Adobe RGB (orange) vs sRGB (white)</figcaption> 605 * </p> 606 */ 607 ADOBE_RGB, 608 /** 609 * <p>{@link ColorSpace.Rgb RGB} color space ProPhoto RGB standardized as ROMM RGB ISO 22028-2:2013.</p> 610 * <table summary="Color space definition"> 611 * <tr> 612 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 613 * </tr> 614 * <tr><td>x</td><td>0.7347</td><td>0.1596</td><td>0.0366</td><td>0.3457</td></tr> 615 * <tr><td>y</td><td>0.2653</td><td>0.8404</td><td>0.0001</td><td>0.3585</td></tr> 616 * <tr><th>Property</th><th colspan="4">Value</th></tr> 617 * <tr><td>Name</td><td colspan="4">ROMM RGB ISO 22028-2:2013</td></tr> 618 * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr> 619 * <tr> 620 * <td>Opto-electronic transfer function</td> 621 * <td colspan="4">\(\begin{equation} 622 * C_{ROMM} = \begin{cases} 16 \times C_{linear} & C_{linear} \lt 0.001953 \\ 623 * C_{linear}^{\frac{1}{1.8}} & C_{linear} \ge 0.001953 \end{cases} 624 * \end{equation}\) 625 * </td> 626 * </tr> 627 * <tr> 628 * <td>Electro-optical transfer function</td> 629 * <td colspan="4">\(\begin{equation} 630 * C_{linear} = \begin{cases}\frac{C_{ROMM}}{16} & C_{ROMM} \lt 0.031248 \\ 631 * C_{ROMM}^{1.8} & C_{ROMM} \ge 0.031248 \end{cases} 632 * \end{equation}\) 633 * </td> 634 * </tr> 635 * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> 636 * </table> 637 * <p> 638 * <img src="{@docRoot}reference/android/images/graphics/colorspace_pro_photo_rgb.png" /> 639 * <figcaption style="text-align: center;">ProPhoto RGB (orange) vs sRGB (white)</figcaption> 640 * </p> 641 */ 642 PRO_PHOTO_RGB, 643 /** 644 * <p>{@link ColorSpace.Rgb RGB} color space ACES standardized as SMPTE ST 2065-1:2012.</p> 645 * <table summary="Color space definition"> 646 * <tr> 647 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 648 * </tr> 649 * <tr><td>x</td><td>0.73470</td><td>0.00000</td><td>0.00010</td><td>0.32168</td></tr> 650 * <tr><td>y</td><td>0.26530</td><td>1.00000</td><td>-0.07700</td><td>0.33767</td></tr> 651 * <tr><th>Property</th><th colspan="4">Value</th></tr> 652 * <tr><td>Name</td><td colspan="4">SMPTE ST 2065-1:2012 ACES</td></tr> 653 * <tr><td>CIE standard illuminant</td><td colspan="4">D60</td></tr> 654 * <tr> 655 * <td>Opto-electronic transfer function</td> 656 * <td colspan="4">\(C_{ACES} = C_{linear}\)</td> 657 * </tr> 658 * <tr> 659 * <td>Electro-optical transfer function</td> 660 * <td colspan="4">\(C_{linear} = C_{ACES}\)</td> 661 * </tr> 662 * <tr><td>Range</td><td colspan="4">\([-65504.0, 65504.0]\)</td></tr> 663 * </table> 664 * <p> 665 * <img src="{@docRoot}reference/android/images/graphics/colorspace_aces.png" /> 666 * <figcaption style="text-align: center;">ACES (orange) vs sRGB (white)</figcaption> 667 * </p> 668 */ 669 ACES, 670 /** 671 * <p>{@link ColorSpace.Rgb RGB} color space ACEScg standardized as Academy S-2014-004.</p> 672 * <table summary="Color space definition"> 673 * <tr> 674 * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> 675 * </tr> 676 * <tr><td>x</td><td>0.713</td><td>0.165</td><td>0.128</td><td>0.32168</td></tr> 677 * <tr><td>y</td><td>0.293</td><td>0.830</td><td>0.044</td><td>0.33767</td></tr> 678 * <tr><th>Property</th><th colspan="4">Value</th></tr> 679 * <tr><td>Name</td><td colspan="4">Academy S-2014-004 ACEScg</td></tr> 680 * <tr><td>CIE standard illuminant</td><td colspan="4">D60</td></tr> 681 * <tr> 682 * <td>Opto-electronic transfer function</td> 683 * <td colspan="4">\(C_{ACEScg} = C_{linear}\)</td> 684 * </tr> 685 * <tr> 686 * <td>Electro-optical transfer function</td> 687 * <td colspan="4">\(C_{linear} = C_{ACEScg}\)</td> 688 * </tr> 689 * <tr><td>Range</td><td colspan="4">\([-65504.0, 65504.0]\)</td></tr> 690 * </table> 691 * <p> 692 * <img src="{@docRoot}reference/android/images/graphics/colorspace_acescg.png" /> 693 * <figcaption style="text-align: center;">ACEScg (orange) vs sRGB (white)</figcaption> 694 * </p> 695 */ 696 ACESCG, 697 /** 698 * <p>{@link Model#XYZ XYZ} color space CIE XYZ. This color space assumes standard 699 * illuminant D50 as its white point.</p> 700 * <table summary="Color space definition"> 701 * <tr><th>Property</th><th colspan="4">Value</th></tr> 702 * <tr><td>Name</td><td colspan="4">Generic XYZ</td></tr> 703 * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr> 704 * <tr><td>Range</td><td colspan="4">\([-2.0, 2.0]\)</td></tr> 705 * </table> 706 */ 707 CIE_XYZ, 708 /** 709 * <p>{@link Model#LAB Lab} color space CIE L*a*b*. This color space uses CIE XYZ D50 710 * as a profile conversion space.</p> 711 * <table summary="Color space definition"> 712 * <tr><th>Property</th><th colspan="4">Value</th></tr> 713 * <tr><td>Name</td><td colspan="4">Generic L*a*b*</td></tr> 714 * <tr><td>CIE standard illuminant</td><td colspan="4">D50</td></tr> 715 * <tr><td>Range</td><td colspan="4">\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)</td></tr> 716 * </table> 717 */ 718 CIE_LAB 719 // Update the initialization block next to #get(Named) when adding new values 720 } 721 722 /** 723 * <p>A render intent determines how a {@link ColorSpace.Connector connector} 724 * maps colors from one color space to another. The choice of mapping is 725 * important when the source color space has a larger color gamut than the 726 * destination color space.</p> 727 * 728 * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent) 729 */ 730 public enum RenderIntent { 731 /** 732 * <p>Compresses the source gamut into the destination gamut. 733 * This render intent affects all colors, inside and outside 734 * of destination gamut. The goal of this render intent is 735 * to preserve the visual relationship between colors.</p> 736 * 737 * <p class="note">This render intent is currently not 738 * implemented and behaves like {@link #RELATIVE}.</p> 739 */ 740 PERCEPTUAL, 741 /** 742 * Similar to the {@link #ABSOLUTE} render intent, this render 743 * intent matches the closest color in the destination gamut 744 * but makes adjustments for the destination white point. 745 */ 746 RELATIVE, 747 /** 748 * <p>Attempts to maintain the relative saturation of colors 749 * from the source gamut to the destination gamut, to keep 750 * highly saturated colors as saturated as possible.</p> 751 * 752 * <p class="note">This render intent is currently not 753 * implemented and behaves like {@link #RELATIVE}.</p> 754 */ 755 SATURATION, 756 /** 757 * Colors that are in the destination gamut are left unchanged. 758 * Colors that fall outside of the destination gamut are mapped 759 * to the closest possible color within the gamut of the destination 760 * color space (they are clipped). 761 */ 762 ABSOLUTE 763 } 764 765 /** 766 * {@usesMathJax} 767 * 768 * <p>List of adaptation matrices that can be used for chromatic adaptation 769 * using the von Kries transform. These matrices are used to convert values 770 * in the CIE XYZ space to values in the LMS space (Long Medium Short).</p> 771 * 772 * <p>Given an adaptation matrix \(A\), the conversion from XYZ to 773 * LMS is straightforward:</p> 774 * 775 * $$\left[ \begin{array}{c} L\\ M\\ S \end{array} \right] = 776 * A \left[ \begin{array}{c} X\\ Y\\ Z \end{array} \right]$$ 777 * 778 * <p>The complete von Kries transform \(T\) uses a diagonal matrix 779 * noted \(D\) to perform the adaptation in LMS space. In addition 780 * to \(A\) and \(D\), the source white point \(W1\) and the destination 781 * white point \(W2\) must be specified:</p> 782 * 783 * $$\begin{align*} 784 * \left[ \begin{array}{c} L_1\\ M_1\\ S_1 \end{array} \right] &= 785 * A \left[ \begin{array}{c} W1_X\\ W1_Y\\ W1_Z \end{array} \right] \\ 786 * \left[ \begin{array}{c} L_2\\ M_2\\ S_2 \end{array} \right] &= 787 * A \left[ \begin{array}{c} W2_X\\ W2_Y\\ W2_Z \end{array} \right] \\ 788 * D &= \left[ \begin{matrix} \frac{L_2}{L_1} & 0 & 0 \\ 789 * 0 & \frac{M_2}{M_1} & 0 \\ 790 * 0 & 0 & \frac{S_2}{S_1} \end{matrix} \right] \\ 791 * T &= A^{-1}.D.A 792 * \end{align*}$$ 793 * 794 * <p>As an example, the resulting matrix \(T\) can then be used to 795 * perform the chromatic adaptation of sRGB XYZ transform from D65 796 * to D50:</p> 797 * 798 * $$sRGB_{D50} = T.sRGB_{D65}$$ 799 * 800 * @see ColorSpace.Connector 801 * @see ColorSpace#connect(ColorSpace, ColorSpace) 802 */ 803 public enum Adaptation { 804 /** 805 * Bradford matrix for the von Kries chromatic adaptation transform. 806 */ 807 BRADFORD(new float[] { 808 0.8951f, -0.7502f, 0.0389f, 809 0.2664f, 1.7135f, -0.0685f, 810 -0.1614f, 0.0367f, 1.0296f 811 }), 812 /** 813 * von Kries matrix for the von Kries chromatic adaptation transform. 814 */ 815 VON_KRIES(new float[] { 816 0.40024f, -0.22630f, 0.00000f, 817 0.70760f, 1.16532f, 0.00000f, 818 -0.08081f, 0.04570f, 0.91822f 819 }); 820 821 final float[] mTransform; 822 823 Adaptation(@NonNull @Size(9) float[] transform) { 824 mTransform = transform; 825 } 826 } 827 828 /** 829 * A color model is required by a {@link ColorSpace} to describe the 830 * way colors can be represented as tuples of numbers. A common color 831 * model is the {@link #RGB RGB} color model which defines a color 832 * as represented by a tuple of 3 numbers (red, green and blue). 833 */ 834 public enum Model { 835 /** 836 * The RGB model is a color model with 3 components that 837 * refer to the three additive primiaries: red, green 838 * andd blue. 839 */ 840 RGB(3), 841 /** 842 * The XYZ model is a color model with 3 components that 843 * are used to model human color vision on a basic sensory 844 * level. 845 */ 846 XYZ(3), 847 /** 848 * The Lab model is a color model with 3 components used 849 * to describe a color space that is more perceptually 850 * uniform than XYZ. 851 */ 852 LAB(3), 853 /** 854 * The CMYK model is a color model with 4 components that 855 * refer to four inks used in color printing: cyan, magenta, 856 * yellow and black (or key). CMYK is a subtractive color 857 * model. 858 */ 859 CMYK(4); 860 861 private final int mComponentCount; 862 863 Model(@IntRange(from = 1, to = 4) int componentCount) { 864 mComponentCount = componentCount; 865 } 866 867 /** 868 * Returns the number of components for this color model. 869 * 870 * @return An integer between 1 and 4 871 */ 872 @IntRange(from = 1, to = 4) 873 public int getComponentCount() { 874 return mComponentCount; 875 } 876 } 877 878 private ColorSpace( 879 @NonNull String name, 880 @NonNull Model model, 881 @IntRange(from = MIN_ID, to = MAX_ID) int id) { 882 883 if (name == null || name.length() < 1) { 884 throw new IllegalArgumentException("The name of a color space cannot be null and " + 885 "must contain at least 1 character"); 886 } 887 888 if (model == null) { 889 throw new IllegalArgumentException("A color space must have a model"); 890 } 891 892 if (id < MIN_ID || id > MAX_ID) { 893 throw new IllegalArgumentException("The id must be between " + 894 MIN_ID + " and " + MAX_ID); 895 } 896 897 mName = name; 898 mModel = model; 899 mId = id; 900 } 901 902 /** 903 * Returns the name of this color space. The name is never null 904 * and contains always at least 1 character. 905 * 906 * @return A non-null String of length >= 1 907 */ 908 @NonNull 909 public String getName() { 910 return mName; 911 } 912 913 /** 914 * Returns the ID of this color space. Positive IDs match the color 915 * spaces enumerated in {@link Named}. A negative ID indicates a 916 * color space created by calling one of the public constructors. 917 * 918 * @return An integer between {@link #MIN_ID} and {@link #MAX_ID} 919 */ 920 @IntRange(from = MIN_ID, to = MAX_ID) 921 public int getId() { 922 return mId; 923 } 924 925 /** 926 * Return the color model of this color space. 927 * 928 * @return A non-null {@link Model} 929 * 930 * @see Model 931 * @see #getComponentCount() 932 */ 933 @NonNull 934 public Model getModel() { 935 return mModel; 936 } 937 938 /** 939 * Returns the number of components that form a color value according 940 * to this color space's color model. 941 * 942 * @return An integer between 1 and 4 943 * 944 * @see Model 945 * @see #getModel() 946 */ 947 @IntRange(from = 1, to = 4) 948 public int getComponentCount() { 949 return mModel.getComponentCount(); 950 } 951 952 /** 953 * Returns whether this color space is a wide-gamut color space. 954 * An RGB color space is wide-gamut if its gamut entirely contains 955 * the {@link Named#SRGB sRGB} gamut and if the area of its gamut is 956 * 90% of greater than the area of the {@link Named#NTSC_1953 NTSC} 957 * gamut. 958 * 959 * @return True if this color space is a wide-gamut color space, 960 * false otherwise 961 */ 962 public abstract boolean isWideGamut(); 963 964 /** 965 * <p>Indicates whether this color space is the sRGB color space or 966 * equivalent to the sRGB color space.</p> 967 * <p>A color space is considered sRGB if it meets all the following 968 * conditions:</p> 969 * <ul> 970 * <li>Its color model is {@link Model#RGB}.</li> 971 * <li> 972 * Its primaries are within 1e-3 of the true 973 * {@link Named#SRGB sRGB} primaries. 974 * </li> 975 * <li> 976 * Its white point is withing 1e-3 of the CIE standard 977 * illuminant {@link #ILLUMINANT_D65 D65}. 978 * </li> 979 * <li>Its opto-electronic transfer function is not linear.</li> 980 * <li>Its electro-optical transfer function is not linear.</li> 981 * <li>Its range is \([0..1]\).</li> 982 * </ul> 983 * <p>This method always returns true for {@link Named#SRGB}.</p> 984 * 985 * @return True if this color space is the sRGB color space (or a 986 * close approximation), false otherwise 987 */ 988 public boolean isSrgb() { 989 return false; 990 } 991 992 /** 993 * Returns the minimum valid value for the specified component of this 994 * color space's color model. 995 * 996 * @param component The index of the component 997 * @return A floating point value less than {@link #getMaxValue(int)} 998 * 999 * @see #getMaxValue(int) 1000 * @see Model#getComponentCount() 1001 */ 1002 public abstract float getMinValue(@IntRange(from = 0, to = 3) int component); 1003 1004 /** 1005 * Returns the maximum valid value for the specified component of this 1006 * color space's color model. 1007 * 1008 * @param component The index of the component 1009 * @return A floating point value greater than {@link #getMinValue(int)} 1010 * 1011 * @see #getMinValue(int) 1012 * @see Model#getComponentCount() 1013 */ 1014 public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component); 1015 1016 /** 1017 * <p>Converts a color value from this color space's model to 1018 * tristimulus CIE XYZ values. If the color model of this color 1019 * space is not {@link Model#RGB RGB}, it is assumed that the 1020 * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} 1021 * standard illuminant.</p> 1022 * 1023 * <p>This method is a convenience for color spaces with a model 1024 * of 3 components ({@link Model#RGB RGB} or {@link Model#LAB} 1025 * for instance). With color spaces using fewer or more components, 1026 * use {@link #toXyz(float[])} instead</p>. 1027 * 1028 * @param r The first component of the value to convert from (typically R in RGB) 1029 * @param g The second component of the value to convert from (typically G in RGB) 1030 * @param b The third component of the value to convert from (typically B in RGB) 1031 * @return A new array of 3 floats, containing tristimulus XYZ values 1032 * 1033 * @see #toXyz(float[]) 1034 * @see #fromXyz(float, float, float) 1035 */ 1036 @NonNull 1037 @Size(3) 1038 public float[] toXyz(float r, float g, float b) { 1039 return toXyz(new float[] { r, g, b }); 1040 } 1041 1042 /** 1043 * <p>Converts a color value from this color space's model to 1044 * tristimulus CIE XYZ values. If the color model of this color 1045 * space is not {@link Model#RGB RGB}, it is assumed that the 1046 * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} 1047 * standard illuminant.</p> 1048 * 1049 * <p class="note">The specified array's length must be at least 1050 * equal to to the number of color components as returned by 1051 * {@link Model#getComponentCount()}.</p> 1052 * 1053 * @param v An array of color components containing the color space's 1054 * color value to convert to XYZ, and large enough to hold 1055 * the resulting tristimulus XYZ values 1056 * @return The array passed in parameter 1057 * 1058 * @see #toXyz(float, float, float) 1059 * @see #fromXyz(float[]) 1060 */ 1061 @NonNull 1062 @Size(min = 3) 1063 public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v); 1064 1065 /** 1066 * <p>Converts tristimulus values from the CIE XYZ space to this 1067 * color space's color model.</p> 1068 * 1069 * @param x The X component of the color value 1070 * @param y The Y component of the color value 1071 * @param z The Z component of the color value 1072 * @return A new array whose size is equal to the number of color 1073 * components as returned by {@link Model#getComponentCount()} 1074 * 1075 * @see #fromXyz(float[]) 1076 * @see #toXyz(float, float, float) 1077 */ 1078 @NonNull 1079 @Size(min = 3) 1080 public float[] fromXyz(float x, float y, float z) { 1081 float[] xyz = new float[mModel.getComponentCount()]; 1082 xyz[0] = x; 1083 xyz[1] = y; 1084 xyz[2] = z; 1085 return fromXyz(xyz); 1086 } 1087 1088 /** 1089 * <p>Converts tristimulus values from the CIE XYZ space to this color 1090 * space's color model. The resulting value is passed back in the specified 1091 * array.</p> 1092 * 1093 * <p class="note>The specified array's length must be at least equal to 1094 * to the number of color components as returned by 1095 * {@link Model#getComponentCount()}, and its first 3 values must 1096 * be the XYZ components to convert from.</p> 1097 * 1098 * @param v An array of color components containing the XYZ values 1099 * to convert from, and large enough to hold the number 1100 * of components of this color space's model 1101 * @return The array passed in parameter 1102 * 1103 * @see #fromXyz(float, float, float) 1104 * @see #toXyz(float[]) 1105 */ 1106 @NonNull 1107 @Size(min = 3) 1108 public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v); 1109 1110 /** 1111 * <p>Returns a string representation of the object. This method returns 1112 * a string equal to the value of:</p> 1113 * 1114 * <pre class="prettyprint"> 1115 * getName() + "(id=" + getId() + ", model=" + getModel() + ")" 1116 * </pre> 1117 * 1118 * <p>For instance, the string representation of the {@link Named#SRGB sRGB} 1119 * color space is equal to the following value:</p> 1120 * 1121 * <pre> 1122 * sRGB IEC61966-2.1 (id=0, model=RGB) 1123 * </pre> 1124 * 1125 * @return A string representation of the object 1126 */ 1127 @Override 1128 public String toString() { 1129 return mName + " (id=" + mId + ", model=" + mModel + ")"; 1130 } 1131 1132 @Override 1133 public boolean equals(Object o) { 1134 if (this == o) return true; 1135 if (o == null || getClass() != o.getClass()) return false; 1136 1137 ColorSpace that = (ColorSpace) o; 1138 1139 if (mId != that.mId) return false; 1140 //noinspection SimplifiableIfStatement 1141 if (!mName.equals(that.mName)) return false; 1142 return mModel == that.mModel; 1143 1144 } 1145 1146 @Override 1147 public int hashCode() { 1148 int result = mName.hashCode(); 1149 result = 31 * result + mModel.hashCode(); 1150 result = 31 * result + mId; 1151 return result; 1152 } 1153 1154 /** 1155 * <p>Connects two color spaces to allow conversion from the source color 1156 * space to the destination color space. If the source and destination 1157 * color spaces do not have the same profile connection space (CIE XYZ 1158 * with the same white point), they are chromatically adapted to use the 1159 * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p> 1160 * 1161 * <p>If the source and destination are the same, an optimized connector 1162 * is returned to avoid unnecessary computations and loss of precision.</p> 1163 * 1164 * <p>Colors are mapped from the source color space to the destination color 1165 * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.</p> 1166 * 1167 * @param source The color space to convert colors from 1168 * @param destination The color space to convert colors to 1169 * @return A non-null connector between the two specified color spaces 1170 * 1171 * @see #connect(ColorSpace) 1172 * @see #connect(ColorSpace, RenderIntent) 1173 * @see #connect(ColorSpace, ColorSpace, RenderIntent) 1174 */ 1175 @NonNull 1176 public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) { 1177 return connect(source, destination, RenderIntent.PERCEPTUAL); 1178 } 1179 1180 /** 1181 * <p>Connects two color spaces to allow conversion from the source color 1182 * space to the destination color space. If the source and destination 1183 * color spaces do not have the same profile connection space (CIE XYZ 1184 * with the same white point), they are chromatically adapted to use the 1185 * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p> 1186 * 1187 * <p>If the source and destination are the same, an optimized connector 1188 * is returned to avoid unnecessary computations and loss of precision.</p> 1189 * 1190 * @param source The color space to convert colors from 1191 * @param destination The color space to convert colors to 1192 * @param intent The render intent to map colors from the source to the destination 1193 * @return A non-null connector between the two specified color spaces 1194 * 1195 * @see #connect(ColorSpace) 1196 * @see #connect(ColorSpace, RenderIntent) 1197 * @see #connect(ColorSpace, ColorSpace) 1198 */ 1199 @NonNull 1200 @SuppressWarnings("ConstantConditions") 1201 public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination, 1202 @NonNull RenderIntent intent) { 1203 if (source.equals(destination)) return Connector.identity(source); 1204 1205 if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) { 1206 return new Connector.Rgb((Rgb) source, (Rgb) destination, intent); 1207 } 1208 1209 return new Connector(source, destination, intent); 1210 } 1211 1212 /** 1213 * <p>Connects the specified color spaces to sRGB. 1214 * If the source color space does not use CIE XYZ D65 as its profile 1215 * connection space, the two spaces are chromatically adapted to use the 1216 * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p> 1217 * 1218 * <p>If the source is the sRGB color space, an optimized connector 1219 * is returned to avoid unnecessary computations and loss of precision.</p> 1220 * 1221 * <p>Colors are mapped from the source color space to the destination color 1222 * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.</p> 1223 * 1224 * @param source The color space to convert colors from 1225 * @return A non-null connector between the specified color space and sRGB 1226 * 1227 * @see #connect(ColorSpace, RenderIntent) 1228 * @see #connect(ColorSpace, ColorSpace) 1229 * @see #connect(ColorSpace, ColorSpace, RenderIntent) 1230 */ 1231 @NonNull 1232 public static Connector connect(@NonNull ColorSpace source) { 1233 return connect(source, RenderIntent.PERCEPTUAL); 1234 } 1235 1236 /** 1237 * <p>Connects the specified color spaces to sRGB. 1238 * If the source color space does not use CIE XYZ D65 as its profile 1239 * connection space, the two spaces are chromatically adapted to use the 1240 * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.</p> 1241 * 1242 * <p>If the source is the sRGB color space, an optimized connector 1243 * is returned to avoid unnecessary computations and loss of precision.</p> 1244 * 1245 * @param source The color space to convert colors from 1246 * @param intent The render intent to map colors from the source to the destination 1247 * @return A non-null connector between the specified color space and sRGB 1248 * 1249 * @see #connect(ColorSpace) 1250 * @see #connect(ColorSpace, ColorSpace) 1251 * @see #connect(ColorSpace, ColorSpace, RenderIntent) 1252 */ 1253 @NonNull 1254 public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) { 1255 if (source.isSrgb()) return Connector.identity(source); 1256 1257 if (source.getModel() == Model.RGB) { 1258 return new Connector.Rgb((Rgb) source, (Rgb) get(Named.SRGB), intent); 1259 } 1260 1261 return new Connector(source, get(Named.SRGB), intent); 1262 } 1263 1264 /** 1265 * <p>Performs the chromatic adaptation of a color space from its native 1266 * white point to the specified white point.</p> 1267 * 1268 * <p>The chromatic adaptation is performed using the 1269 * {@link Adaptation#BRADFORD} matrix.</p> 1270 * 1271 * <p class="note">The color space returned by this method always has 1272 * an ID of {@link #MIN_ID}.</p> 1273 * 1274 * @param colorSpace The color space to chromatically adapt 1275 * @param whitePoint The new white point 1276 * @return A {@link ColorSpace} instance with the same name, primaries, 1277 * transfer functions and range as the specified color space 1278 * 1279 * @see Adaptation 1280 * @see #adapt(ColorSpace, float[], Adaptation) 1281 */ 1282 @NonNull 1283 public static ColorSpace adapt(@NonNull ColorSpace colorSpace, 1284 @NonNull @Size(min = 2, max = 3) float[] whitePoint) { 1285 return adapt(colorSpace, whitePoint, Adaptation.BRADFORD); 1286 } 1287 1288 /** 1289 * <p>Performs the chromatic adaptation of a color space from its native 1290 * white point to the specified white point. If the specified color space 1291 * does not have an {@link Model#RGB RGB} color model, or if the color 1292 * space already has the target white point, the color space is returned 1293 * unmodified.</p> 1294 * 1295 * <p>The chromatic adaptation is performed using the von Kries method 1296 * described in the documentation of {@link Adaptation}.</p> 1297 * 1298 * <p class="note">The color space returned by this method always has 1299 * an ID of {@link #MIN_ID}.</p> 1300 * 1301 * @param colorSpace The color space to chromatically adapt 1302 * @param whitePoint The new white point 1303 * @param adaptation The adaptation matrix 1304 * @return A new color space if the specified color space has an RGB 1305 * model and a white point different from the specified white 1306 * point; the specified color space otherwise 1307 * 1308 * @see Adaptation 1309 * @see #adapt(ColorSpace, float[]) 1310 */ 1311 @NonNull 1312 public static ColorSpace adapt(@NonNull ColorSpace colorSpace, 1313 @NonNull @Size(min = 2, max = 3) float[] whitePoint, 1314 @NonNull Adaptation adaptation) { 1315 if (colorSpace.getModel() == Model.RGB) { 1316 ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace; 1317 if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace; 1318 1319 float[] xyz = whitePoint.length == 3 ? 1320 Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint); 1321 float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform, 1322 xyYToXyz(rgb.getWhitePoint()), xyz); 1323 float[] transform = mul3x3(adaptationTransform, rgb.mTransform); 1324 1325 return new ColorSpace.Rgb(rgb, transform, whitePoint); 1326 } 1327 return colorSpace; 1328 } 1329 1330 /** 1331 * <p>Returns an instance of {@link ColorSpace} whose ID matches the specified 1332 * ID. If the ID is < 0 or > {@link #MAX_ID}, calling this method is equivalent 1333 * to calling <code>get(Named.SRGB)</code>.</p> 1334 * 1335 * <p>This method always returns the same instance for a given ID.</p> 1336 * 1337 * <p>This method is thread-safe.</p> 1338 * 1339 * @param index An integer ID between {@link #MIN_ID} and {@link #MAX_ID} 1340 * @return A non-null {@link ColorSpace} instance 1341 */ 1342 @NonNull 1343 static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) { 1344 if (index < 0 || index > Named.values().length) { 1345 return get(Named.SRGB); 1346 } 1347 return sNamedColorSpaces[index]; 1348 } 1349 1350 /** 1351 * <p>Returns an instance of {@link ColorSpace} identified by the specified 1352 * name. The list of names provided in the {@link Named} enum gives access 1353 * to a variety of common RGB color spaces.</p> 1354 * 1355 * <p>This method always returns the same instance for a given name.</p> 1356 * 1357 * <p>This method is thread-safe.</p> 1358 * 1359 * @param name The name of the color space to get an instance of 1360 * @return A non-null {@link ColorSpace} instance 1361 */ 1362 @NonNull 1363 public static ColorSpace get(@NonNull Named name) { 1364 return sNamedColorSpaces[name.ordinal()]; 1365 } 1366 1367 /** 1368 * <p>Creates a new {@link Renderer} that can be used to visualize and 1369 * debug color spaces. See the documentation of {@link Renderer} for 1370 * more information.</p> 1371 * 1372 * @return A new non-null {@link Renderer} instance 1373 * 1374 * @see Renderer 1375 */ 1376 @NonNull 1377 public static Renderer createRenderer() { 1378 return new Renderer(); 1379 } 1380 1381 static { 1382 sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb( 1383 "sRGB IEC61966-2.1", 1384 SRGB_PRIMARIES, 1385 ILLUMINANT_D65, 1386 x -> rcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1387 x -> response(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1388 0.0f, 1.0f, 1389 Named.SRGB.ordinal() 1390 ); 1391 sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb( 1392 "sRGB IEC61966-2.1 (Linear)", 1393 SRGB_PRIMARIES, 1394 ILLUMINANT_D65, 1395 DoubleUnaryOperator.identity(), 1396 DoubleUnaryOperator.identity(), 1397 0.0f, 1.0f, 1398 Named.LINEAR_SRGB.ordinal() 1399 ); 1400 sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( 1401 "scRGB-nl IEC 61966-2-2:2003", 1402 SRGB_PRIMARIES, 1403 ILLUMINANT_D65, 1404 x -> absRcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1405 x -> absResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1406 -0.5f, 7.5f, 1407 Named.EXTENDED_SRGB.ordinal() 1408 ); 1409 sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( 1410 "scRGB- IEC 61966-2-2:2003", 1411 SRGB_PRIMARIES, 1412 ILLUMINANT_D65, 1413 DoubleUnaryOperator.identity(), 1414 DoubleUnaryOperator.identity(), 1415 -0.5f, 7.5f, 1416 Named.LINEAR_EXTENDED_SRGB.ordinal() 1417 ); 1418 sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb( 1419 "Rec. ITU-R BT.709-5", 1420 new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }, 1421 ILLUMINANT_D65, 1422 x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1423 x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1424 0.0f, 1.0f, 1425 Named.BT709.ordinal() 1426 ); 1427 sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( 1428 "Rec. ITU-R BT.2020-1", 1429 new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, 1430 ILLUMINANT_D65, 1431 x -> rcpResponse(x, 1 / 0.45, 1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145), 1432 x -> response(x, 1 / 0.45, 1 / 1.0993, 0.099 / 1.0993, 1 / 4.5, 0.08145), 1433 0.0f, 1.0f, 1434 Named.BT2020.ordinal() 1435 ); 1436 sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb( 1437 "SMPTE RP 431-2-2007 DCI (P3)", 1438 new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, 1439 new float[] { 0.314f, 0.351f }, 1440 x -> Math.pow(x, 1 / 2.6), 1441 x -> Math.pow(x, 2.6), 1442 0.0f, 1.0f, 1443 Named.DCI_P3.ordinal() 1444 ); 1445 sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb( 1446 "Display P3", 1447 new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, 1448 ILLUMINANT_D65, 1449 x -> rcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1450 x -> response(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), 1451 0.0f, 1.0f, 1452 Named.DISPLAY_P3.ordinal() 1453 ); 1454 sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb( 1455 "NTSC (1953)", 1456 NTSC_1953_PRIMARIES, 1457 ILLUMINANT_C, 1458 x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1459 x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1460 0.0f, 1.0f, 1461 Named.NTSC_1953.ordinal() 1462 ); 1463 sNamedColorSpaces[Named.SMPTE_C.ordinal()] = new ColorSpace.Rgb( 1464 "SMPTE-C RGB", 1465 new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f }, 1466 ILLUMINANT_D65, 1467 x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1468 x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), 1469 0.0f, 1.0f, 1470 Named.SMPTE_C.ordinal() 1471 ); 1472 sNamedColorSpaces[Named.ADOBE_RGB.ordinal()] = new ColorSpace.Rgb( 1473 "Adobe RGB (1998)", 1474 new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f }, 1475 ILLUMINANT_D65, 1476 x -> Math.pow(x, 1 / 2.2), 1477 x -> Math.pow(x, 2.2), 1478 0.0f, 1.0f, 1479 Named.ADOBE_RGB.ordinal() 1480 ); 1481 sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb( 1482 "ROMM RGB ISO 22028-2:2013", 1483 new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f }, 1484 ILLUMINANT_D50, 1485 x -> rcpResponse(x, 1.8, 1.0, 0.0, 1 / 16.0, 0.031248), 1486 x -> response(x, 1.8, 1.0, 0.0, 1 / 16.0, 0.031248), 1487 0.0f, 1.0f, 1488 Named.PRO_PHOTO_RGB.ordinal() 1489 ); 1490 sNamedColorSpaces[Named.ACES.ordinal()] = new ColorSpace.Rgb( 1491 "SMPTE ST 2065-1:2012 ACES", 1492 new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f }, 1493 ILLUMINANT_D60, 1494 DoubleUnaryOperator.identity(), 1495 DoubleUnaryOperator.identity(), 1496 -65504.0f, 65504.0f, 1497 Named.ACES.ordinal() 1498 ); 1499 sNamedColorSpaces[Named.ACESCG.ordinal()] = new ColorSpace.Rgb( 1500 "Academy S-2014-004 ACEScg", 1501 new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f }, 1502 ILLUMINANT_D60, 1503 DoubleUnaryOperator.identity(), 1504 DoubleUnaryOperator.identity(), 1505 -65504.0f, 65504.0f, 1506 Named.ACESCG.ordinal() 1507 ); 1508 sNamedColorSpaces[Named.CIE_XYZ.ordinal()] = new Xyz( 1509 "Generic XYZ", 1510 Named.CIE_XYZ.ordinal() 1511 ); 1512 sNamedColorSpaces[Named.CIE_LAB.ordinal()] = new ColorSpace.Lab( 1513 "Generic L*a*b*", 1514 Named.CIE_LAB.ordinal() 1515 ); 1516 } 1517 1518 // Reciprocal piecewise gamma response 1519 private static double rcpResponse(double x, double g,double a, double b, double c, double d) { 1520 return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c; 1521 } 1522 1523 // Piecewise gamma response 1524 private static double response(double x, double g, double a, double b, double c, double d) { 1525 return x >= d ? Math.pow(a * x + b, g) : c * x; 1526 } 1527 1528 // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color 1529 // spaces that allow negative values 1530 @SuppressWarnings("SameParameterValue") 1531 private static double absRcpResponse(double x, double g, double a, double b, double c, double d) { 1532 return Math.copySign(rcpResponse(x < 0.0 ? -x : x, g, a, b, c, d), x); 1533 } 1534 1535 // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that 1536 // allow negative values 1537 @SuppressWarnings("SameParameterValue") 1538 private static double absResponse(double x, double g, double a, double b, double c, double d) { 1539 return Math.copySign(response(x < 0.0 ? -x : x, g, a, b, c, d), x); 1540 } 1541 1542 /** 1543 * Compares two arrays of float with a precision of 1e-3. 1544 * 1545 * @param a The first array to compare 1546 * @param b The second array to compare 1547 * @return True if the two arrays are equal, false otherwise 1548 */ 1549 private static boolean compare(@NonNull float[] a, @NonNull float[] b) { 1550 if (a == b) return true; 1551 for (int i = 0; i < a.length; i++) { 1552 if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false; 1553 } 1554 return true; 1555 } 1556 1557 /** 1558 * Inverts a 3x3 matrix. This method assumes the matrix is invertible. 1559 * 1560 * @param m A 3x3 matrix as a non-null array of 9 floats 1561 * @return A new array of 9 floats containing the inverse of the input matrix 1562 */ 1563 @NonNull 1564 @Size(9) 1565 private static float[] inverse3x3(@NonNull @Size(9) float[] m) { 1566 float a = m[0]; 1567 float b = m[3]; 1568 float c = m[6]; 1569 float d = m[1]; 1570 float e = m[4]; 1571 float f = m[7]; 1572 float g = m[2]; 1573 float h = m[5]; 1574 float i = m[8]; 1575 1576 float A = e * i - f * h; 1577 float B = f * g - d * i; 1578 float C = d * h - e * g; 1579 1580 float det = a * A + b * B + c * C; 1581 1582 float inverted[] = new float[m.length]; 1583 inverted[0] = A / det; 1584 inverted[1] = B / det; 1585 inverted[2] = C / det; 1586 inverted[3] = (c * h - b * i) / det; 1587 inverted[4] = (a * i - c * g) / det; 1588 inverted[5] = (b * g - a * h) / det; 1589 inverted[6] = (b * f - c * e) / det; 1590 inverted[7] = (c * d - a * f) / det; 1591 inverted[8] = (a * e - b * d) / det; 1592 return inverted; 1593 } 1594 1595 /** 1596 * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats. 1597 * 1598 * @param lhs 3x3 matrix, as a non-null array of 9 floats 1599 * @param rhs 3x3 matrix, as a non-null array of 9 floats 1600 * @return A new array of 9 floats containing the result of the multiplication 1601 * of rhs by lhs 1602 */ 1603 @NonNull 1604 @Size(9) 1605 private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) { 1606 float[] r = new float[9]; 1607 r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2]; 1608 r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2]; 1609 r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2]; 1610 r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5]; 1611 r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5]; 1612 r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5]; 1613 r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8]; 1614 r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8]; 1615 r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8]; 1616 return r; 1617 } 1618 1619 /** 1620 * Multiplies a vector of 3 components by a 3x3 matrix and stores the 1621 * result in the input vector. 1622 * 1623 * @param lhs 3x3 matrix, as a non-null array of 9 floats 1624 * @param rhs Vector of 3 components, as a non-null array of 3 floats 1625 * @return The array of 3 passed as the rhs parameter 1626 */ 1627 @NonNull 1628 @Size(min = 3) 1629 private static float[] mul3x3Float3( 1630 @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) { 1631 float r0 = rhs[0]; 1632 float r1 = rhs[1]; 1633 float r2 = rhs[2]; 1634 rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2; 1635 rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2; 1636 rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2; 1637 return rhs; 1638 } 1639 1640 /** 1641 * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats, 1642 * by a 3x3 matrix represented as an array of 9 floats. 1643 * 1644 * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats 1645 * @param rhs 3x3 matrix, as a non-null array of 9 floats 1646 * @return A new array of 9 floats containing the result of the multiplication 1647 * of rhs by lhs 1648 */ 1649 @NonNull 1650 @Size(9) 1651 private static float[] mul3x3Diag( 1652 @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) { 1653 return new float[] { 1654 lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2], 1655 lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5], 1656 lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8] 1657 }; 1658 } 1659 1660 /** 1661 * Converts a value from CIE xyY to CIE XYZ. Y is assumed to be 1 so the 1662 * input xyY array only contains the x and y components. 1663 * 1664 * @param xyY The xyY value to convert to XYZ, cannot be null, length must be 2 1665 * @return A new float array of length 3 containing XYZ values 1666 */ 1667 @NonNull 1668 @Size(3) 1669 private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) { 1670 return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] }; 1671 } 1672 1673 /** 1674 * Converts values from CIE xyY to CIE L*u*v*. Y is assumed to be 1 so the 1675 * input xyY array only contains the x and y components. After this method 1676 * returns, the xyY array contains the converted u and v components. 1677 * 1678 * @param xyY The xyY value to convert to XYZ, cannot be null, 1679 * length must be a multiple of 2 1680 */ 1681 private static void xyYToUv(@NonNull @Size(multiple = 2) float[] xyY) { 1682 for (int i = 0; i < xyY.length; i += 2) { 1683 float x = xyY[i]; 1684 float y = xyY[i + 1]; 1685 1686 float d = -2.0f * x + 12.0f * y + 3; 1687 float u = (4.0f * x) / d; 1688 float v = (9.0f * y) / d; 1689 1690 xyY[i] = u; 1691 xyY[i + 1] = v; 1692 } 1693 } 1694 1695 /** 1696 * <p>Computes the chromatic adaptation transform from the specified 1697 * source white point to the specified destination white point.</p> 1698 * 1699 * <p>The transform is computed using the von Kris method, described 1700 * in more details in the documentation of {@link Adaptation}. The 1701 * {@link Adaptation} enum provides different matrices that can be 1702 * used to perform the adaptation.</p> 1703 * 1704 * @param matrix The adaptation matrix 1705 * @param srcWhitePoint The white point to adapt from, *will be modified* 1706 * @param dstWhitePoint The white point to adapt to, *will be modified* 1707 * @return A 3x3 matrix as a non-null array of 9 floats 1708 */ 1709 @NonNull 1710 @Size(9) 1711 private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix, 1712 @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) { 1713 float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint); 1714 float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint); 1715 // LMS is a diagonal matrix stored as a float[3] 1716 float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] }; 1717 return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix)); 1718 } 1719 1720 /** 1721 * Implementation of the CIE XYZ color space. Assumes the white point is D50. 1722 */ 1723 private static final class Xyz extends ColorSpace { 1724 private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { 1725 super(name, Model.XYZ, id); 1726 } 1727 1728 @Override 1729 public boolean isWideGamut() { 1730 return true; 1731 } 1732 1733 @Override 1734 public float getMinValue(@IntRange(from = 0, to = 3) int component) { 1735 return -2.0f; 1736 } 1737 1738 @Override 1739 public float getMaxValue(@IntRange(from = 0, to = 3) int component) { 1740 return 2.0f; 1741 } 1742 1743 @Override 1744 public float[] toXyz(@NonNull @Size(min = 3) float[] v) { 1745 v[0] = clamp(v[0]); 1746 v[1] = clamp(v[1]); 1747 v[2] = clamp(v[2]); 1748 return v; 1749 } 1750 1751 @Override 1752 public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { 1753 v[0] = clamp(v[0]); 1754 v[1] = clamp(v[1]); 1755 v[2] = clamp(v[2]); 1756 return v; 1757 } 1758 1759 private static float clamp(float x) { 1760 return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x; 1761 } 1762 } 1763 1764 /** 1765 * Implementation of the CIE L*a*b* color space. Its PCS is CIE XYZ 1766 * with a white point of D50. 1767 */ 1768 private static final class Lab extends ColorSpace { 1769 private static final float A = 216.0f / 24389.0f; 1770 private static final float B = 841.0f / 108.0f; 1771 private static final float C = 4.0f / 29.0f; 1772 private static final float D = 6.0f / 29.0f; 1773 1774 private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { 1775 super(name, Model.LAB, id); 1776 } 1777 1778 @Override 1779 public boolean isWideGamut() { 1780 return true; 1781 } 1782 1783 @Override 1784 public float getMinValue(@IntRange(from = 0, to = 3) int component) { 1785 return component == 0 ? 0.0f : -128.0f; 1786 } 1787 1788 @Override 1789 public float getMaxValue(@IntRange(from = 0, to = 3) int component) { 1790 return component == 0 ? 100.0f : 128.0f; 1791 } 1792 1793 @Override 1794 public float[] toXyz(@NonNull @Size(min = 3) float[] v) { 1795 v[0] = clamp(v[0], 0.0f, 100.0f); 1796 v[1] = clamp(v[1], -128.0f, 128.0f); 1797 v[2] = clamp(v[2], -128.0f, 128.0f); 1798 1799 float fy = (v[0] + 16.0f) / 116.0f; 1800 float fx = fy + (v[1] * 0.002f); 1801 float fz = fy - (v[2] * 0.005f); 1802 float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C); 1803 float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C); 1804 float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C); 1805 1806 v[0] = X * ILLUMINANT_D50_XYZ[0]; 1807 v[1] = Y * ILLUMINANT_D50_XYZ[1]; 1808 v[2] = Z * ILLUMINANT_D50_XYZ[2]; 1809 1810 return v; 1811 } 1812 1813 @Override 1814 public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { 1815 float X = v[0] / ILLUMINANT_D50_XYZ[0]; 1816 float Y = v[1] / ILLUMINANT_D50_XYZ[1]; 1817 float Z = v[2] / ILLUMINANT_D50_XYZ[2]; 1818 1819 float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C; 1820 float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C; 1821 float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C; 1822 1823 float L = 116.0f * fy - 16.0f; 1824 float a = 500.0f * (fx - fy); 1825 float b = 200.0f * (fy - fz); 1826 1827 v[0] = clamp(L, 0.0f, 100.0f); 1828 v[1] = clamp(a, -128.0f, 128.0f); 1829 v[2] = clamp(b, -128.0f, 128.0f); 1830 1831 return v; 1832 } 1833 1834 private static float clamp(float x, float min, float max) { 1835 return x < min ? min : x > max ? max : x; 1836 } 1837 } 1838 1839 /** 1840 * {@usesMathJax} 1841 * 1842 * <p>An RGB color space is an additive color space using the 1843 * {@link Model#RGB RGB} color model (a color is therefore represented 1844 * by a tuple of 3 numbers).</p> 1845 * 1846 * <p>A specific RGB color space is defined by the following properties:</p> 1847 * <ul> 1848 * <li>Three chromaticities of the red, green and blue primaries, which 1849 * define the gamut of the color space.</li> 1850 * <li>A white point chromaticity that defines the stimulus to which 1851 * color space values are normalized (also just called "white").</li> 1852 * <li>An opto-electronic transfer function, also called opto-electronic 1853 * conversion function or often, and approximately, gamma function.</li> 1854 * <li>An electro-optical transfer function, also called electo-optical 1855 * conversion function or often, and approximately, gamma function.</li> 1856 * <li>A range of valid RGB values (most commonly \([0..1]\)).</li> 1857 * </ul> 1858 * 1859 * <p>The most commonly used RGB color space is {@link Named#SRGB sRGB}.</p> 1860 * 1861 * <h3>Primaries and white point chromaticities</h3> 1862 * <p>In this implementation, the chromaticity of the primaries and the white 1863 * point of an RGB color space is defined in the CIE xyY color space. This 1864 * color space separates the chromaticity of a color, the x and y components, 1865 * and its luminance, the Y component. Since the primaries and the white 1866 * point have full brightness, the Y component is assumed to be 1 and only 1867 * the x and y components are needed to encode them.</p> 1868 * <p>For convenience, this implementation also allows to define the 1869 * primaries and white point in the CIE XYZ space. The tristimulus XYZ values 1870 * are internally converted to xyY.</p> 1871 * 1872 * <p> 1873 * <img src="{@docRoot}reference/android/images/graphics/colorspace_srgb.png" /> 1874 * <figcaption style="text-align: center;">sRGB primaries and white point</figcaption> 1875 * </p> 1876 * 1877 * <h3>Transfer functions</h3> 1878 * <p>A transfer function is a color component conversion function, defined as 1879 * a single variable, monotonic mathematical function. It is applied to each 1880 * individual component of a color. They are used to perform the mapping 1881 * between linear tristimulus values and non-linear electronic signal value.</p> 1882 * <p>The <em>opto-electronic transfer function</em> (OETF or OECF) encodes 1883 * tristimulus values in a scene to a non-linear electronic signal value. 1884 * An OETF is often expressed as a power function with an exponent between 1885 * 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).</p> 1886 * <p>The <em>electro-optical transfer function</em> (EOTF or EOCF) decodes 1887 * a non-linear electronic signal value to a tristimulus value at the display. 1888 * An EOTF is often expressed as a power function with an exponent between 1889 * 1.8 and 2.6.</p> 1890 * <p>Transfer functions are used as a compression scheme. For instance, 1891 * linear sRGB values would normally require 11 to 12 bits of precision to 1892 * store all values that can be perceived by the human eye. When encoding 1893 * sRGB values using the appropriate OETF (see {@link Named#SRGB sRGB} for 1894 * an exact mathematical description of that OETF), the values can be 1895 * compressed to only 8 bits precision.</p> 1896 * <p>When manipulating RGB values, particularly sRGB values, it is safe 1897 * to assume that these values have been encoded with the appropriate 1898 * OETF (unless noted otherwise). Encoded values are often said to be in 1899 * "gamma space". They are therefore defined in a non-linear space. This 1900 * in turns means that any linear operation applied to these values is 1901 * going to yield mathematically incorrect results (any linear interpolation 1902 * such as gradient generation for instance, most image processing functions 1903 * such as blurs, etc.).</p> 1904 * <p>To properly process encoded RGB values you must first apply the 1905 * EOTF to decode the value into linear space. After processing, the RGB 1906 * value must be encoded back to non-linear ("gamma") space. Here is a 1907 * formal description of the process, where \(f\) is the processing 1908 * function to apply:</p> 1909 * 1910 * $$RGB_{out} = OETF(f(EOTF(RGB_{in})))$$ 1911 * 1912 * <p class="note">Some RGB color spaces, such as {@link Named#ACES} and 1913 * {@link Named#LINEAR_EXTENDED_SRGB scRGB}, are said to be linear because 1914 * their transfer functions are the identity function: \(f(x) = x\). 1915 * If the source and/or destination are known to be linear, it is not 1916 * necessary to invoke the transfer functions.</p> 1917 * 1918 * <h3>Range</h3> 1919 * <p>Most RGB color spaces allow RGB values in the range \([0..1]\). There 1920 * are however a few RGB color spaces that allow much larger ranges. For 1921 * instance, {@link Named#EXTENDED_SRGB scRGB} is used to manipulate the 1922 * range \([-0.5..7.5]\) while {@link Named#ACES ACES} can be used throughout 1923 * the range \([-65504, 65504]\).</p> 1924 * 1925 * <p> 1926 * <img src="{@docRoot}reference/android/images/graphics/colorspace_scrgb.png" /> 1927 * <figcaption style="text-align: center;">Extended sRGB and its large range</figcaption> 1928 * </p> 1929 * 1930 * <h3>Converting between RGB color spaces</h3> 1931 * <p>Conversion between two color spaces is achieved by using an intermediate 1932 * color space called the profile connection space (PCS). The PCS used by 1933 * this implementation is CIE XYZ. The conversion operation is defined 1934 * as such:</p> 1935 * 1936 * $$RGB_{out} = OETF(T_{dst}^{-1} \cdot T_{src} \cdot EOTF(RGB_{in}))$$ 1937 * 1938 * <p>Where \(T_{src}\) is the {@link #getTransform() RGB to XYZ transform} 1939 * of the source color space and \(T_{dst}^{-1}\) the {@link #getInverseTransform() 1940 * XYZ to RGB transform} of the destination color space.</p> 1941 * <p>Many RGB color spaces commonly used with electronic devices use the 1942 * standard illuminant {@link #ILLUMINANT_D65 D65}. Care must be take however 1943 * when converting between two RGB color spaces if their white points do not 1944 * match. This can be achieved by either calling 1945 * {@link #adapt(ColorSpace, float[])} to adapt one or both color spaces to 1946 * a single common white point. This can be achieved automatically by calling 1947 * {@link ColorSpace#connect(ColorSpace, ColorSpace)}, which also handles 1948 * non-RGB color spaces.</p> 1949 * <p>To learn more about the white point adaptation process, refer to the 1950 * documentation of {@link Adaptation}.</p> 1951 */ 1952 public static class Rgb extends ColorSpace { 1953 @NonNull private final float[] mWhitePoint; 1954 @NonNull private final float[] mPrimaries; 1955 @NonNull private final float[] mTransform; 1956 @NonNull private final float[] mInverseTransform; 1957 1958 @NonNull private final boolean mIsWideGamut; 1959 @NonNull private final boolean mIsSrgb; 1960 1961 @NonNull private final DoubleUnaryOperator mOetf; 1962 @NonNull private final DoubleUnaryOperator mEotf; 1963 @NonNull private final DoubleUnaryOperator mClampedOetf; 1964 @NonNull private final DoubleUnaryOperator mClampedEotf; 1965 1966 private final float mMin; 1967 private final float mMax; 1968 1969 /** 1970 * <p>Creates a new RGB color space using a 3x3 column-major transform matrix. 1971 * The transform matrix must convert from the RGB space to the profile connection 1972 * space CIE XYZ.</p> 1973 * 1974 * <p class="note">The range of the color space is imposed to be \([0..1]\).</p> 1975 * 1976 * @param name Name of the color space, cannot be null, its length must be >= 1 1977 * @param toXYZ 3x3 column-major transform matrix from RGB to the profile 1978 * connection space CIE XYZ as an array of 9 floats, cannot be null 1979 * @param oetf Opto-electronic transfer function, cannot be null 1980 * @param eotf Electro-optical transfer function, cannot be null 1981 * 1982 * @throws IllegalArgumentException If any of the following conditions is met: 1983 * <ul> 1984 * <li>The name is null or has a length of 0.</li> 1985 * <li>The OETF is null or the EOTF is null.</li> 1986 * <li>The minimum valid value is >= the maximum valid value.</li> 1987 * </ul> 1988 * 1989 * @see #get(Named) 1990 */ 1991 public Rgb( 1992 @NonNull @Size(min = 1) String name, 1993 @NonNull @Size(9) float[] toXYZ, 1994 @NonNull DoubleUnaryOperator oetf, 1995 @NonNull DoubleUnaryOperator eotf) { 1996 this(name, computePrimaries(toXYZ, eotf), computeWhitePoint(toXYZ, eotf), 1997 oetf, eotf, 0.0f, 1.0f, MIN_ID); 1998 } 1999 2000 /** 2001 * <p>Creates a new RGB color space using a specified set of primaries 2002 * and a specified white point.</p> 2003 * 2004 * <p>The primaries and white point can be specified in the CIE xyY space 2005 * or in CIE XYZ. The length of the arrays depends on the chosen space:</p> 2006 * 2007 * <table summary="Parameters length"> 2008 * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr> 2009 * <tr><td>xyY</td><td>6</td><td>2</td></tr> 2010 * <tr><td>XYZ</td><td>9</td><td>3</td></tr> 2011 * </table> 2012 * 2013 * <p>When the primaries and/or white point are specified in xyY, the Y component 2014 * does not need to be specified and is assumed to be 1.0. Only the xy components 2015 * are required.</p> 2016 * 2017 * <p class="note">The ID, areturned by {@link #getId()}, of an object created by 2018 * this constructor is always {@link #MIN_ID}.</p> 2019 * 2020 * @param name Name of the color space, cannot be null, its length must be >= 1 2021 * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats 2022 * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats 2023 * @param oetf Opto-electronic transfer function, cannot be null 2024 * @param eotf Electro-optical transfer function, cannot be null 2025 * @param min The minimum valid value in this color space's RGB range 2026 * @param max The maximum valid value in this color space's RGB range 2027 * 2028 * @throws IllegalArgumentException <p>If any of the following conditions is met:</p> 2029 * <ul> 2030 * <li>The name is null or has a length of 0.</li> 2031 * <li>The primaries array is null or has a length that is neither 6 or 9.</li> 2032 * <li>The white point array is null or has a length that is neither 2 or 3.</li> 2033 * <li>The OETF is null or the EOTF is null.</li> 2034 * <li>The minimum valid value is >= the maximum valid value.</li> 2035 * </ul> 2036 * 2037 * @see #get(Named) 2038 */ 2039 public Rgb( 2040 @NonNull @Size(min = 1) String name, 2041 @NonNull @Size(min = 6, max = 9) float[] primaries, 2042 @NonNull @Size(min = 2, max = 3) float[] whitePoint, 2043 @NonNull DoubleUnaryOperator oetf, 2044 @NonNull DoubleUnaryOperator eotf, 2045 float min, 2046 float max) { 2047 this(name, primaries, whitePoint, oetf, eotf, min, max, MIN_ID); 2048 } 2049 2050 /** 2051 * <p>Creates a new RGB color space using a specified set of primaries 2052 * and a specified white point.</p> 2053 * 2054 * <p>The primaries and white point can be specified in the CIE xyY space 2055 * or in CIE XYZ. The length of the arrays depends on the chosen space:</p> 2056 * 2057 * <table summary="Parameters length"> 2058 * <tr><th>Space</th><th>Primaries length</th><th>White point length</th></tr> 2059 * <tr><td>xyY</td><td>6</td><td>2</td></tr> 2060 * <tr><td>XYZ</td><td>9</td><td>3</td></tr> 2061 * </table> 2062 * 2063 * <p>When the primaries and/or white point are specified in xyY, the Y component 2064 * does not need to be specified and is assumed to be 1.0. Only the xy components 2065 * are required.</p> 2066 * 2067 * @param name Name of the color space, cannot be null, its length must be >= 1 2068 * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats 2069 * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats 2070 * @param oetf Opto-electronic transfer function, cannot be null 2071 * @param eotf Electro-optical transfer function, cannot be null 2072 * @param min The minimum valid value in this color space's RGB range 2073 * @param max The maximum valid value in this color space's RGB range 2074 * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} 2075 * 2076 * @throws IllegalArgumentException If any of the following conditions is met: 2077 * <ul> 2078 * <li>The name is null or has a length of 0.</li> 2079 * <li>The primaries array is null or has a length that is neither 6 or 9.</li> 2080 * <li>The white point array is null or has a length that is neither 2 or 3.</li> 2081 * <li>The OETF is null or the EOTF is null.</li> 2082 * <li>The minimum valid value is >= the maximum valid value.</li> 2083 * <li>The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.</li> 2084 * </ul> 2085 * 2086 * @see #get(Named) 2087 */ 2088 private Rgb( 2089 @NonNull @Size(min = 1) String name, 2090 @NonNull @Size(min = 6, max = 9) float[] primaries, 2091 @NonNull @Size(min = 2, max = 3) float[] whitePoint, 2092 @NonNull DoubleUnaryOperator oetf, 2093 @NonNull DoubleUnaryOperator eotf, 2094 float min, 2095 float max, 2096 @IntRange(from = MIN_ID, to = MAX_ID) int id) { 2097 2098 super(name, Model.RGB, id); 2099 2100 if (primaries == null || (primaries.length != 6 && primaries.length != 9)) { 2101 throw new IllegalArgumentException("The color space's primaries must be " + 2102 "defined as an array of 6 floats in xyY or 9 floats in XYZ"); 2103 } 2104 2105 if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) { 2106 throw new IllegalArgumentException("The color space's white point must be " + 2107 "defined as an array of 2 floats in xyY or 3 float in XYZ"); 2108 } 2109 2110 if (oetf == null || eotf == null) { 2111 throw new IllegalArgumentException("The transfer functions of a color space " + 2112 "cannot be null"); 2113 } 2114 2115 if (min >= max) { 2116 throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max + 2117 "; min must be strictly < max"); 2118 } 2119 2120 mWhitePoint = xyWhitePoint(whitePoint); 2121 mPrimaries = xyPrimaries(primaries); 2122 2123 mTransform = computeXYZMatrix(mPrimaries, mWhitePoint); 2124 mInverseTransform = inverse3x3(mTransform); 2125 2126 mOetf = oetf; 2127 mEotf = eotf; 2128 2129 mMin = min; 2130 mMax = max; 2131 2132 DoubleUnaryOperator clamp = this::clamp; 2133 mClampedOetf = oetf.andThen(clamp); 2134 mClampedEotf = clamp.andThen(eotf); 2135 2136 // A color space is wide-gamut if its area is >90% of NTSC 1953 and 2137 // if it entirely contains the Color space definition in xyY 2138 mIsWideGamut = isWideGamut(mPrimaries, min, max); 2139 mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id); 2140 } 2141 2142 /** 2143 * Creates a copy of the specified color space with a new transform. 2144 * 2145 * @param colorSpace The color space to create a copy of 2146 */ 2147 private Rgb(Rgb colorSpace, 2148 @NonNull @Size(9) float[] transform, 2149 @NonNull @Size(min = 2, max = 3) float[] whitePoint) { 2150 super(colorSpace.getName(), Model.RGB, -1); 2151 2152 mWhitePoint = xyWhitePoint(whitePoint); 2153 mPrimaries = colorSpace.mPrimaries; 2154 2155 mTransform = transform; 2156 mInverseTransform = inverse3x3(transform); 2157 2158 mMin = colorSpace.mMin; 2159 mMax = colorSpace.mMax; 2160 2161 mOetf = colorSpace.mOetf; 2162 mEotf = colorSpace.mEotf; 2163 2164 mClampedOetf = colorSpace.mClampedOetf; 2165 mClampedEotf = colorSpace.mClampedEotf; 2166 2167 mIsWideGamut = colorSpace.mIsWideGamut; 2168 mIsSrgb = colorSpace.mIsSrgb; 2169 } 2170 2171 /** 2172 * Copies the non-adapted CIE xyY white point of this color space in 2173 * specified array. The Y component is assumed to be 1 and is therefore 2174 * not copied into the destination. The x and y components are written 2175 * in the array at positions 0 and 1 respectively. 2176 * 2177 * @param whitePoint The destination array, cannot be null, its length 2178 * must be >= 2 2179 * 2180 * @return The destination array passed as a parameter 2181 * 2182 * @see #getWhitePoint(float[]) 2183 */ 2184 @NonNull 2185 @Size(min = 2) 2186 public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) { 2187 whitePoint[0] = mWhitePoint[0]; 2188 whitePoint[1] = mWhitePoint[1]; 2189 return whitePoint; 2190 } 2191 2192 /** 2193 * Returns the non-adapted CIE xyY white point of this color space as 2194 * a new array of 2 floats. The Y component is assumed to be 1 and is 2195 * therefore not copied into the destination. The x and y components 2196 * are written in the array at positions 0 and 1 respectively. 2197 * 2198 * @return A new non-null array of 2 floats 2199 * 2200 * @see #getWhitePoint() 2201 */ 2202 @NonNull 2203 @Size(2) 2204 public float[] getWhitePoint() { 2205 return Arrays.copyOf(mWhitePoint, mWhitePoint.length); 2206 } 2207 2208 /** 2209 * Copies the primaries of this color space in specified array. The Y 2210 * component is assumed to be 1 and is therefore not copied into the 2211 * destination. The x and y components of the first primary are written 2212 * in the array at positions 0 and 1 respectively. 2213 * 2214 * @param primaries The destination array, cannot be null, its length 2215 * must be >= 6 2216 * 2217 * @return The destination array passed as a parameter 2218 * 2219 * @see #getPrimaries(float[]) 2220 */ 2221 @NonNull 2222 @Size(min = 6) 2223 public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) { 2224 System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length); 2225 return primaries; 2226 } 2227 2228 /** 2229 * Returns the primaries of this color space as a new array of 6 floats. 2230 * The Y component is assumed to be 1 and is therefore not copied into 2231 * the destination. The x and y components of the first primary are 2232 * written in the array at positions 0 and 1 respectively. 2233 * 2234 * @return A new non-null array of 2 floats 2235 * 2236 * @see #getWhitePoint() 2237 */ 2238 @NonNull 2239 @Size(6) 2240 public float[] getPrimaries() { 2241 return Arrays.copyOf(mPrimaries, mPrimaries.length); 2242 } 2243 2244 /** 2245 * <p>Copies the transform of this color space in specified array. The 2246 * transform is used to convert from RGB to XYZ (with the same white 2247 * point as this color space). To connect color spaces, you must first 2248 * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the 2249 * same white point.</p> 2250 * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} 2251 * to convert between color spaces.</p> 2252 * 2253 * @param transform The destination array, cannot be null, its length 2254 * must be >= 9 2255 * 2256 * @return The destination array passed as a parameter 2257 * 2258 * @see #getInverseTransform() 2259 */ 2260 @NonNull 2261 @Size(min = 9) 2262 public float[] getTransform(@NonNull @Size(min = 9) float[] transform) { 2263 System.arraycopy(mTransform, 0, transform, 0, mTransform.length); 2264 return transform; 2265 } 2266 2267 /** 2268 * <p>Returns the transform of this color space as a new array. The 2269 * transform is used to convert from RGB to XYZ (with the same white 2270 * point as this color space). To connect color spaces, you must first 2271 * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the 2272 * same white point.</p> 2273 * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} 2274 * to convert between color spaces.</p> 2275 * 2276 * @return A new array of 9 floats 2277 * 2278 * @see #getInverseTransform(float[]) 2279 */ 2280 @NonNull 2281 @Size(9) 2282 public float[] getTransform() { 2283 return Arrays.copyOf(mTransform, mTransform.length); 2284 } 2285 2286 /** 2287 * <p>Copies the inverse transform of this color space in specified array. 2288 * The inverse transform is used to convert from XYZ to RGB (with the 2289 * same white point as this color space). To connect color spaces, you 2290 * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them 2291 * to the same white point.</p> 2292 * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} 2293 * to convert between color spaces.</p> 2294 * 2295 * @param inverseTransform The destination array, cannot be null, its length 2296 * must be >= 9 2297 * 2298 * @return The destination array passed as a parameter 2299 * 2300 * @see #getTransform() 2301 */ 2302 @NonNull 2303 @Size(min = 9) 2304 public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) { 2305 System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length); 2306 return inverseTransform; 2307 } 2308 2309 /** 2310 * <p>Returns the inverse transform of this color space as a new array. 2311 * The inverse transform is used to convert from XYZ to RGB (with the 2312 * same white point as this color space). To connect color spaces, you 2313 * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them 2314 * to the same white point.</p> 2315 * <p>It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} 2316 * to convert between color spaces.</p> 2317 * 2318 * @return A new array of 9 floats 2319 * 2320 * @see #getTransform(float[]) 2321 */ 2322 @NonNull 2323 @Size(9) 2324 public float[] getInverseTransform() { 2325 return Arrays.copyOf(mInverseTransform, mInverseTransform.length); 2326 } 2327 2328 /** 2329 * <p>Returns the opto-electronic transfer function (OETF) of this color space. 2330 * The inverse function is the electro-optical transfer function (EOTF) returned 2331 * by {@link #getEotf()}. These functions are defined to satisfy the following 2332 * equality for \(x \in [0..1]\):</p> 2333 * 2334 * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ 2335 * 2336 * <p>For RGB colors, this function can be used to convert from linear space 2337 * to "gamma space" (gamma encoded). The terms gamma space and gamma encoded 2338 * are frequently used because many OETFs can be closely approximated using 2339 * a simple power function of the form \(x^{\frac{1}{\gamma}}\) (the 2340 * approximation of the {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) 2341 * for instance).</p> 2342 * 2343 * @return A transfer function that converts from linear space to "gamma space" 2344 * 2345 * @see #getEotf() 2346 */ 2347 @NonNull 2348 public DoubleUnaryOperator getOetf() { 2349 return mOetf; 2350 } 2351 2352 /** 2353 * <p>Returns the electro-optical transfer function (EOTF) of this color space. 2354 * The inverse function is the opto-electronic transfer function (OETF) 2355 * returned by {@link #getOetf()}. These functions are defined to satisfy the 2356 * following equality for \(x \in [0..1]\):</p> 2357 * 2358 * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ 2359 * 2360 * <p>For RGB colors, this function can be used to convert from "gamma space" 2361 * (gamma encoded) to linear space. The terms gamma space and gamma encoded 2362 * are frequently used because many EOTFs can be closely approximated using 2363 * a simple power function of the form \(x^\gamma\) (the approximation of the 2364 * {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) for instance).</p> 2365 * 2366 * @return A transfer function that converts from "gamma space" to linear space 2367 * 2368 * @see #getOetf() 2369 */ 2370 @NonNull 2371 public DoubleUnaryOperator getEotf() { 2372 return mEotf; 2373 } 2374 2375 @Override 2376 public boolean isSrgb() { 2377 return mIsSrgb; 2378 } 2379 2380 @Override 2381 public boolean isWideGamut() { 2382 return mIsWideGamut; 2383 } 2384 2385 @Override 2386 public float getMinValue(int component) { 2387 return mMin; 2388 } 2389 2390 @Override 2391 public float getMaxValue(int component) { 2392 return mMax; 2393 } 2394 2395 /** 2396 * <p>Decodes an RGB value to linear space. This is achieved by 2397 * applying this color space's electro-optical transfer function 2398 * to the supplied values.</p> 2399 * 2400 * <p>Refer to the documentation of {@link ColorSpace.Rgb} for 2401 * more information about transfer functions and their use for 2402 * encoding and decoding RGB values.</p> 2403 * 2404 * @param r The red component to decode to linear space 2405 * @param g The green component to decode to linear space 2406 * @param b The blue component to decode to linear space 2407 * @return A new array of 3 floats containing linear RGB values 2408 * 2409 * @see #toLinear(float[]) 2410 * @see #fromLinear(float, float, float) 2411 */ 2412 @NonNull 2413 @Size(3) 2414 public float[] toLinear(float r, float g, float b) { 2415 return toLinear(new float[] { r, g, b }); 2416 } 2417 2418 /** 2419 * <p>Decodes an RGB value to linear space. This is achieved by 2420 * applying this color space's electro-optical transfer function 2421 * to the first 3 values of the supplied array. The result is 2422 * stored back in the input array.</p> 2423 * 2424 * <p>Refer to the documentation of {@link ColorSpace.Rgb} for 2425 * more information about transfer functions and their use for 2426 * encoding and decoding RGB values.</p> 2427 * 2428 * @param v A non-null array of non-linear RGB values, its length 2429 * must be at least 3 2430 * @return The specified array 2431 * 2432 * @see #toLinear(float, float, float) 2433 * @see #fromLinear(float[]) 2434 */ 2435 @NonNull 2436 @Size(min = 3) 2437 public float[] toLinear(@NonNull @Size(min = 3) float[] v) { 2438 v[0] = (float) mClampedEotf.applyAsDouble(v[0]); 2439 v[1] = (float) mClampedEotf.applyAsDouble(v[1]); 2440 v[2] = (float) mClampedEotf.applyAsDouble(v[2]); 2441 return v; 2442 } 2443 2444 /** 2445 * <p>Encodes an RGB value from linear space to this color space's 2446 * "gamma space". This is achieved by applying this color space's 2447 * opto-electronic transfer function to the supplied values.</p> 2448 * 2449 * <p>Refer to the documentation of {@link ColorSpace.Rgb} for 2450 * more information about transfer functions and their use for 2451 * encoding and decoding RGB values.</p> 2452 * 2453 * @param r The red component to encode from linear space 2454 * @param g The green component to encode from linear space 2455 * @param b The blue component to encode from linear space 2456 * @return A new array of 3 floats containing non-linear RGB values 2457 * 2458 * @see #fromLinear(float[]) 2459 * @see #toLinear(float, float, float) 2460 */ 2461 @NonNull 2462 @Size(3) 2463 public float[] fromLinear(float r, float g, float b) { 2464 return fromLinear(new float[] { r, g, b }); 2465 } 2466 2467 /** 2468 * <p>Encodes an RGB value from linear space to this color space's 2469 * "gamma space". This is achieved by applying this color space's 2470 * opto-electronic transfer function to the first 3 values of the 2471 * supplied array. The result is stored back in the input array.</p> 2472 * 2473 * <p>Refer to the documentation of {@link ColorSpace.Rgb} for 2474 * more information about transfer functions and their use for 2475 * encoding and decoding RGB values.</p> 2476 * 2477 * @param v A non-null array of linear RGB values, its length 2478 * must be at least 3 2479 * @return A new array of 3 floats containing non-linear RGB values 2480 * 2481 * @see #fromLinear(float[]) 2482 * @see #toLinear(float, float, float) 2483 */ 2484 @NonNull 2485 @Size(min = 3) 2486 public float[] fromLinear(@NonNull @Size(min = 3) float[] v) { 2487 v[0] = (float) mClampedOetf.applyAsDouble(v[0]); 2488 v[1] = (float) mClampedOetf.applyAsDouble(v[1]); 2489 v[2] = (float) mClampedOetf.applyAsDouble(v[2]); 2490 return v; 2491 } 2492 2493 @Override 2494 @NonNull 2495 @Size(min = 3) 2496 public float[] toXyz(@NonNull @Size(min = 3) float[] v) { 2497 v[0] = (float) mClampedEotf.applyAsDouble(v[0]); 2498 v[1] = (float) mClampedEotf.applyAsDouble(v[1]); 2499 v[2] = (float) mClampedEotf.applyAsDouble(v[2]); 2500 return mul3x3Float3(mTransform, v); 2501 } 2502 2503 @Override 2504 @NonNull 2505 @Size(min = 3) 2506 public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { 2507 mul3x3Float3(mInverseTransform, v); 2508 v[0] = (float) mClampedOetf.applyAsDouble(v[0]); 2509 v[1] = (float) mClampedOetf.applyAsDouble(v[1]); 2510 v[2] = (float) mClampedOetf.applyAsDouble(v[2]); 2511 return v; 2512 } 2513 2514 private double clamp(double x) { 2515 return x < mMin ? mMin : x > mMax ? mMax : x; 2516 } 2517 2518 @Override 2519 public boolean equals(Object o) { 2520 if (this == o) return true; 2521 if (o == null || getClass() != o.getClass()) return false; 2522 if (!super.equals(o)) return false; 2523 2524 Rgb rgb = (Rgb) o; 2525 2526 if (Float.compare(rgb.mMin, mMin) != 0) return false; 2527 if (Float.compare(rgb.mMax, mMax) != 0) return false; 2528 if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false; 2529 if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false; 2530 //noinspection SimplifiableIfStatement 2531 if (!mOetf.equals(rgb.mOetf)) return false; 2532 return mEotf.equals(rgb.mEotf); 2533 } 2534 2535 @Override 2536 public int hashCode() { 2537 int result = super.hashCode(); 2538 result = 31 * result + Arrays.hashCode(mWhitePoint); 2539 result = 31 * result + Arrays.hashCode(mPrimaries); 2540 result = 31 * result + mOetf.hashCode(); 2541 result = 31 * result + mEotf.hashCode(); 2542 result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0); 2543 result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0); 2544 return result; 2545 } 2546 2547 /** 2548 * Computes whether a color space is the sRGB color space or at least 2549 * a close approximation. 2550 * 2551 * @param primaries The set of RGB primaries in xyY as an array of 6 floats 2552 * @param whitePoint The white point in xyY as an array of 2 floats 2553 * @param OETF The opto-electronic transfer function 2554 * @param EOTF The electro-optical transfer function 2555 * @param min The minimum value of the color space's range 2556 * @param max The minimum value of the color space's range 2557 * @param id The ID of the color space 2558 * @return True if the color space can be considered as the sRGB color space 2559 * 2560 * @see #isSrgb() 2561 */ 2562 @SuppressWarnings("RedundantIfStatement") 2563 private static boolean isSrgb( 2564 @NonNull @Size(6) float[] primaries, 2565 @NonNull @Size(2) float[] whitePoint, 2566 @NonNull DoubleUnaryOperator OETF, 2567 @NonNull DoubleUnaryOperator EOTF, 2568 float min, 2569 float max, 2570 @IntRange(from = MIN_ID, to = MAX_ID) int id) { 2571 if (id == 0) return true; 2572 if (!compare(primaries, SRGB_PRIMARIES)) { 2573 return false; 2574 } 2575 if (!compare(whitePoint, ILLUMINANT_D65)) { 2576 return false; 2577 } 2578 if (OETF.applyAsDouble(0.5) < 0.5001) return false; 2579 if (EOTF.applyAsDouble(0.5) > 0.5001) return false; 2580 if (min != 0.0f) return false; 2581 if (max != 1.0f) return false; 2582 return true; 2583 } 2584 2585 /** 2586 * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form 2587 * a wide color gamut. A color gamut is considered wide if its area is > 90% 2588 * of the area of NTSC 1953 and if it contains the sRGB color gamut entirely. 2589 * If the conditions above are not met, the color space is considered as having 2590 * a wide color gamut if its range is larger than [0..1]. 2591 * 2592 * @param primaries RGB primaries in CIE xyY as an array of 6 floats 2593 * @param min The minimum value of the color space's range 2594 * @param max The minimum value of the color space's range 2595 * @return True if the color space has a wide gamut, false otherwise 2596 * 2597 * @see #isWideGamut() 2598 * @see #area(float[]) 2599 */ 2600 private static boolean isWideGamut(@NonNull @Size(6) float[] primaries, 2601 float min, float max) { 2602 return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f && 2603 contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f); 2604 } 2605 2606 /** 2607 * Computes the area of the triangle represented by a set of RGB primaries 2608 * in the CIE xyY space. 2609 * 2610 * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats 2611 * @return The area of the triangle 2612 * 2613 * @see #isWideGamut(float[], float, float) 2614 */ 2615 private static float area(@NonNull @Size(6) float[] primaries) { 2616 float Rx = primaries[0]; 2617 float Ry = primaries[1]; 2618 float Gx = primaries[2]; 2619 float Gy = primaries[3]; 2620 float Bx = primaries[4]; 2621 float By = primaries[5]; 2622 float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By; 2623 float r = 0.5f * det; 2624 return r < 0.0f ? -r : r; 2625 } 2626 2627 /** 2628 * Computes the cross product of two 2D vectors. 2629 * 2630 * @param ax The x coordinate of the first vector 2631 * @param ay The y coordinate of the first vector 2632 * @param bx The x coordinate of the second vector 2633 * @param by The y coordinate of the second vector 2634 * @return The result of a x b 2635 */ 2636 private static float cross(float ax, float ay, float bx, float by) { 2637 return ax * by - ay * bx; 2638 } 2639 2640 /** 2641 * Decides whether a 2D triangle, identified by the 6 coordinates of its 2642 * 3 vertices, is contained within another 2D triangle, also identified 2643 * by the 6 coordinates of its 3 vertices. 2644 * 2645 * In the illustration below, we want to test whether the RGB triangle 2646 * is contained within the triangle XYZ formed by the 3 vertices at 2647 * the "+" locations. 2648 * 2649 * Y . 2650 * . + . 2651 * . .. 2652 * . . 2653 * . . 2654 * . G 2655 * * 2656 * * * 2657 * ** * 2658 * * ** 2659 * * * 2660 * ** * 2661 * * * 2662 * * * 2663 * ** * 2664 * * * 2665 * * ** 2666 * ** * R ... 2667 * * * ..... 2668 * * ***** .. 2669 * ** ************ . + 2670 * B * ************ . X 2671 * ......***** . 2672 * ...... . . 2673 * .. 2674 * + . 2675 * Z . 2676 * 2677 * RGB is contained within XYZ if all the following conditions are true 2678 * (with "x" the cross product operator): 2679 * 2680 * --> --> 2681 * GR x RX >= 0 2682 * --> --> 2683 * RX x BR >= 0 2684 * --> --> 2685 * RG x GY >= 0 2686 * --> --> 2687 * GY x RG >= 0 2688 * --> --> 2689 * RB x BZ >= 0 2690 * --> --> 2691 * BZ x GB >= 0 2692 * 2693 * @param p1 The enclosing triangle 2694 * @param p2 The enclosed triangle 2695 * @return True if the triangle p1 contains the triangle p2 2696 * 2697 * @see #isWideGamut(float[], float, float) 2698 */ 2699 @SuppressWarnings("RedundantIfStatement") 2700 private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) { 2701 // Translate the vertices p1 in the coordinates system 2702 // with the vertices p2 as the origin 2703 float[] p0 = new float[] { 2704 p1[0] - p2[0], p1[1] - p2[1], 2705 p1[2] - p2[2], p1[3] - p2[3], 2706 p1[4] - p2[4], p1[5] - p2[5], 2707 }; 2708 // Check the first vertex of p1 2709 if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 || 2710 cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) { 2711 return false; 2712 } 2713 // Check the second vertex of p1 2714 if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 || 2715 cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) { 2716 return false; 2717 } 2718 // Check the third vertex of p1 2719 if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 || 2720 cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) { 2721 return false; 2722 } 2723 return true; 2724 } 2725 2726 /** 2727 * Computes the primaries of a color space identified only by 2728 * its RGB->XYZ transform matrix. This method assumes that the 2729 * range of the color space is [0..1]. 2730 * 2731 * @param toXYZ The color space's 3x3 transform matrix to XYZ 2732 * @param EOTF The color space's electro-optical transfer function 2733 * @return A new array of 6 floats containing the color space's 2734 * primaries in CIE xyY 2735 */ 2736 @NonNull 2737 @Size(6) 2738 private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ, 2739 DoubleUnaryOperator EOTF) { 2740 float one = (float) EOTF.applyAsDouble(1.0); 2741 float[] r = mul3x3Float3(toXYZ, new float[] { one, 0.0f, 0.0f }); 2742 float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, one, 0.0f }); 2743 float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, one }); 2744 2745 float rSum = r[0] + r[1] + r[2]; 2746 float gSum = g[0] + g[1] + g[2]; 2747 float bSum = b[0] + b[1] + b[2]; 2748 2749 return new float[] { 2750 r[0] / rSum, r[1] / rSum, 2751 g[0] / gSum, g[1] / gSum, 2752 b[0] / bSum, b[1] / bSum, 2753 }; 2754 } 2755 2756 /** 2757 * Computes the white point of a color space identified only by 2758 * its RGB->XYZ transform matrix. This method assumes that the 2759 * range of the color space is [0..1]. 2760 * 2761 * @param toXYZ The color space's 3x3 transform matrix to XYZ 2762 * @param EOTF The color space's electro-optical transfer function 2763 * @return A new array of 2 floats containing the color space's 2764 * white point in CIE xyY 2765 */ 2766 @NonNull 2767 @Size(2) 2768 private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ, 2769 @NonNull DoubleUnaryOperator EOTF) { 2770 float one = (float) EOTF.applyAsDouble(1.0); 2771 float[] w = mul3x3Float3(toXYZ, new float[] { one, one, one }); 2772 float sum = w[0] + w[1] + w[2]; 2773 return new float[] { w[0] / sum, w[1] / sum }; 2774 } 2775 2776 /** 2777 * Converts the specified RGB primaries point to xyY if needed. The primaries 2778 * can be specified as an array of 6 floats (in CIE xyY) or 9 floats 2779 * (in CIE XYZ). If no conversion is needed, the input array is copied. 2780 * 2781 * @param primaries The primaries in xyY or XYZ 2782 * @return A new array of 6 floats containing the primaries in xyY 2783 */ 2784 @NonNull 2785 @Size(6) 2786 private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) { 2787 float[] xyPrimaries = new float[6]; 2788 2789 // XYZ to xyY 2790 if (primaries.length == 9) { 2791 float sum; 2792 2793 sum = primaries[0] + primaries[1] + primaries[2]; 2794 xyPrimaries[0] = primaries[0] / sum; 2795 xyPrimaries[1] = primaries[1] / sum; 2796 2797 sum = primaries[3] + primaries[4] + primaries[5]; 2798 xyPrimaries[2] = primaries[3] / sum; 2799 xyPrimaries[3] = primaries[4] / sum; 2800 2801 sum = primaries[6] + primaries[7] + primaries[8]; 2802 xyPrimaries[4] = primaries[6] / sum; 2803 xyPrimaries[5] = primaries[7] / sum; 2804 } else { 2805 System.arraycopy(primaries, 0, xyPrimaries, 0, 6); 2806 } 2807 2808 return xyPrimaries; 2809 } 2810 2811 /** 2812 * Converts the specified white point to xyY if needed. The white point 2813 * can be specified as an array of 2 floats (in CIE xyY) or 3 floats 2814 * (in CIE XYZ). If no conversion is needed, the input array is copied. 2815 * 2816 * @param whitePoint The white point in xyY or XYZ 2817 * @return A new array of 2 floats containing the white point in xyY 2818 */ 2819 @NonNull 2820 @Size(2) 2821 private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) { 2822 float[] xyWhitePoint = new float[2]; 2823 2824 // XYZ to xyY 2825 if (whitePoint.length == 3) { 2826 float sum = whitePoint[0] + whitePoint[1] + whitePoint[2]; 2827 xyWhitePoint[0] = whitePoint[0] / sum; 2828 xyWhitePoint[1] = whitePoint[1] / sum; 2829 } else { 2830 System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2); 2831 } 2832 2833 return xyWhitePoint; 2834 } 2835 2836 /** 2837 * Computes the matrix that converts from RGB to XYZ based on RGB 2838 * primaries and a white point, both specified in the CIE xyY space. 2839 * The Y component of the primaries and white point is implied to be 1. 2840 * 2841 * @param primaries The RGB primaries in xyY, as an array of 6 floats 2842 * @param whitePoint The white point in xyY, as an array of 2 floats 2843 * @return A 3x3 matrix as a new array of 9 floats 2844 */ 2845 @NonNull 2846 @Size(9) 2847 private static float[] computeXYZMatrix( 2848 @NonNull @Size(6) float[] primaries, 2849 @NonNull @Size(2) float[] whitePoint) { 2850 float Rx = primaries[0]; 2851 float Ry = primaries[1]; 2852 float Gx = primaries[2]; 2853 float Gy = primaries[3]; 2854 float Bx = primaries[4]; 2855 float By = primaries[5]; 2856 float Wx = whitePoint[0]; 2857 float Wy = whitePoint[1]; 2858 2859 float oneRxRy = (1 - Rx) / Ry; 2860 float oneGxGy = (1 - Gx) / Gy; 2861 float oneBxBy = (1 - Bx) / By; 2862 float oneWxWy = (1 - Wx) / Wy; 2863 2864 float RxRy = Rx / Ry; 2865 float GxGy = Gx / Gy; 2866 float BxBy = Bx / By; 2867 float WxWy = Wx / Wy; 2868 2869 float BY = 2870 ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) / 2871 ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy)); 2872 float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy); 2873 float RY = 1 - GY - BY; 2874 2875 float RYRy = RY / Ry; 2876 float GYGy = GY / Gy; 2877 float BYBy = BY / By; 2878 2879 return new float[] { 2880 RYRy * Rx, RY, RYRy * (1 - Rx - Ry), 2881 GYGy * Gx, GY, GYGy * (1 - Gx - Gy), 2882 BYBy * Bx, BY, BYBy * (1 - Bx - By) 2883 }; 2884 } 2885 } 2886 2887 /** 2888 * {@usesMathJax} 2889 * 2890 * <p>A connector transforms colors from a source color space to a destination 2891 * color space.</p> 2892 * 2893 * <p>A source color space is connected to a destination color space using the 2894 * color transform \(C\) computed from their respective transforms noted 2895 * \(T_{src}\) and \(T_{dst}\) in the following equation:</p> 2896 * 2897 * $$C = T^{-1}_{dst} . T_{src}$$ 2898 * 2899 * <p>The transform \(C\) shown above is only valid when the source and 2900 * destination color spaces have the same profile connection space (PCS). 2901 * We know that instances of {@link ColorSpace} always use CIE XYZ as their 2902 * PCS but their white points might differ. When they do, we must perform 2903 * a chromatic adaptation of the color spaces' transforms. To do so, we 2904 * use the von Kries method described in the documentation of {@link Adaptation}, 2905 * using the CIE standard illuminant {@link ColorSpace#ILLUMINANT_D50 D50} 2906 * as the target white point.</p> 2907 * 2908 * <p>Example of conversion from {@link Named#SRGB sRGB} to 2909 * {@link Named#DCI_P3 DCI-P3}:</p> 2910 * 2911 * <pre class="prettyprint"> 2912 * ColorSpace.Connector connector = ColorSpace.connect( 2913 * ColorSpace.get(ColorSpace.Named.SRGB), 2914 * ColorSpace.get(ColorSpace.Named.DCI_P3)); 2915 * float[] p3 = connector.transform(1.0f, 0.0f, 0.0f); 2916 * // p3 contains { 0.9473, 0.2740, 0.2076 } 2917 * </pre> 2918 * 2919 * @see Adaptation 2920 * @see ColorSpace#adapt(ColorSpace, float[], Adaptation) 2921 * @see ColorSpace#adapt(ColorSpace, float[]) 2922 * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent) 2923 * @see ColorSpace#connect(ColorSpace, ColorSpace) 2924 * @see ColorSpace#connect(ColorSpace, RenderIntent) 2925 * @see ColorSpace#connect(ColorSpace) 2926 */ 2927 public static class Connector { 2928 @NonNull private final ColorSpace mSource; 2929 @NonNull private final ColorSpace mDestination; 2930 @NonNull private final ColorSpace mTransformSource; 2931 @NonNull private final ColorSpace mTransformDestination; 2932 @NonNull private final RenderIntent mIntent; 2933 @NonNull @Size(3) private final float[] mTransform; 2934 2935 /** 2936 * Creates a new connector between a source and a destination color space. 2937 * 2938 * @param source The source color space, cannot be null 2939 * @param destination The destination color space, cannot be null 2940 * @param intent The render intent to use when compressing gamuts 2941 */ 2942 Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination, 2943 @NonNull RenderIntent intent) { 2944 this(source, destination, 2945 source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source, 2946 destination.getModel() == Model.RGB ? 2947 adapt(destination, ILLUMINANT_D50_XYZ) : destination, 2948 intent, computeTransform(source, destination, intent)); 2949 } 2950 2951 /** 2952 * To connect between color spaces, we might need to use adapted transforms. 2953 * This should be transparent to the user so this constructor takes the 2954 * original source and destinations (returned by the getters), as well as 2955 * possibly adapted color spaces used by transform(). 2956 */ 2957 private Connector( 2958 @NonNull ColorSpace source, @NonNull ColorSpace destination, 2959 @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination, 2960 @NonNull RenderIntent intent, @Nullable @Size(3) float[] transform) { 2961 mSource = source; 2962 mDestination = destination; 2963 mTransformSource = transformSource; 2964 mTransformDestination = transformDestination; 2965 mIntent = intent; 2966 mTransform = transform; 2967 } 2968 2969 /** 2970 * Computes an extra transform to apply in XYZ space depending on the 2971 * selected rendering intent. 2972 */ 2973 private static float[] computeTransform(@NonNull ColorSpace source, 2974 @NonNull ColorSpace destination, @NonNull RenderIntent intent) { 2975 if (intent != RenderIntent.ABSOLUTE) return null; 2976 2977 boolean srcRGB = source.getModel() == Model.RGB; 2978 boolean dstRGB = destination.getModel() == Model.RGB; 2979 2980 if (srcRGB && dstRGB) return null; 2981 2982 if (srcRGB || dstRGB) { 2983 ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination); 2984 float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ; 2985 float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ; 2986 return new float[] { 2987 srcXYZ[0] / dstXYZ[0], 2988 srcXYZ[1] / dstXYZ[1], 2989 srcXYZ[2] / dstXYZ[2], 2990 }; 2991 } 2992 2993 return null; 2994 } 2995 2996 /** 2997 * Returns the source color space this connector will convert from. 2998 * 2999 * @return A non-null instance of {@link ColorSpace} 3000 * 3001 * @see #getDestination() 3002 */ 3003 @NonNull 3004 public ColorSpace getSource() { 3005 return mSource; 3006 } 3007 3008 /** 3009 * Returns the destination color space this connector will convert to. 3010 * 3011 * @return A non-null instance of {@link ColorSpace} 3012 * 3013 * @see #getSource() 3014 */ 3015 @NonNull 3016 public ColorSpace getDestination() { 3017 return mDestination; 3018 } 3019 3020 /** 3021 * Returns the render intent this connector will use when mapping the 3022 * source color space to the destination color space. 3023 * 3024 * @return A non-null {@link RenderIntent} 3025 * 3026 * @see RenderIntent 3027 */ 3028 public RenderIntent getIntent() { 3029 return mIntent; 3030 } 3031 3032 /** 3033 * <p>Transforms the specified color from the source color space 3034 * to a color in the destination color space. This convenience 3035 * method assumes a source color model with 3 components 3036 * (typically RGB). To transform from color models with more than 3037 * 3 components, such as {@link Model#CMYK CMYK}, use 3038 * {@link #transform(float[])} instead.</p> 3039 * 3040 * @param r The red component of the color to transform 3041 * @param g The green component of the color to transform 3042 * @param b The blue component of the color to transform 3043 * @return A new array of 3 floats containing the specified color 3044 * transformed from the source space to the destination space 3045 * 3046 * @see #transform(float[]) 3047 */ 3048 @NonNull 3049 @Size(3) 3050 public float[] transform(float r, float g, float b) { 3051 return transform(new float[] { r, g, b }); 3052 } 3053 3054 /** 3055 * <p>Transforms the specified color from the source color space 3056 * to a color in the destination color space.</p> 3057 * 3058 * @param v A non-null array of 3 floats containing the value to transform 3059 * and that will hold the result of the transform 3060 * @return The v array passed as a parameter, containing the specified color 3061 * transformed from the source space to the destination space 3062 * 3063 * @see #transform(float, float, float) 3064 */ 3065 @NonNull 3066 @Size(min = 3) 3067 public float[] transform(@NonNull @Size(min = 3) float[] v) { 3068 float[] xyz = mTransformSource.toXyz(v); 3069 if (mTransform != null) { 3070 xyz[0] *= mTransform[0]; 3071 xyz[1] *= mTransform[1]; 3072 xyz[2] *= mTransform[2]; 3073 } 3074 return mTransformDestination.fromXyz(xyz); 3075 } 3076 3077 /** 3078 * Optimized connector for RGB->RGB conversions. 3079 */ 3080 private static class Rgb extends Connector { 3081 @NonNull private final ColorSpace.Rgb mSource; 3082 @NonNull private final ColorSpace.Rgb mDestination; 3083 @NonNull private final float[] mTransform; 3084 3085 Rgb(@NonNull ColorSpace.Rgb source, @NonNull ColorSpace.Rgb destination, 3086 @NonNull RenderIntent intent) { 3087 super(source, destination, source, destination, intent, null); 3088 mSource = source; 3089 mDestination = destination; 3090 mTransform = computeTransform(source, destination, intent); 3091 } 3092 3093 @Override 3094 public float[] transform(@NonNull @Size(min = 3) float[] rgb) { 3095 rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]); 3096 rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]); 3097 rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]); 3098 mul3x3Float3(mTransform, rgb); 3099 rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]); 3100 rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]); 3101 rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]); 3102 return rgb; 3103 } 3104 3105 /** 3106 * <p>Computes the color transform that connects two RGB color spaces.</p> 3107 * 3108 * <p>We can only connect color spaces if they use the same profile 3109 * connection space. We assume the connection space is always 3110 * CIE XYZ but we maye need to perform a chromatic adaptation to 3111 * match the white points. If an adaptation is needed, we use the 3112 * CIE standard illuminant D50. The unmatched color space is adapted 3113 * using the von Kries transform and the {@link Adaptation#BRADFORD} 3114 * matrix.</p> 3115 * 3116 * @param source The source color space, cannot be null 3117 * @param destination The destination color space, cannot be null 3118 * @param intent The render intent to use when compressing gamuts 3119 * @return An array of 9 floats containing the 3x3 matrix transform 3120 */ 3121 @NonNull 3122 @Size(9) 3123 private static float[] computeTransform( 3124 @NonNull ColorSpace.Rgb source, 3125 @NonNull ColorSpace.Rgb destination, 3126 @NonNull RenderIntent intent) { 3127 if (compare(source.mWhitePoint, destination.mWhitePoint)) { 3128 // RGB->RGB using the PCS of both color spaces since they have the same 3129 return mul3x3(destination.mInverseTransform, source.mTransform); 3130 } else { 3131 // RGB->RGB using CIE XYZ D50 as the PCS 3132 float[] transform = source.mTransform; 3133 float[] inverseTransform = destination.mInverseTransform; 3134 3135 float[] srcXYZ = xyYToXyz(source.mWhitePoint); 3136 float[] dstXYZ = xyYToXyz(destination.mWhitePoint); 3137 3138 if (!compare(source.mWhitePoint, ILLUMINANT_D50)) { 3139 float[] srcAdaptation = chromaticAdaptation( 3140 Adaptation.BRADFORD.mTransform, srcXYZ, 3141 Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); 3142 transform = mul3x3(srcAdaptation, source.mTransform); 3143 } 3144 3145 if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) { 3146 float[] dstAdaptation = chromaticAdaptation( 3147 Adaptation.BRADFORD.mTransform, dstXYZ, 3148 Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); 3149 inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform)); 3150 } 3151 3152 if (intent == RenderIntent.ABSOLUTE) { 3153 transform = mul3x3Diag( 3154 new float[] { 3155 srcXYZ[0] / dstXYZ[0], 3156 srcXYZ[1] / dstXYZ[1], 3157 srcXYZ[2] / dstXYZ[2], 3158 }, transform); 3159 } 3160 3161 return mul3x3(inverseTransform, transform); 3162 } 3163 } 3164 } 3165 3166 /** 3167 * Returns the identity connector for a given color space. 3168 * 3169 * @param source The source and destination color space 3170 * @return A non-null connector that does not perform any transform 3171 * 3172 * @see ColorSpace#connect(ColorSpace, ColorSpace) 3173 */ 3174 static Connector identity(ColorSpace source) { 3175 return new Connector(source, source, RenderIntent.RELATIVE) { 3176 @Override 3177 public float[] transform(@NonNull @Size(min = 3) float[] v) { 3178 return v; 3179 } 3180 }; 3181 } 3182 } 3183 3184 /** 3185 * <p>A color space renderer can be used to visualize and compare the gamut and 3186 * white point of one or more color spaces. The output is an sRGB {@link Bitmap} 3187 * showing a CIE 1931 xyY or a CIE 1976 UCS chromaticity diagram.</p> 3188 * 3189 * <p>The following code snippet shows how to compare the {@link Named#SRGB} 3190 * and {@link Named#DCI_P3} color spaces in a CIE 1931 diagram:</p> 3191 * 3192 * <pre class="prettyprint"> 3193 * Bitmap bitmap = ColorSpace.createRenderer() 3194 * .size(768) 3195 * .clip(true) 3196 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3197 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3198 * .render(); 3199 * </pre> 3200 * <p> 3201 * <img src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" /> 3202 * <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption> 3203 * </p> 3204 * 3205 * <p>A renderer can also be used to show the location of specific colors, 3206 * associated with a color space, in the CIE 1931 xyY chromaticity diagram. 3207 * See {@link #add(ColorSpace, float, float, float, int)} for more information.</p> 3208 * 3209 * @see ColorSpace#createRenderer() 3210 */ 3211 public static class Renderer { 3212 private static final int NATIVE_SIZE = 1440; 3213 private static final float UCS_SCALE = 9.0f / 6.0f; 3214 3215 // Number of subdivision of the inside of the spectral locus 3216 private static final int CHROMATICITY_RESOLUTION = 32; 3217 private static final double ONE_THIRD = 1.0 / 3.0; 3218 3219 @IntRange(from = 128, to = Integer.MAX_VALUE) 3220 private int mSize = 1024; 3221 3222 private boolean mShowWhitePoint = true; 3223 private boolean mClip = false; 3224 private boolean mUcs = false; 3225 3226 private final List<Pair<ColorSpace, Integer>> mColorSpaces = new ArrayList<>(2); 3227 private final List<Point> mPoints = new ArrayList<>(0); 3228 3229 private Renderer() { 3230 } 3231 3232 /** 3233 * <p>Defines whether the chromaticity diagram should be clipped by the first 3234 * registered color space. The default value is false.</p> 3235 * 3236 * <p>The following code snippet and image show the default behavior:</p> 3237 * <pre class="prettyprint"> 3238 * Bitmap bitmap = ColorSpace.createRenderer() 3239 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3240 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3241 * .render(); 3242 * </pre> 3243 * <p> 3244 * <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" /> 3245 * <figcaption style="text-align: center;">Clipping disabled</figcaption> 3246 * </p> 3247 * 3248 * <p>Here is the same example with clipping enabled:</p> 3249 * <pre class="prettyprint"> 3250 * Bitmap bitmap = ColorSpace.createRenderer() 3251 * .clip(true) 3252 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3253 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3254 * .render(); 3255 * </pre> 3256 * <p> 3257 * <img src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" /> 3258 * <figcaption style="text-align: center;">Clipping enabled</figcaption> 3259 * </p> 3260 * 3261 * @param clip True to clip the chromaticity diagram to the first registered color space, 3262 * false otherwise 3263 * @return This instance of {@link Renderer} 3264 */ 3265 @NonNull 3266 public Renderer clip(boolean clip) { 3267 mClip = clip; 3268 return this; 3269 } 3270 3271 /** 3272 * <p>Defines whether the chromaticity diagram should use the uniform 3273 * chromaticity scale (CIE 1976 UCS). When the uniform chromaticity scale 3274 * is used, the distance between two points on the diagram is approximately 3275 * proportional to the perceived color difference.</p> 3276 * 3277 * <p>The following code snippet shows how to enable the uniform chromaticity 3278 * scale. The image below shows the result:</p> 3279 * <pre class="prettyprint"> 3280 * Bitmap bitmap = ColorSpace.createRenderer() 3281 * .uniformChromaticityScale(true) 3282 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3283 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3284 * .render(); 3285 * </pre> 3286 * <p> 3287 * <img src="{@docRoot}reference/android/images/graphics/colorspace_ucs.png" /> 3288 * <figcaption style="text-align: center;">CIE 1976 UCS diagram</figcaption> 3289 * </p> 3290 * 3291 * @param ucs True to render the chromaticity diagram as the CIE 1976 UCS diagram 3292 * @return This instance of {@link Renderer} 3293 */ 3294 @NonNull 3295 public Renderer uniformChromaticityScale(boolean ucs) { 3296 mUcs = ucs; 3297 return this; 3298 } 3299 3300 /** 3301 * Sets the dimensions (width and height) in pixels of the output bitmap. 3302 * The size must be at least 128px and defaults to 1024px. 3303 * 3304 * @param size The size in pixels of the output bitmap 3305 * @return This instance of {@link Renderer} 3306 */ 3307 @NonNull 3308 public Renderer size(@IntRange(from = 128, to = Integer.MAX_VALUE) int size) { 3309 mSize = Math.max(128, size); 3310 return this; 3311 } 3312 3313 /** 3314 * Shows or hides the white point of each color space in the output bitmap. 3315 * The default is true. 3316 * 3317 * @param show True to show the white point of each color space, false 3318 * otherwise 3319 * @return This instance of {@link Renderer} 3320 */ 3321 @NonNull 3322 public Renderer showWhitePoint(boolean show) { 3323 mShowWhitePoint = show; 3324 return this; 3325 } 3326 3327 /** 3328 * <p>Adds a color space to represent on the output CIE 1931 chromaticity 3329 * diagram. The color space is represented as a triangle showing the 3330 * footprint of its color gamut and, optionally, the location of its 3331 * white point.</p> 3332 * 3333 * <p class="note">Color spaces with a color model that is not RGB are 3334 * accepted but ignored.</p> 3335 * 3336 * <p>The following code snippet and image show an example of calling this 3337 * method to compare {@link Named#SRGB sRGB} and {@link Named#DCI_P3 DCI-P3}:</p> 3338 * <pre class="prettyprint"> 3339 * Bitmap bitmap = ColorSpace.createRenderer() 3340 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3341 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3342 * .render(); 3343 * </pre> 3344 * <p> 3345 * <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" /> 3346 * <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption> 3347 * </p> 3348 * 3349 * <p>Adding a color space extending beyond the boundaries of the 3350 * spectral locus will alter the size of the diagram within the output 3351 * bitmap as shown in this example:</p> 3352 * <pre class="prettyprint"> 3353 * Bitmap bitmap = ColorSpace.createRenderer() 3354 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3355 * .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845) 3356 * .add(ColorSpace.get(ColorSpace.Named.ACES), 0xff097ae9) 3357 * .add(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), 0xff000000) 3358 * .render(); 3359 * </pre> 3360 * <p> 3361 * <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison2.png" /> 3362 * <figcaption style="text-align: center;">sRGB, DCI-P3, ACES and scRGB</figcaption> 3363 * </p> 3364 * 3365 * @param colorSpace The color space whose gamut to render on the diagram 3366 * @param color The sRGB color to use to render the color space's gamut and white point 3367 * @return This instance of {@link Renderer} 3368 * 3369 * @see #clip(boolean) 3370 * @see #showWhitePoint(boolean) 3371 */ 3372 @NonNull 3373 public Renderer add(@NonNull ColorSpace colorSpace, @ColorInt int color) { 3374 mColorSpaces.add(new Pair<>(colorSpace, color)); 3375 return this; 3376 } 3377 3378 /** 3379 * <p>Adds a color to represent as a point on the chromaticity diagram. 3380 * The color is associated with a color space which will be used to 3381 * perform the conversion to CIE XYZ and compute the location of the point 3382 * on the diagram. The point is rendered as a colored circle.</p> 3383 * 3384 * <p>The following code snippet and image show an example of calling this 3385 * method to render the location of several sRGB colors as white circles:</p> 3386 * <pre class="prettyprint"> 3387 * Bitmap bitmap = ColorSpace.createRenderer() 3388 * .clip(true) 3389 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff) 3390 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.0f, 0.1f, 0xffffffff) 3391 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.1f, 0.1f, 0xffffffff) 3392 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.2f, 0.1f, 0xffffffff) 3393 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.3f, 0.1f, 0xffffffff) 3394 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.4f, 0.1f, 0xffffffff) 3395 * .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.5f, 0.1f, 0xffffffff) 3396 * .render(); 3397 * </pre> 3398 * <p> 3399 * <img src="{@docRoot}reference/android/images/graphics/colorspace_points.png" /> 3400 * <figcaption style="text-align: center;"> 3401 * Locating colors on the chromaticity diagram 3402 * </figcaption> 3403 * </p> 3404 * 3405 * @param colorSpace The color space of the color to locate on the diagram 3406 * @param r The first component of the color to locate on the diagram 3407 * @param g The second component of the color to locate on the diagram 3408 * @param b The third component of the color to locate on the diagram 3409 * @param pointColor The sRGB color to use to render the point on the diagram 3410 * @return This instance of {@link Renderer} 3411 */ 3412 @NonNull 3413 public Renderer add(@NonNull ColorSpace colorSpace, float r, float g, float b, 3414 @ColorInt int pointColor) { 3415 mPoints.add(new Point(colorSpace, new float[] { r, g, b }, pointColor)); 3416 return this; 3417 } 3418 3419 /** 3420 * <p>Renders the {@link #add(ColorSpace, int) color spaces} and 3421 * {@link #add(ColorSpace, float, float, float, int) points} registered 3422 * with this renderer. The output bitmap is an sRGB image with the 3423 * dimensions specified by calling {@link #size(int)} (1204x1024px by 3424 * default).</p> 3425 * 3426 * @return A new non-null {@link Bitmap} with the dimensions specified 3427 * by {@link #size(int)} (1024x1024 by default) 3428 */ 3429 @NonNull 3430 public Bitmap render() { 3431 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 3432 Bitmap bitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ARGB_8888); 3433 Canvas canvas = new Canvas(bitmap); 3434 3435 float[] primaries = new float[6]; 3436 float[] whitePoint = new float[2]; 3437 3438 int width = NATIVE_SIZE; 3439 int height = NATIVE_SIZE; 3440 3441 Path path = new Path(); 3442 3443 setTransform(canvas, width, height, primaries); 3444 drawBox(canvas, width, height, paint, path); 3445 setUcsTransform(canvas, height); 3446 drawLocus(canvas, width, height, paint, path, primaries); 3447 drawGamuts(canvas, width, height, paint, path, primaries, whitePoint); 3448 drawPoints(canvas, width, height, paint); 3449 3450 return bitmap; 3451 } 3452 3453 /** 3454 * Draws registered points at their correct position in the xyY coordinates. 3455 * Each point is positioned according to its associated color space. 3456 * 3457 * @param canvas The canvas to transform 3458 * @param width Width in pixel of the final image 3459 * @param height Height in pixel of the final image 3460 * @param paint A pre-allocated paint used to avoid temporary allocations 3461 */ 3462 private void drawPoints(@NonNull Canvas canvas, int width, int height, 3463 @NonNull Paint paint) { 3464 3465 paint.setStyle(Paint.Style.FILL); 3466 3467 float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f); 3468 3469 float[] v = new float[3]; 3470 float[] xy = new float[2]; 3471 3472 for (final Point point : mPoints) { 3473 v[0] = point.mRgb[0]; 3474 v[1] = point.mRgb[1]; 3475 v[2] = point.mRgb[2]; 3476 point.mColorSpace.toXyz(v); 3477 3478 paint.setColor(point.mColor); 3479 3480 // XYZ to xyY, assuming Y=1.0, then to L*u*v* if needed 3481 float sum = v[0] + v[1] + v[2]; 3482 xy[0] = v[0] / sum; 3483 xy[1] = v[1] / sum; 3484 if (mUcs) xyYToUv(xy); 3485 3486 canvas.drawCircle(width * xy[0], height - height * xy[1], radius, paint); 3487 } 3488 } 3489 3490 /** 3491 * Draws the color gamuts and white points of all the registered color 3492 * spaces. Only color spaces with an RGB color model are rendered, the 3493 * others are ignored. 3494 * 3495 * @param canvas The canvas to transform 3496 * @param width Width in pixel of the final image 3497 * @param height Height in pixel of the final image 3498 * @param paint A pre-allocated paint used to avoid temporary allocations 3499 * @param path A pre-allocated path used to avoid temporary allocations 3500 * @param primaries A pre-allocated array of 6 floats to avoid temporary allocations 3501 * @param whitePoint A pre-allocated array of 2 floats to avoid temporary allocations 3502 */ 3503 private void drawGamuts( 3504 @NonNull Canvas canvas, int width, int height, 3505 @NonNull Paint paint, @NonNull Path path, 3506 @NonNull @Size(6) float[] primaries, @NonNull @Size(2) float[] whitePoint) { 3507 3508 float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f); 3509 3510 for (final Pair<ColorSpace, Integer> item : mColorSpaces) { 3511 ColorSpace colorSpace = item.first; 3512 int color = item.second; 3513 3514 if (colorSpace.getModel() != Model.RGB) continue; 3515 3516 Rgb rgb = (Rgb) colorSpace; 3517 getPrimaries(rgb, primaries, mUcs); 3518 3519 path.rewind(); 3520 path.moveTo(width * primaries[0], height - height * primaries[1]); 3521 path.lineTo(width * primaries[2], height - height * primaries[3]); 3522 path.lineTo(width * primaries[4], height - height * primaries[5]); 3523 path.close(); 3524 3525 paint.setStyle(Paint.Style.STROKE); 3526 paint.setColor(color); 3527 canvas.drawPath(path, paint); 3528 3529 // Draw the white point 3530 if (mShowWhitePoint) { 3531 rgb.getWhitePoint(whitePoint); 3532 if (mUcs) xyYToUv(whitePoint); 3533 3534 paint.setStyle(Paint.Style.FILL); 3535 paint.setColor(color); 3536 canvas.drawCircle( 3537 width * whitePoint[0], height - height * whitePoint[1], radius, paint); 3538 } 3539 } 3540 } 3541 3542 /** 3543 * Returns the primaries of the specified RGB color space. This method handles 3544 * the special case of the {@link Named#EXTENDED_SRGB} family of color spaces. 3545 * 3546 * @param rgb The color space whose primaries to extract 3547 * @param primaries A pre-allocated array of 6 floats that will hold the result 3548 * @param asUcs True if the primaries should be returned in Luv, false for xyY 3549 */ 3550 @NonNull 3551 @Size(6) 3552 private static float[] getPrimaries(@NonNull Rgb rgb, 3553 @NonNull @Size(6) float[] primaries, boolean asUcs) { 3554 // TODO: We should find a better way to handle these cases 3555 if (rgb.equals(ColorSpace.get(Named.EXTENDED_SRGB)) || 3556 rgb.equals(ColorSpace.get(Named.LINEAR_EXTENDED_SRGB))) { 3557 primaries[0] = 1.41f; 3558 primaries[1] = 0.33f; 3559 primaries[2] = 0.27f; 3560 primaries[3] = 1.24f; 3561 primaries[4] = -0.23f; 3562 primaries[5] = -0.57f; 3563 } else { 3564 rgb.getPrimaries(primaries); 3565 } 3566 if (asUcs) xyYToUv(primaries); 3567 return primaries; 3568 } 3569 3570 /** 3571 * Draws the CIE 1931 chromaticity diagram: the spectral locus and its inside. 3572 * This method respect the clip parameter. 3573 * 3574 * @param canvas The canvas to transform 3575 * @param width Width in pixel of the final image 3576 * @param height Height in pixel of the final image 3577 * @param paint A pre-allocated paint used to avoid temporary allocations 3578 * @param path A pre-allocated path used to avoid temporary allocations 3579 * @param primaries A pre-allocated array of 6 floats to avoid temporary allocations 3580 */ 3581 private void drawLocus( 3582 @NonNull Canvas canvas, int width, int height, @NonNull Paint paint, 3583 @NonNull Path path, @NonNull @Size(6) float[] primaries) { 3584 3585 int vertexCount = SPECTRUM_LOCUS_X.length * CHROMATICITY_RESOLUTION * 6; 3586 float[] vertices = new float[vertexCount * 2]; 3587 int[] colors = new int[vertices.length]; 3588 computeChromaticityMesh(vertices, colors); 3589 3590 if (mUcs) xyYToUv(vertices); 3591 for (int i = 0; i < vertices.length; i += 2) { 3592 vertices[i] *= width; 3593 vertices[i + 1] = height - vertices[i + 1] * height; 3594 } 3595 3596 // Draw the spectral locus 3597 if (mClip && mColorSpaces.size() > 0) { 3598 for (final Pair<ColorSpace, Integer> item : mColorSpaces) { 3599 ColorSpace colorSpace = item.first; 3600 if (colorSpace.getModel() != Model.RGB) continue; 3601 3602 Rgb rgb = (Rgb) colorSpace; 3603 getPrimaries(rgb, primaries, mUcs); 3604 3605 break; 3606 } 3607 3608 path.rewind(); 3609 path.moveTo(width * primaries[0], height - height * primaries[1]); 3610 path.lineTo(width * primaries[2], height - height * primaries[3]); 3611 path.lineTo(width * primaries[4], height - height * primaries[5]); 3612 path.close(); 3613 3614 int[] solid = new int[colors.length]; 3615 Arrays.fill(solid, 0xff6c6c6c); 3616 canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0, 3617 null, 0, solid, 0, null, 0, 0, paint); 3618 3619 canvas.save(); 3620 canvas.clipPath(path); 3621 3622 canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0, 3623 null, 0, colors, 0, null, 0, 0, paint); 3624 3625 canvas.restore(); 3626 } else { 3627 canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0, 3628 null, 0, colors, 0, null, 0, 0, paint); 3629 } 3630 3631 // Draw the non-spectral locus 3632 int index = (CHROMATICITY_RESOLUTION - 1) * 12; 3633 path.reset(); 3634 path.moveTo(vertices[index], vertices[index + 1]); 3635 for (int x = 2; x < SPECTRUM_LOCUS_X.length; x++) { 3636 index += CHROMATICITY_RESOLUTION * 12; 3637 path.lineTo(vertices[index], vertices[index + 1]); 3638 } 3639 path.close(); 3640 3641 paint.setStrokeWidth(4.0f / (mUcs ? UCS_SCALE : 1.0f)); 3642 paint.setStyle(Paint.Style.STROKE); 3643 paint.setColor(0xff000000); 3644 canvas.drawPath(path, paint); 3645 } 3646 3647 /** 3648 * Draws the diagram box, including borders, tick marks, grid lines 3649 * and axis labels. 3650 * 3651 * @param canvas The canvas to transform 3652 * @param width Width in pixel of the final image 3653 * @param height Height in pixel of the final image 3654 * @param paint A pre-allocated paint used to avoid temporary allocations 3655 * @param path A pre-allocated path used to avoid temporary allocations 3656 */ 3657 private void drawBox(@NonNull Canvas canvas, int width, int height, @NonNull Paint paint, 3658 @NonNull Path path) { 3659 3660 int lineCount = 10; 3661 float scale = 1.0f; 3662 if (mUcs) { 3663 lineCount = 7; 3664 scale = UCS_SCALE; 3665 } 3666 3667 // Draw the unit grid 3668 paint.setStyle(Paint.Style.STROKE); 3669 paint.setStrokeWidth(2.0f); 3670 paint.setColor(0xffc0c0c0); 3671 3672 for (int i = 1; i < lineCount - 1; i++) { 3673 float v = i / 10.0f; 3674 float x = (width * v) * scale; 3675 float y = height - (height * v) * scale; 3676 3677 canvas.drawLine(0.0f, y, 0.9f * width, y, paint); 3678 canvas.drawLine(x, height, x, 0.1f * height, paint); 3679 } 3680 3681 // Draw tick marks 3682 paint.setStrokeWidth(4.0f); 3683 paint.setColor(0xff000000); 3684 for (int i = 1; i < lineCount - 1; i++) { 3685 float v = i / 10.0f; 3686 float x = (width * v) * scale; 3687 float y = height - (height * v) * scale; 3688 3689 canvas.drawLine(0.0f, y, width / 100.0f, y, paint); 3690 canvas.drawLine(x, height, x, height - (height / 100.0f), paint); 3691 } 3692 3693 // Draw the axis labels 3694 paint.setStyle(Paint.Style.FILL); 3695 paint.setTextSize(36.0f); 3696 paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); 3697 3698 Rect bounds = new Rect(); 3699 for (int i = 1; i < lineCount - 1; i++) { 3700 String text = "0." + i; 3701 paint.getTextBounds(text, 0, text.length(), bounds); 3702 3703 float v = i / 10.0f; 3704 float x = (width * v) * scale; 3705 float y = height - (height * v) * scale; 3706 3707 canvas.drawText(text, -0.05f * width + 10, y + bounds.height() / 2.0f, paint); 3708 canvas.drawText(text, x - bounds.width() / 2.0f, 3709 height + bounds.height() + 16, paint); 3710 } 3711 paint.setStyle(Paint.Style.STROKE); 3712 3713 // Draw the diagram box 3714 path.moveTo(0.0f, height); 3715 path.lineTo(0.9f * width, height); 3716 path.lineTo(0.9f * width, 0.1f * height); 3717 path.lineTo(0.0f, 0.1f * height); 3718 path.close(); 3719 canvas.drawPath(path, paint); 3720 } 3721 3722 /** 3723 * Computes and applies the Canvas transforms required to make the color 3724 * gamut of each color space visible in the final image. 3725 * 3726 * @param canvas The canvas to transform 3727 * @param width Width in pixel of the final image 3728 * @param height Height in pixel of the final image 3729 * @param primaries Array of 6 floats used to avoid temporary allocations 3730 */ 3731 private void setTransform(@NonNull Canvas canvas, int width, int height, 3732 @NonNull @Size(6) float[] primaries) { 3733 3734 RectF primariesBounds = new RectF(); 3735 for (final Pair<ColorSpace, Integer> item : mColorSpaces) { 3736 ColorSpace colorSpace = item.first; 3737 if (colorSpace.getModel() != Model.RGB) continue; 3738 3739 Rgb rgb = (Rgb) colorSpace; 3740 getPrimaries(rgb, primaries, mUcs); 3741 3742 primariesBounds.left = Math.min(primariesBounds.left, primaries[4]); 3743 primariesBounds.top = Math.min(primariesBounds.top, primaries[5]); 3744 primariesBounds.right = Math.max(primariesBounds.right, primaries[0]); 3745 primariesBounds.bottom = Math.max(primariesBounds.bottom, primaries[3]); 3746 } 3747 3748 float max = mUcs ? 0.6f : 0.9f; 3749 3750 primariesBounds.left = Math.min(0.0f, primariesBounds.left); 3751 primariesBounds.top = Math.min(0.0f, primariesBounds.top); 3752 primariesBounds.right = Math.max(max, primariesBounds.right); 3753 primariesBounds.bottom = Math.max(max, primariesBounds.bottom); 3754 3755 float scaleX = max / primariesBounds.width(); 3756 float scaleY = max / primariesBounds.height(); 3757 float scale = Math.min(scaleX, scaleY); 3758 3759 canvas.scale(mSize / (float) NATIVE_SIZE, mSize / (float) NATIVE_SIZE); 3760 canvas.scale(scale, scale); 3761 canvas.translate( 3762 (primariesBounds.width() - max) * width / 2.0f, 3763 (primariesBounds.height() - max) * height / 2.0f); 3764 3765 // The spectrum extends ~0.85 vertically and ~0.65 horizontally 3766 // We shift the canvas a little bit to get nicer margins 3767 canvas.translate(0.05f * width, -0.05f * height); 3768 } 3769 3770 /** 3771 * Computes and applies the Canvas transforms required to render the CIE 3772 * 197 UCS chromaticity diagram. 3773 * 3774 * @param canvas The canvas to transform 3775 * @param height Height in pixel of the final image 3776 */ 3777 private void setUcsTransform(@NonNull Canvas canvas, int height) { 3778 if (mUcs) { 3779 canvas.translate(0.0f, (height - height * UCS_SCALE)); 3780 canvas.scale(UCS_SCALE, UCS_SCALE); 3781 } 3782 } 3783 3784 // X coordinates of the spectral locus in CIE 1931 3785 private static final float[] SPECTRUM_LOCUS_X = { 3786 0.175596f, 0.172787f, 0.170806f, 0.170085f, 0.160343f, 3787 0.146958f, 0.139149f, 0.133536f, 0.126688f, 0.115830f, 3788 0.109616f, 0.099146f, 0.091310f, 0.078130f, 0.068717f, 3789 0.054675f, 0.040763f, 0.027497f, 0.016270f, 0.008169f, 3790 0.004876f, 0.003983f, 0.003859f, 0.004646f, 0.007988f, 3791 0.013870f, 0.022244f, 0.027273f, 0.032820f, 0.038851f, 3792 0.045327f, 0.052175f, 0.059323f, 0.066713f, 0.074299f, 3793 0.089937f, 0.114155f, 0.138695f, 0.154714f, 0.192865f, 3794 0.229607f, 0.265760f, 0.301588f, 0.337346f, 0.373083f, 3795 0.408717f, 0.444043f, 0.478755f, 0.512467f, 0.544767f, 3796 0.575132f, 0.602914f, 0.627018f, 0.648215f, 0.665746f, 3797 0.680061f, 0.691487f, 0.700589f, 0.707901f, 0.714015f, 3798 0.719017f, 0.723016f, 0.734674f, 0.717203f, 0.699732f, 3799 0.682260f, 0.664789f, 0.647318f, 0.629847f, 0.612376f, 3800 0.594905f, 0.577433f, 0.559962f, 0.542491f, 0.525020f, 3801 0.507549f, 0.490077f, 0.472606f, 0.455135f, 0.437664f, 3802 0.420193f, 0.402721f, 0.385250f, 0.367779f, 0.350308f, 3803 0.332837f, 0.315366f, 0.297894f, 0.280423f, 0.262952f, 3804 0.245481f, 0.228010f, 0.210538f, 0.193067f, 0.175596f 3805 }; 3806 // Y coordinates of the spectral locus in CIE 1931 3807 private static final float[] SPECTRUM_LOCUS_Y = { 3808 0.005295f, 0.004800f, 0.005472f, 0.005976f, 0.014496f, 3809 0.026643f, 0.035211f, 0.042704f, 0.053441f, 0.073601f, 3810 0.086866f, 0.112037f, 0.132737f, 0.170464f, 0.200773f, 3811 0.254155f, 0.317049f, 0.387997f, 0.463035f, 0.538504f, 3812 0.587196f, 0.610526f, 0.654897f, 0.675970f, 0.715407f, 3813 0.750246f, 0.779682f, 0.792153f, 0.802971f, 0.812059f, 3814 0.819430f, 0.825200f, 0.829460f, 0.832306f, 0.833833f, 3815 0.833316f, 0.826231f, 0.814796f, 0.805884f, 0.781648f, 3816 0.754347f, 0.724342f, 0.692326f, 0.658867f, 0.624470f, 3817 0.589626f, 0.554734f, 0.520222f, 0.486611f, 0.454454f, 3818 0.424252f, 0.396516f, 0.372510f, 0.351413f, 0.334028f, 3819 0.319765f, 0.308359f, 0.299317f, 0.292044f, 0.285945f, 3820 0.280951f, 0.276964f, 0.265326f, 0.257200f, 0.249074f, 3821 0.240948f, 0.232822f, 0.224696f, 0.216570f, 0.208444f, 3822 0.200318f, 0.192192f, 0.184066f, 0.175940f, 0.167814f, 3823 0.159688f, 0.151562f, 0.143436f, 0.135311f, 0.127185f, 3824 0.119059f, 0.110933f, 0.102807f, 0.094681f, 0.086555f, 3825 0.078429f, 0.070303f, 0.062177f, 0.054051f, 0.045925f, 3826 0.037799f, 0.029673f, 0.021547f, 0.013421f, 0.005295f 3827 }; 3828 3829 /** 3830 * Computes a 2D mesh representation of the CIE 1931 chromaticity 3831 * diagram. 3832 * 3833 * @param vertices Array of floats that will hold the mesh vertices 3834 * @param colors Array of floats that will hold the mesh colors 3835 */ 3836 private static void computeChromaticityMesh(@NonNull float[] vertices, 3837 @NonNull int[] colors) { 3838 3839 ColorSpace colorSpace = get(Named.SRGB); 3840 3841 float[] color = new float[3]; 3842 3843 int vertexIndex = 0; 3844 int colorIndex = 0; 3845 3846 for (int x = 0; x < SPECTRUM_LOCUS_X.length; x++) { 3847 int nextX = (x % (SPECTRUM_LOCUS_X.length - 1)) + 1; 3848 3849 float a1 = (float) Math.atan2( 3850 SPECTRUM_LOCUS_Y[x] - ONE_THIRD, 3851 SPECTRUM_LOCUS_X[x] - ONE_THIRD); 3852 float a2 = (float) Math.atan2( 3853 SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD, 3854 SPECTRUM_LOCUS_X[nextX] - ONE_THIRD); 3855 3856 float radius1 = (float) Math.pow( 3857 sqr(SPECTRUM_LOCUS_X[x] - ONE_THIRD) + 3858 sqr(SPECTRUM_LOCUS_Y[x] - ONE_THIRD), 3859 0.5); 3860 float radius2 = (float) Math.pow( 3861 sqr(SPECTRUM_LOCUS_X[nextX] - ONE_THIRD) + 3862 sqr(SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD), 3863 0.5); 3864 3865 // Compute patches; each patch is a quad with a different 3866 // color associated with each vertex 3867 for (int c = 1; c <= CHROMATICITY_RESOLUTION; c++) { 3868 float f1 = c / (float) CHROMATICITY_RESOLUTION; 3869 float f2 = (c - 1) / (float) CHROMATICITY_RESOLUTION; 3870 3871 double cr1 = radius1 * Math.cos(a1); 3872 double sr1 = radius1 * Math.sin(a1); 3873 double cr2 = radius2 * Math.cos(a2); 3874 double sr2 = radius2 * Math.sin(a2); 3875 3876 // Compute the XYZ coordinates of the 4 vertices of the patch 3877 float v1x = (float) (ONE_THIRD + cr1 * f1); 3878 float v1y = (float) (ONE_THIRD + sr1 * f1); 3879 float v1z = 1 - v1x - v1y; 3880 3881 float v2x = (float) (ONE_THIRD + cr1 * f2); 3882 float v2y = (float) (ONE_THIRD + sr1 * f2); 3883 float v2z = 1 - v2x - v2y; 3884 3885 float v3x = (float) (ONE_THIRD + cr2 * f2); 3886 float v3y = (float) (ONE_THIRD + sr2 * f2); 3887 float v3z = 1 - v3x - v3y; 3888 3889 float v4x = (float) (ONE_THIRD + cr2 * f1); 3890 float v4y = (float) (ONE_THIRD + sr2 * f1); 3891 float v4z = 1 - v4x - v4y; 3892 3893 // Compute the sRGB representation of each XYZ coordinate of the patch 3894 colors[colorIndex ] = computeColor(color, v1x, v1y, v1z, colorSpace); 3895 colors[colorIndex + 1] = computeColor(color, v2x, v2y, v2z, colorSpace); 3896 colors[colorIndex + 2] = computeColor(color, v3x, v3y, v3z, colorSpace); 3897 colors[colorIndex + 3] = colors[colorIndex]; 3898 colors[colorIndex + 4] = colors[colorIndex + 2]; 3899 colors[colorIndex + 5] = computeColor(color, v4x, v4y, v4z, colorSpace); 3900 colorIndex += 6; 3901 3902 // Flip the mesh upside down to match Canvas' coordinates system 3903 vertices[vertexIndex++] = v1x; 3904 vertices[vertexIndex++] = v1y; 3905 vertices[vertexIndex++] = v2x; 3906 vertices[vertexIndex++] = v2y; 3907 vertices[vertexIndex++] = v3x; 3908 vertices[vertexIndex++] = v3y; 3909 vertices[vertexIndex++] = v1x; 3910 vertices[vertexIndex++] = v1y; 3911 vertices[vertexIndex++] = v3x; 3912 vertices[vertexIndex++] = v3y; 3913 vertices[vertexIndex++] = v4x; 3914 vertices[vertexIndex++] = v4y; 3915 } 3916 } 3917 } 3918 3919 @ColorInt 3920 private static int computeColor(@NonNull @Size(3) float[] color, 3921 float x, float y, float z, @NonNull ColorSpace cs) { 3922 color[0] = x; 3923 color[1] = y; 3924 color[2] = z; 3925 cs.fromXyz(color); 3926 return 0xff000000 | 3927 (((int) (color[0] * 255.0f) & 0xff) << 16) | 3928 (((int) (color[1] * 255.0f) & 0xff) << 8) | 3929 (((int) (color[2] * 255.0f) & 0xff) ); 3930 } 3931 3932 private static double sqr(double v) { 3933 return v * v; 3934 } 3935 3936 private static class Point { 3937 @NonNull final ColorSpace mColorSpace; 3938 @NonNull final float[] mRgb; 3939 final int mColor; 3940 3941 Point(@NonNull ColorSpace colorSpace, 3942 @NonNull @Size(3) float[] rgb, @ColorInt int color) { 3943 mColorSpace = colorSpace; 3944 mRgb = rgb; 3945 mColor = color; 3946 } 3947 } 3948 } 3949} 3950