From 68bd5fdd1ad3cf0b74c225b31adf1f68393bfbb6 Mon Sep 17 00:00:00 2001
From: Romain Guy Denotes that the annotated element represents a packed color
+ * long. If applied to a long array, every element in the array
+ * represents a color long. For more information on how colors
+ * are packed in a long, please refer to the documentation of
+ * the {@link android.graphics.Color} class. Example: Converts the specified half-precision float value into a
- * single-precision float value with the following special cases:{@code
+ * public void setFillColor(@ColorLong long color);
+ * }
+ *
+ * @see android.graphics.Color
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD})
+public @interface ColorLong {
+}
diff --git a/core/java/android/util/Half.java b/core/java/android/util/Half.java
index 1abc10d63509c..e4f8dd1b12b83 100644
--- a/core/java/android/util/Half.java
+++ b/core/java/android/util/Half.java
@@ -543,9 +543,9 @@ public final class Half {
/**
*
Converts the specified single-precision float value into a - * half-precision float value with the following special cases:
+ * half-precision float value. The following special cases are handled: *A {@link ColorSpace} is used to identify a specific organization of colors. + * Each color space is characterized by a {@link Model color model} that defines + * how a color value is represented (for instance the {@link Model#RGB RGB} color + * model defines a color value as a triplet of numbers).
+ * + *Each component of a color must fall within a valid range, specific to each + * color space, defined by {@link #getMinValue(int)} and {@link #getMaxValue(int)} + * This range is commonly \([0..1]\). While it is recommended to use values in the + * valid range, a color space always clamps input and output values when performing + * operations such as converting to a different color space.
+ * + *This implementation provides a pre-defined set of common color spaces + * described in the {@link Named} enum. To obtain an instance of one of the + * pre-defined color spaces, simply invoke {@link #get(Named)}:
+ * + *+ * ColorSpace sRgb = ColorSpace.get(ColorSpace.Named.SRGB); + *+ * + *
The {@link #get(Named)} method always returns the same instance for a given + * name. Color spaces with an {@link Model#RGB RGB} color model can be safely + * cast to {@link Rgb}. Doing so gives you access to more APIs to query various + * properties of RGB color models: color gamut primaries, transfer functions, + * conversions to and from linear space, etc. Please refer to {@link Rgb} for + * more information.
+ * + *The documentation of {@link Named} provides a detailed description of the + * various characteristics of each available color space.
+ * + *To allow conversion between color spaces, this implementation uses the CIE + * XYZ profile connection space (PCS). Color values can be converted to and from + * this PCS using {@link #toXyz(float[])} and {@link #fromXyz(float[])}.
+ * + *For color space with a non-RGB color model, the white point of the PCS + * must be the CIE standard illuminant D50. RGB color spaces use their + * native white point (D65 for {@link Named#SRGB sRGB} for instance and must + * undergo {@link Adaptation chromatic adaptation} as necessary.
+ * + *Since the white point of the PCS is not defined for RGB color space, it is + * highly recommended to use the variants of the {@link #connect(ColorSpace, ColorSpace)} + * method to perform conversions between color spaces. A color space can be + * manually adapted to a specific white point using {@link #adapt(ColorSpace, float[])}. + * Please refer to the documentation of {@link Rgb RGB color spaces} for more + * information. Several common CIE standard illuminants are provided in this + * class as reference (see {@link #ILLUMINANT_D65} or {@link #ILLUMINANT_D50} + * for instance).
+ * + *Here is an example of how to convert from a color space to another:
+ * + *+ * // Convert from DCI-P3 to Rec.2020 + * ColorSpace.Connector connector = ColorSpace.connect( + * ColorSpace.get(ColorSpace.Named.DCI_P3), + * ColorSpace.get(ColorSpace.Named.BT2020)); + * + * float[] bt2020 = connector.transform(p3r, p3g, p3b); + *+ * + *
You can easily convert to {@link Named#SRGB sRGB} by omitting the second + * parameter:
+ * + *+ * // Convert from DCI-P3 to sRGB + * ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3)); + * + * float[] sRGB = connector.transform(p3r, p3g, p3b); + *+ * + *
Conversions also work between color spaces with different color models:
+ * + *+ * // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB) + * ColorSpace.Connector connector = ColorSpace.connect( + * ColorSpace.get(ColorSpace.Named.CIE_LAB), + * ColorSpace.get(ColorSpace.Named.BT709)); + *+ * + *
Color spaces and other related classes ({@link Connector} for instance) + * are immutable and stateless. They can be safely used from multiple concurrent + * threads.
+ * + *Public static methods provided by this class, such as {@link #get(Named)} + * and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be + * thread-safe.
+ * + * @see #get(Named) + * @see Named + * @see Model + * @see Connector + * @see Adaptation + */ +@SuppressWarnings("StaticInitializerReferencesSubClass") +public abstract class ColorSpace { + /** + * Standard CIE 1931 2° illuminant A, encoded in xyY. + * This illuminant has a color temperature of 2856K. + */ + public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f }; + /** + * Standard CIE 1931 2° illuminant B, encoded in xyY. + * This illuminant has a color temperature of 4874K. + */ + public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f }; + /** + * Standard CIE 1931 2° illuminant C, encoded in xyY. + * This illuminant has a color temperature of 6774K. + */ + public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f }; + /** + * Standard CIE 1931 2° illuminant D50, encoded in xyY. + * This illuminant has a color temperature of 5003K. This illuminant + * is used by the profile connection space in ICC profiles. + */ + public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f }; + /** + * Standard CIE 1931 2° illuminant D55, encoded in xyY. + * This illuminant has a color temperature of 5503K. + */ + public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f }; + /** + * Standard CIE 1931 2° illuminant D60, encoded in xyY. + * This illuminant has a color temperature of 6004K. + */ + public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f }; + /** + * Standard CIE 1931 2° illuminant D65, encoded in xyY. + * This illuminant has a color temperature of 6504K. This illuminant + * is commonly used in RGB color spaces such as sRGB, BT.209, etc. + */ + public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f }; + /** + * Standard CIE 1931 2° illuminant D75, encoded in xyY. + * This illuminant has a color temperature of 7504K. + */ + public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f }; + /** + * Standard CIE 1931 2° illuminant E, encoded in xyY. + * This illuminant has a color temperature of 5454K. + */ + public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f }; + + /** + * The minimum ID value a color space can have. + * + * @see #getId() + */ + public static final int MIN_ID = -1; // Do not change + /** + * The maximum ID value a color space can have. + * + * @see #getId() + */ + public static final int MAX_ID = 64; // Do not change, used to encode in longs + + private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }; + private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f }; + private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f }; + + // See static initialization block next to #get(Named) + private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; + + @NonNull private final String mName; + @NonNull private final Model mModel; + @IntRange(from = MIN_ID, to = MAX_ID) private final int mId; + + /** + * {@usesMathJax} + * + *List of common, named color spaces. A corresponding instance of + * {@link ColorSpace} can be obtained by calling {@link ColorSpace#get(Named)}:
+ * + *+ * ColorSpace cs = ColorSpace.get(ColorSpace.Named.DCI_P3); + *+ * + * @see ColorSpace#get(Named) + */ + public enum Named { + // NOTE: Do NOT change the order of the enum + /** + *
{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.640 | 0.300 | 0.150 | 0.3127 |
| y | 0.330 | 0.600 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | sRGB IEC61966-2.1 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \le 0.0031308 \\ + * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \gt 0.0031308 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \le 0.04045 \\ + * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \gt 0.04045 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.640 | 0.300 | 0.150 | 0.3127 |
| y | 0.330 | 0.600 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | sRGB IEC61966-2.1 (Linear) | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(C_{sRGB} = C_{linear}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{sRGB}\) | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space scRGB-nl standardized as IEC 61966-2-2:2003.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.640 | 0.300 | 0.150 | 0.3127 |
| y | 0.330 | 0.600 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | scRGB-nl IEC 61966-2-2:2003 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| & + * \left| C_{linear} \right| \le 0.0031308 \\ + * sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 & + * \left| C_{linear} \right| \gt 0.0031308 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} & + * \left| C_{scRGB} \right| \le 0.04045 \\ + * sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} & + * \left| C_{scRGB} \right| \gt 0.04045 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([-0.5..7.5[\) | |||
{@link ColorSpace.Rgb RGB} color space scRGB standardized as IEC 61966-2-2:2003.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.640 | 0.300 | 0.150 | 0.3127 |
| y | 0.330 | 0.600 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | scRGB IEC 61966-2-2:2003 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(C_{scRGB} = C_{linear}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{scRGB}\) | + *|||
| Range | \([-0.5..7.5[\) | |||
{@link ColorSpace.Rgb RGB} color space BT.709 standardized as Rec. ITU-R BT.709-5.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.640 | 0.300 | 0.150 | 0.3127 |
| y | 0.330 | 0.600 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | Rec. ITU-R BT.709-5 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space BT.2020 standardized as Rec. ITU-R BT.2020-1.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.708 | 0.170 | 0.131 | 0.3127 |
| y | 0.292 | 0.797 | 0.046 | 0.3290 |
| Property | Value | |||
| Name | Rec. ITU-R BT.2020-1 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{BT2020} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.0181 \\ + * 1.0993 \times C_{linear}^{\frac{1}{2.2}} - 0.0993 & C_{linear} \ge 0.0181 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT2020}}{4.5} & C_{BT2020} \lt 0.08145 \\ + * \left( \frac{C_{BT2020} + 0.0993}{1.0993} \right) ^{2.2} & C_{BT2020} \ge 0.08145 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space DCI-P3 standardized as SMPTE RP 431-2-2007.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.680 | 0.265 | 0.150 | 0.314 |
| y | 0.320 | 0.690 | 0.060 | 0.351 |
| Property | Value | |||
| Name | SMPTE RP 431-2-2007 DCI (P3) | |||
| CIE standard illuminant | N/A | |||
| Opto-electronic transfer function | + *\(C_{P3} = C_{linear}^{\frac{1}{2.6}}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{P3}^{2.6}\) | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space Display P3 based on SMPTE RP 431-2-2007 and IEC 61966-2.1:1999.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.680 | 0.265 | 0.150 | 0.3127 |
| y | 0.320 | 0.690 | 0.060 | 0.3290 |
| Property | Value | |||
| Name | Display P3 | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \le 0.0031308 \\ + * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \gt 0.0031308 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \le 0.04045 \\ + * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \gt 0.04045 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space NTSC, 1953 standard.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.67 | 0.21 | 0.14 | 0.310 |
| y | 0.33 | 0.71 | 0.08 | 0.316 |
| Property | Value | |||
| Name | NTSC (1953) | |||
| CIE standard illuminant | C | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space SMPTE C.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.630 | 0.310 | 0.155 | 0.3127 |
| y | 0.340 | 0.595 | 0.070 | 0.3290 |
| Property | Value | |||
| Name | SMPTE-C RGB | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space Adobe RGB (1998).
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.64 | 0.21 | 0.15 | 0.3127 |
| y | 0.33 | 0.71 | 0.06 | 0.3290 |
| Property | Value | |||
| Name | Adobe RGB (1998) | |||
| CIE standard illuminant | D65 | |||
| Opto-electronic transfer function | + *\(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{RGB}^{2.2}\) | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space ProPhoto RGB standardized as ROMM RGB ISO 22028-2:2013.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.7347 | 0.1596 | 0.0366 | 0.3457 |
| y | 0.2653 | 0.8404 | 0.0001 | 0.3585 |
| Property | Value | |||
| Name | ROMM RGB ISO 22028-2:2013 | |||
| CIE standard illuminant | D50 | |||
| Opto-electronic transfer function | + *\(\begin{equation} + * C_{ROMM} = \begin{cases} 16 \times C_{linear} & C_{linear} \lt 0.001953 \\ + * C_{linear}^{\frac{1}{1.8}} & C_{linear} \ge 0.001953 \end{cases} + * \end{equation}\) + * | + *|||
| Electro-optical transfer function | + *\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{ROMM}}{16} & C_{ROMM} \lt 0.031248 \\ + * C_{ROMM}^{1.8} & C_{ROMM} \ge 0.031248 \end{cases} + * \end{equation}\) + * | + *|||
| Range | \([0..1]\) | |||
{@link ColorSpace.Rgb RGB} color space ACES standardized as SMPTE ST 2065-1:2012.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.73470 | 0.00000 | 0.00010 | 0.32168 |
| y | 0.26530 | 1.00000 | -0.07700 | 0.33767 |
| Property | Value | |||
| Name | SMPTE ST 2065-1:2012 ACES | |||
| CIE standard illuminant | D60 | |||
| Opto-electronic transfer function | + *\(C_{ACES} = C_{linear}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{ACES}\) | + *|||
| Range | \([-65504.0, 65504.0]\) | |||
{@link ColorSpace.Rgb RGB} color space ACEScg standardized as Academy S-2014-004.
+ *| Chromaticity | Red | Green | Blue | White point | + *
|---|---|---|---|---|
| x | 0.713 | 0.165 | 0.128 | 0.32168 |
| y | 0.293 | 0.830 | 0.044 | 0.33767 |
| Property | Value | |||
| Name | Academy S-2014-004 ACEScg | |||
| CIE standard illuminant | D60 | |||
| Opto-electronic transfer function | + *\(C_{ACEScg} = C_{linear}\) | + *|||
| Electro-optical transfer function | + *\(C_{linear} = C_{ACEScg}\) | + *|||
| Range | \([-65504.0, 65504.0]\) | |||
{@link Model#XYZ XYZ} color space CIE XYZ. This color space assumes standard + * illuminant D50 as its white point.
+ *| Property | Value | |||
|---|---|---|---|---|
| Name | Generic XYZ | |||
| CIE standard illuminant | D50 | |||
| Range | \([-2.0, 2.0]\) | |||
{@link Model#LAB Lab} color space CIE L*a*b*. This color space uses CIE XYZ D50 + * as a profile conversion space.
+ *| Property | Value | |||
|---|---|---|---|---|
| Name | Generic L*a*b* | |||
| CIE standard illuminant | D50 | |||
| Range | \(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\) | |||
A render intent determines how a {@link ColorSpace.Connector connector} + * maps colors from one color space to another. The choice of mapping is + * important when the source color space has a larger color gamut than the + * destination color space.
+ * + * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent) + */ + public enum RenderIntent { + /** + *Compresses the source gamut into the destination gamut. + * This render intent affects all colors, inside and outside + * of destination gamut. The goal of this render intent is + * to preserve the visual relationship between colors.
+ * + *This render intent is currently not + * implemented and behaves like {@link #RELATIVE}.
+ */ + PERCEPTUAL, + /** + * Similar to the {@link #ABSOLUTE} render intent, this render + * intent matches the closest color in the destination gamut + * but makes adjustments for the destination white point. + */ + RELATIVE, + /** + *Attempts to maintain the relative saturation of colors + * from the source gamut to the destination gamut, to keep + * highly saturated colors as saturated as possible.
+ * + *This render intent is currently not + * implemented and behaves like {@link #RELATIVE}.
+ */ + SATURATION, + /** + * Colors that are in the destination gamut are left unchanged. + * Colors that fall outside of the destination gamut are mapped + * to the closest possible color within the gamut of the destination + * color space (they are clipped). + */ + ABSOLUTE + } + + /** + * {@usesMathJax} + * + *List of adaptation matrices that can be used for chromatic adaptation + * using the von Kries transform. These matrices are used to convert values + * in the CIE XYZ space to values in the LMS space (Long Medium Short).
+ * + *Given an adaptation matrix \(A\), the conversion from XYZ to + * LMS is straightforward:
+ * + * $$\left[ \begin{array}{c} L\\ M\\ S \end{array} \right] = + * A \left[ \begin{array}{c} X\\ Y\\ Z \end{array} \right]$$ + * + *The complete von Kries transform \(T\) uses a diagonal matrix + * noted \(D\) to perform the adaptation in LMS space. In addition + * to \(A\) and \(D\), the source white point \(W1\) and the destination + * white point \(W2\) must be specified:
+ * + * $$\begin{align*} + * \left[ \begin{array}{c} L_1\\ M_1\\ S_1 \end{array} \right] &= + * A \left[ \begin{array}{c} W1_X\\ W1_Y\\ W1_Z \end{array} \right] \\ + * \left[ \begin{array}{c} L_2\\ M_2\\ S_2 \end{array} \right] &= + * A \left[ \begin{array}{c} W2_X\\ W2_Y\\ W2_Z \end{array} \right] \\ + * D &= \left[ \begin{matrix} \frac{L_2}{L_1} & 0 & 0 \\ + * 0 & \frac{M_2}{M_1} & 0 \\ + * 0 & 0 & \frac{S_2}{S_1} \end{matrix} \right] \\ + * T &= A^{-1}.D.A + * \end{align*}$$ + * + *As an example, the resulting matrix \(T\) can then be used to + * perform the chromatic adaptation of sRGB XYZ transform from D65 + * to D50:
+ * + * $$sRGB_{D50} = T.sRGB_{D65}$$ + * + * @see ColorSpace.Connector + * @see ColorSpace#connect(ColorSpace, ColorSpace) + */ + public enum Adaptation { + /** + * Bradford matrix for the von Kries chromatic adaptation transform. + */ + BRADFORD(new float[] { + 0.8951f, -0.7502f, 0.0389f, + 0.2664f, 1.7135f, -0.0685f, + -0.1614f, 0.0367f, 1.0296f + }), + /** + * von Kries matrix for the von Kries chromatic adaptation transform. + */ + VON_KRIES(new float[] { + 0.40024f, -0.22630f, 0.00000f, + 0.70760f, 1.16532f, 0.00000f, + -0.08081f, 0.04570f, 0.91822f + }); + + final float[] mTransform; + + Adaptation(@NonNull @Size(9) float[] transform) { + mTransform = transform; + } + } + + /** + * A color model is required by a {@link ColorSpace} to describe the + * way colors can be represented as tuples of numbers. A common color + * model is the {@link #RGB RGB} color model which defines a color + * as represented by a tuple of 3 numbers (red, green and blue). + */ + public enum Model { + /** + * The RGB model is a color model with 3 components that + * refer to the three additive primiaries: red, green + * andd blue. + */ + RGB(3), + /** + * The XYZ model is a color model with 3 components that + * are used to model human color vision on a basic sensory + * level. + */ + XYZ(3), + /** + * The Lab model is a color model with 3 components used + * to describe a color space that is more perceptually + * uniform than XYZ. + */ + LAB(3), + /** + * The CMYK model is a color model with 4 components that + * refer to four inks used in color printing: cyan, magenta, + * yellow and black (or key). CMYK is a subtractive color + * model. + */ + CMYK(4); + + private final int mComponentCount; + + Model(@IntRange(from = 1, to = 4) int componentCount) { + mComponentCount = componentCount; + } + + /** + * Returns the number of components for this color model. + * + * @return An integer between 1 and 4 + */ + @IntRange(from = 1, to = 4) + public int getComponentCount() { + return mComponentCount; + } + } + + private ColorSpace( + @NonNull String name, + @NonNull Model model, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + + if (name == null || name.length() < 1) { + throw new IllegalArgumentException("The name of a color space cannot be null and " + + "must contain at least 1 character"); + } + + if (model == null) { + throw new IllegalArgumentException("A color space must have a model"); + } + + if (id < MIN_ID || id > MAX_ID) { + throw new IllegalArgumentException("The id must be between " + + MIN_ID + " and " + MAX_ID); + } + + mName = name; + mModel = model; + mId = id; + } + + /** + * Returns the name of this color space. The name is never null + * and contains always at least 1 character. + * + * @return A non-null String of length >= 1 + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Returns the ID of this color space. Positive IDs match the color + * spaces enumerated in {@link Named}. A negative ID indicates a + * color space created by calling one of the public constructors. + * + * @return An integer between {@link #MIN_ID} and {@link #MAX_ID} + */ + @IntRange(from = MIN_ID, to = MAX_ID) + public int getId() { + return mId; + } + + /** + * Return the color model of this color space. + * + * @return A non-null {@link Model} + * + * @see Model + * @see #getComponentCount() + */ + @NonNull + public Model getModel() { + return mModel; + } + + /** + * Returns the number of components that form a color value according + * to this color space's color model. + * + * @return An integer between 1 and 4 + * + * @see Model + * @see #getModel() + */ + @IntRange(from = 1, to = 4) + public int getComponentCount() { + return mModel.getComponentCount(); + } + + /** + * Returns whether this color space is a wide-gamut color space. + * An RGB color space is wide-gamut if its gamut entirely contains + * the {@link Named#SRGB sRGB} gamut and if the area of its gamut is + * 90% of greater than the area of the {@link Named#NTSC_1953 NTSC} + * gamut. + * + * @return True if this color space is a wide-gamut color space, + * false otherwise + */ + public abstract boolean isWideGamut(); + + /** + *Indicates whether this color space is the sRGB color space or + * equivalent to the sRGB color space.
+ *A color space is considered sRGB if it meets all the following + * conditions:
+ *This method always returns true for {@link Named#SRGB}.
+ * + * @return True if this color space is the sRGB color space (or a + * close approximation), false otherwise + */ + public boolean isSrgb() { + return false; + } + + /** + * Returns the minimum valid value for the specified component of this + * color space's color model. + * + * @param component The index of the component + * @return A floating point value less than {@link #getMaxValue(int)} + * + * @see #getMaxValue(int) + * @see Model#getComponentCount() + */ + public abstract float getMinValue(@IntRange(from = 0, to = 3) int component); + + /** + * Returns the maximum valid value for the specified component of this + * color space's color model. + * + * @param component The index of the component + * @return A floating point value greater than {@link #getMinValue(int)} + * + * @see #getMinValue(int) + * @see Model#getComponentCount() + */ + public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component); + + /** + *Converts a color value from this color space's model to + * tristimulus CIE XYZ values. If the color model of this color + * space is not {@link Model#RGB RGB}, it is assumed that the + * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} + * standard illuminant.
+ * + *This method is a convenience for color spaces with a model + * of 3 components ({@link Model#RGB RGB} or {@link Model#LAB} + * for instance). With color spaces using fewer or more components, + * use {@link #toXyz(float[])} instead
. + * + * @param r The first component of the value to convert from (typically R in RGB) + * @param g The second component of the value to convert from (typically G in RGB) + * @param b The third component of the value to convert from (typically B in RGB) + * @return A new array of 3 floats, containing tristimulus XYZ values + * + * @see #toXyz(float[]) + * @see #fromXyz(float, float, float) + */ + @NonNull + @Size(3) + public float[] toXyz(float r, float g, float b) { + return toXyz(new float[] { r, g, b }); + } + + /** + *Converts a color value from this color space's model to + * tristimulus CIE XYZ values. If the color model of this color + * space is not {@link Model#RGB RGB}, it is assumed that the + * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} + * standard illuminant.
+ * + *The specified array's length must be at least + * equal to to the number of color components as returned by + * {@link Model#getComponentCount()}.
+ * + * @param v An array of color components containing the color space's + * color value to convert to XYZ, and large enough to hold + * the resulting tristimulus XYZ values + * @return The array passed in parameter + * + * @see #toXyz(float, float, float) + * @see #fromXyz(float[]) + */ + @NonNull + @Size(min = 3) + public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v); + + /** + *Converts tristimulus values from the CIE XYZ space to this + * color space's color model.
+ * + * @param x The X component of the color value + * @param y The Y component of the color value + * @param z The Z component of the color value + * @return A new array whose size is equal to the number of color + * components as returned by {@link Model#getComponentCount()} + * + * @see #fromXyz(float[]) + * @see #toXyz(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] fromXyz(float x, float y, float z) { + float[] xyz = new float[mModel.getComponentCount()]; + xyz[0] = x; + xyz[1] = y; + xyz[2] = z; + return fromXyz(xyz); + } + + /** + *Converts tristimulus values from the CIE XYZ space to this color + * space's color model. The resulting value is passed back in the specified + * array.
+ * + *+ * getName() + "(id=" + getId() + ", model=" + getModel() + ")" + * + * + *
For instance, the string representation of the {@link Named#SRGB sRGB} + * color space is equal to the following value:
+ * + *+ * sRGB IEC61966-2.1 (id=0, model=RGB) + *+ * + * @return A string representation of the object + */ + @Override + public String toString() { + return mName + " (id=" + mId + ", model=" + mModel + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ColorSpace that = (ColorSpace) o; + + if (mId != that.mId) return false; + //noinspection SimplifiableIfStatement + if (!mName.equals(that.mName)) return false; + return mModel == that.mModel; + + } + + @Override + public int hashCode() { + int result = mName.hashCode(); + result = 31 * result + mModel.hashCode(); + result = 31 * result + mId; + return result; + } + + /** + *
Connects two color spaces to allow conversion from the source color + * space to the destination color space. If the source and destination + * color spaces do not have the same profile connection space (CIE XYZ + * with the same white point), they are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ * + *If the source and destination are the same, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.
+ * + *Colors are mapped from the source color space to the destination color + * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.
+ * + * @param source The color space to convert colors from + * @param destination The color space to convert colors to + * @return A non-null connector between the two specified color spaces + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) { + return connect(source, destination, RenderIntent.PERCEPTUAL); + } + + /** + *Connects two color spaces to allow conversion from the source color + * space to the destination color space. If the source and destination + * color spaces do not have the same profile connection space (CIE XYZ + * with the same white point), they are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ * + *If the source and destination are the same, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.
+ * + * @param source The color space to convert colors from + * @param destination The color space to convert colors to + * @param intent The render intent to map colors from the source to the destination + * @return A non-null connector between the two specified color spaces + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace) + */ + @NonNull + @SuppressWarnings("ConstantConditions") + public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination, + @NonNull RenderIntent intent) { + if (source.equals(destination)) return Connector.identity(source); + + if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) { + return new Connector.RGB((Rgb) source, (Rgb) destination, intent); + } + + return new Connector(source, destination, intent); + } + + /** + *Connects the specified color spaces to sRGB. + * If the source color space does not use CIE XYZ D65 as its profile + * connection space, the two spaces are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ * + *If the source is the sRGB color space, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.
+ * + *Colors are mapped from the source color space to the destination color + * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.
+ * + * @param source The color space to convert colors from + * @return A non-null connector between the specified color space and sRGB + * + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source) { + return connect(source, RenderIntent.PERCEPTUAL); + } + + /** + *Connects the specified color spaces to sRGB. + * If the source color space does not use CIE XYZ D65 as its profile + * connection space, the two spaces are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ * + *If the source is the sRGB color space, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.
+ * + * @param source The color space to convert colors from + * @param intent The render intent to map colors from the source to the destination + * @return A non-null connector between the specified color space and sRGB + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, ColorSpace) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) { + if (source.isSrgb()) return Connector.identity(source); + + if (source.getModel() == Model.RGB) { + return new Connector.RGB((Rgb) source, (Rgb) get(Named.SRGB), intent); + } + + return new Connector(source, get(Named.SRGB), intent); + } + + /** + *Performs the chromatic adaptation of a color space from its native + * white point to the specified white point.
+ * + *The chromatic adaptation is performed using the + * {@link Adaptation#BRADFORD} matrix.
+ * + *The color space returned by this method always has + * an ID of {@link #MIN_ID}.
+ * + * @param colorSpace The color space to chromatically adapt + * @param whitePoint The new white point + * @return A {@link ColorSpace} instance with the same name, primaries, + * transfer functions and range as the specified color space + * + * @see Adaptation + * @see #adapt(ColorSpace, float[], Adaptation) + */ + @NonNull + public static ColorSpace adapt(@NonNull ColorSpace colorSpace, + @NonNull @Size(min = 2, max = 3) float[] whitePoint) { + return adapt(colorSpace, whitePoint, Adaptation.BRADFORD); + } + + /** + *Performs the chromatic adaptation of a color space from its native + * white point to the specified white point. If the specified color space + * does not have an {@link Model#RGB RGB} color model, or if the color + * space already has the target white point, the color space is returned + * unmodified.
+ * + *The chromatic adaptation is performed using the von Kries method + * described in the documentation of {@link Adaptation}.
+ * + *The color space returned by this method always has + * an ID of {@link #MIN_ID}.
+ * + * @param colorSpace The color space to chromatically adapt + * @param whitePoint The new white point + * @param adaptation The adaptation matrix + * @return A new color space if the specified color space has an RGB + * model and a white point different from the specified white + * point; the specified color space otherwise + * + * @see Adaptation + * @see #adapt(ColorSpace, float[]) + */ + @NonNull + public static ColorSpace adapt(@NonNull ColorSpace colorSpace, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @NonNull Adaptation adaptation) { + if (colorSpace.getModel() == Model.RGB) { + ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace; + if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace; + + float[] xyz = whitePoint.length == 3 ? + Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint); + float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform, + xyYToXyz(rgb.getWhitePoint()), xyz); + float[] transform = mul3x3(adaptationTransform, rgb.mTransform); + + return new ColorSpace.Rgb(rgb, transform, whitePoint); + } + return colorSpace; + } + + /** + *Returns an instance of {@link ColorSpace} whose ID matches the specified
+ * ID. If the ID is < 0 or > {@link #MAX_ID}, calling this method is equivalent
+ * to calling get(Named.SRGB).
This method always returns the same instance for a given ID.
+ * + *This method is thread-safe.
+ * + * @param index An integer ID between {@link #MIN_ID} and {@link #MAX_ID} + * @return A non-null {@link ColorSpace} instance + */ + @NonNull + static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) { + if (index < 0 || index > Named.values().length) { + return get(Named.SRGB); + } + return sNamedColorSpaces[index]; + } + + /** + *Returns an instance of {@link ColorSpace} identified by the specified + * name. The list of names provided in the {@link Named} enum gives access + * to a variety of common RGB color spaces.
+ * + *This method always returns the same instance for a given name.
+ * + *This method is thread-safe.
+ * + * @param name The name of the color space to get an instance of + * @return A non-null {@link ColorSpace} instance + */ + @NonNull + public static ColorSpace get(@NonNull Named name) { + return sNamedColorSpaces[name.ordinal()]; + } + + static { + sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb( + "sRGB IEC61966-2.1", + SRGB_PRIMARIES, + ILLUMINANT_D65, + x -> rcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + x -> response(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + 0.0f, 1.0f, + Named.SRGB.ordinal() + ); + sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb( + "sRGB IEC61966-2.1 (Linear)", + SRGB_PRIMARIES, + ILLUMINANT_D65, + DoubleUnaryOperator.identity(), + DoubleUnaryOperator.identity(), + 0.0f, 1.0f, + Named.LINEAR_SRGB.ordinal() + ); + sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( + "scRGB-nl IEC 61966-2-2:2003", + SRGB_PRIMARIES, + ILLUMINANT_D65, + x -> absRcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + x -> absResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + -0.5f, 7.5f, + Named.EXTENDED_SRGB.ordinal() + ); + sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( + "scRGB- IEC 61966-2-2:2003", + SRGB_PRIMARIES, + ILLUMINANT_D65, + DoubleUnaryOperator.identity(), + DoubleUnaryOperator.identity(), + -0.5f, 7.5f, + Named.LINEAR_EXTENDED_SRGB.ordinal() + ); + sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb( + "Rec. ITU-R BT.709-5", + new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }, + ILLUMINANT_D65, + x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + 0.0f, 1.0f, + Named.BT709.ordinal() + ); + sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( + "Rec. ITU-R BT.2020-1", + new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, + ILLUMINANT_D65, + x -> rcpResponse(x, 1 / 0.45, 1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145), + x -> response(x, 1 / 0.45, 1 / 1.0993, 0.099 / 1.0993, 1 / 4.5, 0.08145), + 0.0f, 1.0f, + Named.BT2020.ordinal() + ); + sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb( + "SMPTE RP 431-2-2007 DCI (P3)", + new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, + new float[] { 0.314f, 0.351f }, + x -> Math.pow(x, 1 / 2.6), + x -> Math.pow(x, 2.6), + 0.0f, 1.0f, + Named.DCI_P3.ordinal() + ); + sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb( + "Display P3", + new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, + ILLUMINANT_D65, + x -> rcpResponse(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + x -> response(x, 2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045), + 0.0f, 1.0f, + Named.DISPLAY_P3.ordinal() + ); + sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb( + "NTSC (1953)", + NTSC_1953_PRIMARIES, + ILLUMINANT_C, + x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + 0.0f, 1.0f, + Named.NTSC_1953.ordinal() + ); + sNamedColorSpaces[Named.SMPTE_C.ordinal()] = new ColorSpace.Rgb( + "SMPTE-C RGB", + new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f }, + ILLUMINANT_D65, + x -> rcpResponse(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + x -> response(x, 1 / 0.45, 1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081), + 0.0f, 1.0f, + Named.SMPTE_C.ordinal() + ); + sNamedColorSpaces[Named.ADOBE_RGB.ordinal()] = new ColorSpace.Rgb( + "Adobe RGB (1998)", + new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f }, + ILLUMINANT_D65, + x -> Math.pow(x, 1 / 2.2), + x -> Math.pow(x, 2.2), + 0.0f, 1.0f, + Named.ADOBE_RGB.ordinal() + ); + sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb( + "ROMM RGB ISO 22028-2:2013", + new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f }, + ILLUMINANT_D50, + x -> rcpResponse(x, 1.8, 1.0, 0.0, 1 / 16.0, 0.031248), + x -> response(x, 1.8, 1.0, 0.0, 1 / 16.0, 0.031248), + 0.0f, 1.0f, + Named.PRO_PHOTO_RGB.ordinal() + ); + sNamedColorSpaces[Named.ACES.ordinal()] = new ColorSpace.Rgb( + "SMPTE ST 2065-1:2012 ACES", + new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f }, + ILLUMINANT_D60, + DoubleUnaryOperator.identity(), + DoubleUnaryOperator.identity(), + -65504.0f, 65504.0f, + Named.ACES.ordinal() + ); + sNamedColorSpaces[Named.ACESCG.ordinal()] = new ColorSpace.Rgb( + "Academy S-2014-004 ACEScg", + new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f }, + ILLUMINANT_D60, + DoubleUnaryOperator.identity(), + DoubleUnaryOperator.identity(), + -65504.0f, 65504.0f, + Named.ACESCG.ordinal() + ); + sNamedColorSpaces[Named.CIE_XYZ.ordinal()] = new Xyz( + "Generic XYZ", + Named.CIE_XYZ.ordinal() + ); + sNamedColorSpaces[Named.CIE_LAB.ordinal()] = new ColorSpace.Lab( + "Generic L*a*b*", + Named.CIE_LAB.ordinal() + ); + } + + // Reciprocal piecewise gamma response + private static double rcpResponse(double x, double g,double a, double b, double c, double d) { + return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c; + } + + // Piecewise gamma response + private static double response(double x, double g, double a, double b, double c, double d) { + return x >= d ? Math.pow(a * x + b, g) : c * x; + } + + // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color + // spaces that allow negative values + @SuppressWarnings("SameParameterValue") + private static double absRcpResponse(double x, double g, double a, double b, double c, double d) { + return Math.copySign(rcpResponse(x < 0.0 ? -x : x, g, a, b, c, d), x); + } + + // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that + // allow negative values + @SuppressWarnings("SameParameterValue") + private static double absResponse(double x, double g, double a, double b, double c, double d) { + return Math.copySign(response(x < 0.0 ? -x : x, g, a, b, c, d), x); + } + + /** + * Compares two arrays of float with a precision of 1e-3. + * + * @param a The first array to compare + * @param b The second array to compare + * @return True if the two arrays are equal, false otherwise + */ + private static boolean compare(@NonNull float[] a, @NonNull float[] b) { + if (a == b) return true; + for (int i = 0; i < a.length; i++) { + if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false; + } + return true; + } + + /** + * Inverts a 3x3 matrix. This method assumes the matrix is invertible. + * + * @param m A 3x3 matrix as a non-null array of 9 floats + * @return A new array of 9 floats containing the inverse of the input matrix + */ + @NonNull + @Size(9) + private static float[] inverse3x3(@NonNull @Size(9) float[] m) { + float a = m[0]; + float b = m[3]; + float c = m[6]; + float d = m[1]; + float e = m[4]; + float f = m[7]; + float g = m[2]; + float h = m[5]; + float i = m[8]; + + float A = e * i - f * h; + float B = f * g - d * i; + float C = d * h - e * g; + + float det = a * A + b * B + c * C; + + float inverted[] = new float[m.length]; + inverted[0] = A / det; + inverted[1] = B / det; + inverted[2] = C / det; + inverted[3] = (c * h - b * i) / det; + inverted[4] = (a * i - c * g) / det; + inverted[5] = (b * g - a * h) / det; + inverted[6] = (b * f - c * e) / det; + inverted[7] = (c * d - a * f) / det; + inverted[8] = (a * e - b * d) / det; + return inverted; + } + + /** + * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats. + * + * @param lhs 3x3 matrix, as a non-null array of 9 floats + * @param rhs 3x3 matrix, as a non-null array of 9 floats + * @return A new array of 9 floats containing the result of the multiplication + * of rhs by lhs + */ + @NonNull + @Size(9) + private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) { + float[] r = new float[9]; + r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2]; + r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2]; + r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2]; + r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5]; + r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5]; + r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5]; + r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8]; + r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8]; + r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8]; + return r; + } + + /** + * Multiplies a vector of 3 components by a 3x3 matrix and stores the + * result in the input vector. + * + * @param lhs 3x3 matrix, as a non-null array of 9 floats + * @param rhs Vector of 3 components, as a non-null array of 3 floats + * @return The array of 3 passed as the rhs parameter + */ + @NonNull + @Size(min = 3) + private static float[] mul3x3Float3( + @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) { + float r0 = rhs[0]; + float r1 = rhs[1]; + float r2 = rhs[2]; + rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2; + rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2; + rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2; + return rhs; + } + + /** + * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats, + * by a 3x3 matrix represented as an array of 9 floats. + * + * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats + * @param rhs 3x3 matrix, as a non-null array of 9 floats + * @return A new array of 9 floats containing the result of the multiplication + * of rhs by lhs + */ + @NonNull + @Size(9) + private static float[] mul3x3Diag( + @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) { + return new float[] { + lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2], + lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5], + lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8] + }; + } + + /** + * Converts a value from CIE xyY to CIE XYZ. Y is assumed to be 1 so the + * input xyY array only contains the x and y components. + * + * @param xyY The xyY value to convert to XYZ, cannot be null, length must be 2 + * @return A new float array of length 3 containing XYZ values + */ + @NonNull + @Size(3) + private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) { + return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] }; + } + + /** + *Computes the chromatic adaptation transform from the specified + * source white point to the specified destination white point.
+ * + *The transform is computed using the von Kris method, described + * in more details in the documentation of {@link Adaptation}. The + * {@link Adaptation} enum provides different matrices that can be + * used to perform the adaptation.
+ * + * @param matrix The adaptation matrix + * @param srcWhitePoint The white point to adapt from, *will be modified* + * @param dstWhitePoint The white point to adapt to, *will be modified* + * @return A 3x3 matrix as a non-null array of 9 floats + */ + @NonNull + @Size(9) + private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix, + @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) { + float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint); + float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint); + // LMS is a diagonal matrix stored as a float[3] + float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] }; + return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix)); + } + + /** + * Implementation of the CIE XYZ color space. Assumes the white point is D50. + */ + private static final class Xyz extends ColorSpace { + private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { + super(name, Model.XYZ, id); + } + + @Override + public boolean isWideGamut() { + return true; + } + + @Override + public float getMinValue(@IntRange(from = 0, to = 3) int component) { + return -2.0f; + } + + @Override + public float getMaxValue(@IntRange(from = 0, to = 3) int component) { + return 2.0f; + } + + @Override + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0]); + v[1] = clamp(v[1]); + v[2] = clamp(v[2]); + return v; + } + + @Override + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0]); + v[1] = clamp(v[1]); + v[2] = clamp(v[2]); + return v; + } + + private static float clamp(float x) { + return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x; + } + } + + /** + * Implementation of the CIE L*a*b* color space. Its PCS is CIE XYZ + * with a white point of D50. + */ + private static final class Lab extends ColorSpace { + private static final float A = 216.0f / 24389.0f; + private static final float B = 841.0f / 108.0f; + private static final float C = 4.0f / 29.0f; + private static final float D = 6.0f / 29.0f; + + private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { + super(name, Model.LAB, id); + } + + @Override + public boolean isWideGamut() { + return true; + } + + @Override + public float getMinValue(@IntRange(from = 0, to = 3) int component) { + return component == 0 ? 0.0f : -128.0f; + } + + @Override + public float getMaxValue(@IntRange(from = 0, to = 3) int component) { + return component == 0 ? 100.0f : 128.0f; + } + + @Override + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0], 0.0f, 100.0f); + v[1] = clamp(v[1], -128.0f, 128.0f); + v[2] = clamp(v[2], -128.0f, 128.0f); + + float fy = (v[0] + 16.0f) / 116.0f; + float fx = fy + (v[1] * 0.002f); + float fz = fy - (v[2] * 0.005f); + float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C); + float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C); + float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C); + + v[0] = X * ILLUMINANT_D50_XYZ[0]; + v[1] = Y * ILLUMINANT_D50_XYZ[1]; + v[2] = Z * ILLUMINANT_D50_XYZ[2]; + + return v; + } + + @Override + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + float X = v[0] / ILLUMINANT_D50_XYZ[0]; + float Y = v[1] / ILLUMINANT_D50_XYZ[1]; + float Z = v[2] / ILLUMINANT_D50_XYZ[2]; + + float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C; + float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C; + float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C; + + float L = 116.0f * fy - 16.0f; + float a = 500.0f * (fx - fy); + float b = 200.0f * (fy - fz); + + v[0] = clamp(L, 0.0f, 100.0f); + v[1] = clamp(a, -128.0f, 128.0f); + v[2] = clamp(b, -128.0f, 128.0f); + + return v; + } + + private static float clamp(float x, float min, float max) { + return x < min ? min : x > max ? max : x; + } + } + + /** + * {@usesMathJax} + * + *An RGB color space is an additive color space using the + * {@link Model#RGB RGB} color model (a color is therefore represented + * by a tuple of 3 numbers).
+ * + *A specific RGB color space is defined by the following properties:
+ *The most commonly used RGB color space is {@link Named#SRGB sRGB}.
+ * + *In this implementation, the chromaticity of the primaries and the white + * point of an RGB color space is defined in the CIE xyY color space. This + * color space separates the chromaticity of a color, the x and y components, + * and its luminance, the Y component. Since the primaries and the white + * point have full brightness, the Y component is assumed to be 1 and only + * the x and y components are needed to encode them.
+ *For convenience, this implementation also allows to define the + * primaries and white point in the CIE XYZ space. The tristimulus XYZ values + * are internally converted to xyY.
+ * + *A transfer function is a color component conversion function, defined as + * a single variable, monotonic mathematical function. It is applied to each + * individual component of a color. They are used to perform the mapping + * between linear tristimulus values and non-linear electronic signal value.
+ *The opto-electronic transfer function (OETF or OECF) encodes + * tristimulus values in a scene to a non-linear electronic signal value. + * An OETF is often expressed as a power function with an exponent between + * 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).
+ *The electro-optical transfer function (EOTF or EOCF) decodes + * a non-linear electronic signal value to a tristimulus value at the display. + * An EOTF is often expressed as a power function with an exponent between + * 1.8 and 2.6.
+ *Transfer functions are used as a compression scheme. For instance, + * linear sRGB values would normally require 11 to 12 bits of precision to + * store all values that can be perceived by the human eye. When encoding + * sRGB values using the appropriate OETF (see {@link Named#SRGB sRGB} for + * an exact mathematical description of that OETF), the values can be + * compressed to only 8 bits precision.
+ *When manipulating RGB values, particularly sRGB values, it is safe + * to assume that these values have been encoded with the appropriate + * OETF (unless noted otherwise). Encoded values are often said to be in + * "gamma space". They are therefore defined in a non-linear space. This + * in turns means that any linear operation applied to these values is + * going to yield mathematically incorrect results (any linear interpolation + * such as gradient generation for instance, most image processing functions + * such as blurs, etc.).
+ *To properly process encoded RGB values you must first apply the + * EOTF to decode the value into linear space. After processing, the RGB + * value must be encoded back to non-linear ("gamma") space. Here is a + * formal description of the process, where \(f\) is the processing + * function to apply:
+ * + * $$RGB_{out} = OETF(f(EOTF(RGB_{in})))$$ + * + *Some RGB color spaces, such as {@link Named#ACES} and + * {@link Named#LINEAR_EXTENDED_SRGB scRGB}, are said to be linear because + * their transfer functions are the identity function: \(f(x) = x\). + * If the source and/or destination are known to be linear, it is not + * necessary to invoke the transfer functions.
+ * + *Most RGB color spaces allow RGB values in the range \([0..1]\). There + * are however a few RGB color spaces that allow much larger ranges. For + * instance, {@link Named#EXTENDED_SRGB scRGB} is used to manipulate the + * range \([-0.5..7.5]\) while {@link Named#ACES ACES} can be used throughout + * the range \([-65504, 65504]\).
+ * + *Conversion between two color spaces is achieved by using an intermediate + * color space called the profile connection space (PCS). The PCS used by + * this implementation is CIE XYZ. The conversion operation is defined + * as such:
+ * + * $$RGB_{out} = OETF(T_{dst}^{-1} \cdot T_{src} \cdot EOTF(RGB_{in}))$$ + * + *Where \(T_{src}\) is the {@link #getTransform() RGB to XYZ transform} + * of the source color space and \(T_{dst}^{-1}\) the {@link #getInverseTransform() + * XYZ to RGB transform} of the destination color space.
+ *Many RGB color spaces commonly used with electronic devices use the + * standard illuminant {@link #ILLUMINANT_D65 D65}. Care must be take however + * when converting between two RGB color spaces if their white points do not + * match. This can be achieved by either calling + * {@link #adapt(ColorSpace, float[])} to adapt one or both color spaces to + * a single common white point. This can be achieved automatically by calling + * {@link ColorSpace#connect(ColorSpace, ColorSpace)}, which also handles + * non-RGB color spaces.
+ *To learn more about the white point adaptation process, refer to the + * documentation of {@link Adaptation}.
+ */ + public static class Rgb extends ColorSpace { + @NonNull private final float[] mWhitePoint; + @NonNull private final float[] mPrimaries; + @NonNull private final float[] mTransform; + @NonNull private final float[] mInverseTransform; + + @NonNull private final boolean mIsWideGamut; + @NonNull private final boolean mIsSrgb; + + @NonNull private final DoubleUnaryOperator mOetf; + @NonNull private final DoubleUnaryOperator mEotf; + @NonNull private final DoubleUnaryOperator mClampedOetf; + @NonNull private final DoubleUnaryOperator mClampedEotf; + + private final float mMin; + private final float mMax; + + /** + *Creates a new RGB color space using a 3x3 column-major transform matrix. + * The transform matrix must convert from the RGB space to the profile connection + * space CIE XYZ.
+ * + *The range of the color space is imposed to be \([0..1]\).
+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param toXYZ 3x3 column-major transform matrix from RGB to the profile + * connection space CIE XYZ as an array of 9 floats, cannot be null + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * + * @throws IllegalArgumentException If any of the following conditions is met: + *Creates a new RGB color space using a specified set of primaries + * and a specified white point.
+ * + *The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ * + *| Space | Primaries length | White point length |
|---|---|---|
| xyY | 6 | 2 |
| XYZ | 9 | 3 |
When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.
+ * + *The ID, areturned by {@link #getId()}, of an object created by + * this constructor is always {@link #MIN_ID}.
+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * @param min The minimum valid value in this color space's RGB range + * @param max The maximum valid value in this color space's RGB range + * + * @throws IllegalArgumentExceptionIf any of the following conditions is met:
+ *Creates a new RGB color space using a specified set of primaries + * and a specified white point.
+ * + *The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ * + *| Space | Primaries length | White point length |
|---|---|---|
| xyY | 6 | 2 |
| XYZ | 9 | 3 |
When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.
+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * @param min The minimum valid value in this color space's RGB range + * @param max The maximum valid value in this color space's RGB range + * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} + * + * @throws IllegalArgumentException If any of the following conditions is met: + *Copies the transform of this color space in specified array. The + * transform is used to convert from RGB to XYZ (with the same white + * point as this color space). To connect color spaces, you must first + * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the + * same white point.
+ *It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.
+ * + * @param transform The destination array, cannot be null, its length + * must be >= 9 + * + * @return The destination array passed as a parameter + * + * @see #getInverseTransform() + */ + @NonNull + @Size(min = 9) + public float[] getTransform(@NonNull @Size(min = 9) float[] transform) { + System.arraycopy(mTransform, 0, transform, 0, mTransform.length); + return transform; + } + + /** + *Returns the transform of this color space as a new array. The + * transform is used to convert from RGB to XYZ (with the same white + * point as this color space). To connect color spaces, you must first + * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the + * same white point.
+ *It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.
+ * + * @return A new array of 9 floats + * + * @see #getInverseTransform(float[]) + */ + @NonNull + @Size(9) + public float[] getTransform() { + return Arrays.copyOf(mTransform, mTransform.length); + } + + /** + *Copies the inverse transform of this color space in specified array. + * The inverse transform is used to convert from XYZ to RGB (with the + * same white point as this color space). To connect color spaces, you + * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them + * to the same white point.
+ *It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.
+ * + * @param inverseTransform The destination array, cannot be null, its length + * must be >= 9 + * + * @return The destination array passed as a parameter + * + * @see #getTransform() + */ + @NonNull + @Size(min = 9) + public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) { + System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length); + return inverseTransform; + } + + /** + *Returns the inverse transform of this color space as a new array. + * The inverse transform is used to convert from XYZ to RGB (with the + * same white point as this color space). To connect color spaces, you + * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them + * to the same white point.
+ *It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.
+ * + * @return A new array of 9 floats + * + * @see #getTransform(float[]) + */ + @NonNull + @Size(9) + public float[] getInverseTransform() { + return Arrays.copyOf(mInverseTransform, mInverseTransform.length); + } + + /** + *Returns the opto-electronic transfer function (OETF) of this color space. + * The inverse function is the electro-optical transfer function (EOTF) returned + * by {@link #getEotf()}. These functions are defined to satisfy the following + * equality for \(x \in [0..1]\):
+ * + * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ + * + *For RGB colors, this function can be used to convert from linear space + * to "gamma space" (gamma encoded). The terms gamma space and gamma encoded + * are frequently used because many OETFs can be closely approximated using + * a simple power function of the form \(x^{\frac{1}{\gamma}}\) (the + * approximation of the {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) + * for instance).
+ * + * @return A transfer function that converts from linear space to "gamma space" + * + * @see #getEotf() + */ + @NonNull + public DoubleUnaryOperator getOetf() { + return mOetf; + } + + /** + *Returns the electro-optical transfer function (EOTF) of this color space. + * The inverse function is the opto-electronic transfer function (OETF) + * returned by {@link #getOetf()}. These functions are defined to satisfy the + * following equality for \(x \in [0..1]\):
+ * + * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ + * + *For RGB colors, this function can be used to convert from "gamma space" + * (gamma encoded) to linear space. The terms gamma space and gamma encoded + * are frequently used because many EOTFs can be closely approximated using + * a simple power function of the form \(x^\gamma\) (the approximation of the + * {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) for instance).
+ * + * @return A transfer function that converts from "gamma space" to linear space + * + * @see #getOetf() + */ + @NonNull + public DoubleUnaryOperator getEotf() { + return mEotf; + } + + @Override + public boolean isSrgb() { + return mIsSrgb; + } + + @Override + public boolean isWideGamut() { + return mIsWideGamut; + } + + @Override + public float getMinValue(int component) { + return mMin; + } + + @Override + public float getMaxValue(int component) { + return mMax; + } + + /** + *Decodes an RGB value to linear space. This is achieved by + * applying this color space's electro-optical transfer function + * to the supplied values.
+ * + *Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.
+ * + * @param r The red component to decode to linear space + * @param g The green component to decode to linear space + * @param b The blue component to decode to linear space + * @return A new array of 3 floats containing linear RGB values + * + * @see #toLinear(float[]) + * @see #fromLinear(float, float, float) + */ + @NonNull + @Size(3) + public float[] toLinear(float r, float g, float b) { + return toLinear(new float[] { r, g, b }); + } + + /** + *Decodes an RGB value to linear space. This is achieved by + * applying this color space's electro-optical transfer function + * to the first 3 values of the supplied array. The result is + * stored back in the input array.
+ * + *Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.
+ * + * @param v A non-null array of non-linear RGB values, its length + * must be at least 3 + * @return The specified array + * + * @see #toLinear(float, float, float) + * @see #fromLinear(float[]) + */ + @NonNull + @Size(min = 3) + public float[] toLinear(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedEotf.applyAsDouble(v[0]); + v[1] = (float) mClampedEotf.applyAsDouble(v[1]); + v[2] = (float) mClampedEotf.applyAsDouble(v[2]); + return v; + } + + /** + *Encodes an RGB value from linear space to this color space's + * "gamma space". This is achieved by applying this color space's + * opto-electronic transfer function to the supplied values.
+ * + *Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.
+ * + * @param r The red component to encode from linear space + * @param g The green component to encode from linear space + * @param b The blue component to encode from linear space + * @return A new array of 3 floats containing non-linear RGB values + * + * @see #fromLinear(float[]) + * @see #toLinear(float, float, float) + */ + @NonNull + @Size(3) + public float[] fromLinear(float r, float g, float b) { + return fromLinear(new float[] { r, g, b }); + } + + /** + *Encodes an RGB value from linear space to this color space's + * "gamma space". This is achieved by applying this color space's + * opto-electronic transfer function to the first 3 values of the + * supplied array. The result is stored back in the input array.
+ * + *Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.
+ * + * @param v A non-null array of linear RGB values, its length + * must be at least 3 + * @return A new array of 3 floats containing non-linear RGB values + * + * @see #fromLinear(float[]) + * @see #toLinear(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] fromLinear(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedOetf.applyAsDouble(v[0]); + v[1] = (float) mClampedOetf.applyAsDouble(v[1]); + v[2] = (float) mClampedOetf.applyAsDouble(v[2]); + return v; + } + + @Override + @NonNull + @Size(min = 3) + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedEotf.applyAsDouble(v[0]); + v[1] = (float) mClampedEotf.applyAsDouble(v[1]); + v[2] = (float) mClampedEotf.applyAsDouble(v[2]); + return mul3x3Float3(mTransform, v); + } + + @Override + @NonNull + @Size(min = 3) + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + mul3x3Float3(mInverseTransform, v); + v[0] = (float) mClampedOetf.applyAsDouble(v[0]); + v[1] = (float) mClampedOetf.applyAsDouble(v[1]); + v[2] = (float) mClampedOetf.applyAsDouble(v[2]); + return v; + } + + private double clamp(double x) { + return x < mMin ? mMin : x > mMax ? mMax : x; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + Rgb rgb = (Rgb) o; + + if (Float.compare(rgb.mMin, mMin) != 0) return false; + if (Float.compare(rgb.mMax, mMax) != 0) return false; + if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false; + if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false; + //noinspection SimplifiableIfStatement + if (!mOetf.equals(rgb.mOetf)) return false; + return mEotf.equals(rgb.mEotf); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(mWhitePoint); + result = 31 * result + Arrays.hashCode(mPrimaries); + result = 31 * result + mOetf.hashCode(); + result = 31 * result + mEotf.hashCode(); + result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0); + result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0); + return result; + } + + /** + * Computes whether a color space is the sRGB color space or at least + * a close approximation. + * + * @param primaries The set of RGB primaries in xyY as an array of 6 floats + * @param whitePoint The white point in xyY as an array of 2 floats + * @param OETF The opto-electronic transfer function + * @param EOTF The electro-optical transfer function + * @param min The minimum value of the color space's range + * @param max The minimum value of the color space's range + * @param id The ID of the color space + * @return True if the color space can be considered as the sRGB color space + * + * @see #isSrgb() + */ + @SuppressWarnings("RedundantIfStatement") + private static boolean isSrgb( + @NonNull @Size(6) float[] primaries, + @NonNull @Size(2) float[] whitePoint, + @NonNull DoubleUnaryOperator OETF, + @NonNull DoubleUnaryOperator EOTF, + float min, + float max, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + if (id == 0) return true; + if (!compare(primaries, SRGB_PRIMARIES)) { + return false; + } + if (!compare(whitePoint, ILLUMINANT_D65)) { + return false; + } + if (OETF.applyAsDouble(0.5) < 0.5001) return false; + if (EOTF.applyAsDouble(0.5) > 0.5001) return false; + if (min != 0.0f) return false; + if (max != 1.0f) return false; + return true; + } + + /** + * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form + * a wide color gamut. A color gamut is considered wide if its area is > 90% + * of the area of NTSC 1953 and if it contains the sRGB color gamut entirely. + * If the conditions above are not met, the color space is considered as having + * a wide color gamut if its range is larger than [0..1]. + * + * @param primaries RGB primaries in CIE xyY or XYZ as an array of 6 or 9 floats + * @param min The minimum value of the color space's range + * @param max The minimum value of the color space's range + * @return True if the color space has a wide gamut, false otherwise + * + * @see #isWideGamut() + * @see #area(float[]) + */ + private static boolean isWideGamut(@NonNull @Size(min = 6, max = 9) float[] primaries, + float min, float max) { + return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f && + contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f); + } + + /** + * Computes the area of the triangle represented by a set of RGB primaries + * in the CIE xyY space. + * + * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats + * @return The area of the triangle + * + * @see #isWideGamut(float[], float, float) + */ + private static float area(@NonNull @Size(6) float[] primaries) { + float Rx = primaries[0]; + float Ry = primaries[1]; + float Gx = primaries[2]; + float Gy = primaries[3]; + float Bx = primaries[4]; + float By = primaries[5]; + float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By; + float r = 0.5f * det; + return r < 0.0f ? -r : r; + } + + /** + * Computes the cross product of two 2D vectors. + * + * @param ax The x coordinate of the first vector + * @param ay The y coordinate of the first vector + * @param bx The x coordinate of the second vector + * @param by The y coordinate of the second vector + * @return The result of a x b + */ + private static float cross(float ax, float ay, float bx, float by) { + return ax * by - ay * bx; + } + + /** + * Decides whether a 2D triangle, identified by the 6 coordinates of its + * 3 vertices, is contained within another 2D triangle, also identified + * by the 6 coordinates of its 3 vertices. + * + * In the illustration below, we want to test whether the RGB triangle + * is contained within the triangle XYZ formed by the 3 vertices at + * the "+" locations. + * + * Y . + * . + . + * . .. + * . . + * . . + * . G + * * + * * * + * ** * + * * ** + * * * + * ** * + * * * + * * * + * ** * + * * * + * * ** + * ** * R ... + * * * ..... + * * ***** .. + * ** ************ . + + * B * ************ . X + * ......***** . + * ...... . . + * .. + * + . + * Z . + * + * RGB is contained within XYZ if all the following conditions are true + * (with "x" the cross product operator): + * + * --> --> + * GR x RX >= 0 + * --> --> + * RX x BR >= 0 + * --> --> + * RG x GY >= 0 + * --> --> + * GY x RG >= 0 + * --> --> + * RB x BZ >= 0 + * --> --> + * BZ x GB >= 0 + * + * @param p1 The enclosing triangle + * @param p2 The enclosed triangle + * @return True if the triangle p1 contains the triangle p2 + * + * @see #isWideGamut(float[], float, float) + */ + @SuppressWarnings("RedundantIfStatement") + private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) { + // Translate the vertices p1 in the coordinates system + // with the vertices p2 as the origin + float[] p0 = new float[] { + p1[0] - p2[0], p1[1] - p2[1], + p1[2] - p2[2], p1[3] - p2[3], + p1[4] - p2[4], p1[5] - p2[5], + }; + // Check the first vertex of p1 + if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 || + cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) { + return false; + } + // Check the second vertex of p1 + if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 || + cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) { + return false; + } + // Check the third vertex of p1 + if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 || + cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) { + return false; + } + return true; + } + + /** + * Computes the primaries of a color space identified only by + * its RGB->XYZ transform matrix. This method assumes that the + * range of the color space is [0..1]. + * + * @param toXYZ The color space's 3x3 transform matrix to XYZ + * @param EOTF The color space's electro-optical transfer function + * @return A new array of 6 floats containing the color space's + * primaries in CIE xyY + */ + @NonNull + @Size(6) + private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ, + DoubleUnaryOperator EOTF) { + float one = (float) EOTF.applyAsDouble(1.0); + float[] r = mul3x3Float3(toXYZ, new float[] { one, 0.0f, 0.0f }); + float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, one, 0.0f }); + float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, one }); + + float rSum = r[0] + r[1] + r[2]; + float gSum = g[0] + g[1] + g[2]; + float bSum = b[0] + b[1] + b[2]; + + return new float[] { + r[0] / rSum, r[1] / rSum, + g[0] / gSum, g[1] / gSum, + b[0] / bSum, b[1] / bSum, + }; + } + + /** + * Computes the white point of a color space identified only by + * its RGB->XYZ transform matrix. This method assumes that the + * range of the color space is [0..1]. + * + * @param toXYZ The color space's 3x3 transform matrix to XYZ + * @param EOTF The color space's electro-optical transfer function + * @return A new array of 2 floats containing the color space's + * white point in CIE xyY + */ + @NonNull + @Size(2) + private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ, + @NonNull DoubleUnaryOperator EOTF) { + float one = (float) EOTF.applyAsDouble(1.0); + float[] w = mul3x3Float3(toXYZ, new float[] { one, one, one }); + float sum = w[0] + w[1] + w[2]; + return new float[] { w[0] / sum, w[1] / sum }; + } + + /** + * Converts the specified RGB primaries point to xyY if needed. The primaries + * can be specified as an array of 6 floats (in CIE xyY) or 9 floats + * (in CIE XYZ). If no conversion is needed, the input array is copied. + * + * @param primaries The primaries in xyY or XYZ + * @return A new array of 6 floats containing the primaries in xyY + */ + @NonNull + @Size(2) + private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) { + float[] xyPrimaries = new float[6]; + + // XYZ to xyY + if (primaries.length == 9) { + float sum; + + sum = primaries[0] + primaries[1] + primaries[2]; + xyPrimaries[0] = primaries[0] / sum; + xyPrimaries[1] = primaries[1] / sum; + + sum = primaries[3] + primaries[4] + primaries[5]; + xyPrimaries[2] = primaries[3] / sum; + xyPrimaries[3] = primaries[4] / sum; + + sum = primaries[6] + primaries[7] + primaries[8]; + xyPrimaries[4] = primaries[6] / sum; + xyPrimaries[5] = primaries[7] / sum; + } else { + System.arraycopy(primaries, 0, xyPrimaries, 0, 6); + } + + return xyPrimaries; + } + + /** + * Converts the specified white point to xyY if needed. The white point + * can be specified as an array of 2 floats (in CIE xyY) or 3 floats + * (in CIE XYZ). If no conversion is needed, the input array is copied. + * + * @param whitePoint The white point in xyY or XYZ + * @return A new array of 2 floats containing the white point in xyY + */ + @NonNull + @Size(2) + private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) { + float[] xyWhitePoint = new float[2]; + + // XYZ to xyY + if (whitePoint.length == 3) { + float sum = whitePoint[0] + whitePoint[1] + whitePoint[2]; + xyWhitePoint[0] = whitePoint[0] / sum; + xyWhitePoint[1] = whitePoint[1] / sum; + } else { + System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2); + } + + return xyWhitePoint; + } + + /** + * Computes the matrix that converts from RGB to XYZ based on RGB + * primaries and a white point, both specified in the CIE xyY space. + * The Y component of the primaries and white point is implied to be 1. + * + * @param primaries The RGB primaries in xyY, as an array of 6 floats + * @param whitePoint The white point in xyY, as an array of 2 floats + * @return A 3x3 matrix as a new array of 9 floats + */ + @NonNull + @Size(9) + private static float[] computeXYZMatrix( + @NonNull @Size(6) float[] primaries, + @NonNull @Size(2) float[] whitePoint) { + float Rx = primaries[0]; + float Ry = primaries[1]; + float Gx = primaries[2]; + float Gy = primaries[3]; + float Bx = primaries[4]; + float By = primaries[5]; + float Wx = whitePoint[0]; + float Wy = whitePoint[1]; + + float oneRxRy = (1 - Rx) / Ry; + float oneGxGy = (1 - Gx) / Gy; + float oneBxBy = (1 - Bx) / By; + float oneWxWy = (1 - Wx) / Wy; + + float RxRy = Rx / Ry; + float GxGy = Gx / Gy; + float BxBy = Bx / By; + float WxWy = Wx / Wy; + + float BY = + ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) / + ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy)); + float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy); + float RY = 1 - GY - BY; + + float RYRy = RY / Ry; + float GYGy = GY / Gy; + float BYBy = BY / By; + + return new float[] { + RYRy * Rx, RY, RYRy * (1 - Rx - Ry), + GYGy * Gx, GY, GYGy * (1 - Gx - Gy), + BYBy * Bx, BY, BYBy * (1 - Bx - By) + }; + } + } + + /** + * {@usesMathJax} + * + *A connector transforms colors from a source color space to a destination + * color space.
+ * + *A source color space is connected to a destination color space using the + * color transform \(C\) computed from their respective transforms noted + * \(T_{src}\) and \(T_{dst}\) in the following equation:
+ * + * $$C = T^{-1}_{dst} . T_{src}$$ + * + *The transform \(C\) shown above is only valid when the source and + * destination color spaces have the same profile connection space (PCS). + * We know that instances of {@link ColorSpace} always use CIE XYZ as their + * PCS but their white points might differ. When they do, we must perform + * a chromatic adaptation of the color spaces' transforms. To do so, we + * use the von Kries method described in the documentation of {@link Adaptation}, + * using the CIE standard illuminant {@link ColorSpace#ILLUMINANT_D50 D50} + * as the target white point.
+ * + *Example of conversion from {@link Named#SRGB sRGB} to + * {@link Named#DCI_P3 DCI-P3}:
+ * + *
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.SRGB),
+ * ColorSpace.get(ColorSpace.Named.DCI_P3));
+ * float[] p3 = connector.transform(1.0f, 0.0f, 0.0f);
+ * // p3 contains { 0.9473, 0.2740, 0.2076 }
+ *
+ *
+ * @see Adaptation
+ * @see ColorSpace#adapt(ColorSpace, float[], Adaptation)
+ * @see ColorSpace#adapt(ColorSpace, float[])
+ * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ * @see ColorSpace#connect(ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace)
+ */
+ public static class Connector {
+ @NonNull private final ColorSpace mSource;
+ @NonNull private final ColorSpace mDestination;
+ @NonNull private final ColorSpace mTransformSource;
+ @NonNull private final ColorSpace mTransformDestination;
+ @NonNull private final RenderIntent mIntent;
+ @NonNull @Size(3) private final float[] mTransform;
+
+ /**
+ * Creates a new connector between a source and a destination color space.
+ *
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @param intent The render intent to use when compressing gamuts
+ */
+ Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ this(source, destination,
+ source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source,
+ destination.getModel() == Model.RGB ?
+ adapt(destination, ILLUMINANT_D50_XYZ) : destination,
+ intent, computeTransform(source, destination, intent));
+ }
+
+ /**
+ * To connect between color spaces, we might need to use adapted transforms.
+ * This should be transparent to the user so this constructor takes the
+ * original source and destinations (returned by the getters), as well as
+ * possibly adapted color spaces used by transform().
+ */
+ private Connector(
+ @NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination,
+ @NonNull RenderIntent intent, @NonNull @Size(3) float[] transform) {
+ mSource = source;
+ mDestination = destination;
+ mTransformSource = transformSource;
+ mTransformDestination = transformDestination;
+ mIntent = intent;
+ mTransform = transform;
+ }
+
+ /**
+ * Computes an extra transform to apply in XYZ space depending on the
+ * selected rendering intent.
+ */
+ private static float[] computeTransform(@NonNull ColorSpace source,
+ @NonNull ColorSpace destination, @NonNull RenderIntent intent) {
+ if (intent != RenderIntent.ABSOLUTE) return null;
+
+ boolean srcRGB = source.getModel() == Model.RGB;
+ boolean dstRGB = destination.getModel() == Model.RGB;
+
+ if (srcRGB && dstRGB) return null;
+
+ if (srcRGB || dstRGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination);
+ float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ return new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ };
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the source color space this connector will convert from.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getDestination()
+ */
+ @NonNull
+ public ColorSpace getSource() {
+ return mSource;
+ }
+
+ /**
+ * Returns the destination color space this connector will convert to.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getSource()
+ */
+ @NonNull
+ public ColorSpace getDestination() {
+ return mDestination;
+ }
+
+ /**
+ * Returns the render intent this connector will use when mapping the
+ * source color space to the destination color space.
+ *
+ * @return A non-null {@link RenderIntent}
+ *
+ * @see RenderIntent
+ */
+ public RenderIntent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Transforms the specified color from the source color space + * to a color in the destination color space. This convenience + * method assumes a source color model with 3 components + * (typically RGB). To transform from color models with more than + * 3 components, such as {@link Model#CMYK CMYK}, use + * {@link #transform(float[])} instead.
+ * + * @param r The red component of the color to transform + * @param g The green component of the color to transform + * @param b The blue component of the color to transform + * @return A new array of 3 floats containing the specified color + * transformed from the source space to the destination space + * + * @see #transform(float[]) + */ + @NonNull + @Size(3) + public float[] transform(float r, float g, float b) { + return transform(new float[] { r, g, b }); + } + + /** + *Transforms the specified color from the source color space + * to a color in the destination color space.
+ * + * @param v A non-null array of 3 floats containing the value to transform + * and that will hold the result of the transform + * @return The v array passed as a parameter, containing the specified color + * transformed from the source space to the destination space + * + * @see #transform(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] transform(@NonNull @Size(min = 3) float[] v) { + float[] xyz = mTransformSource.toXyz(v); + if (mTransform != null) { + xyz[0] *= mTransform[0]; + xyz[1] *= mTransform[1]; + xyz[2] *= mTransform[2]; + } + return mTransformDestination.fromXyz(xyz); + } + + /** + * Optimized connector for RGB->RGB conversions. + */ + private static class RGB extends Connector { + @NonNull private final ColorSpace.Rgb mSource; + @NonNull private final ColorSpace.Rgb mDestination; + @NonNull private final float[] mTransform; + + RGB(@NonNull ColorSpace.Rgb source, + @NonNull ColorSpace.Rgb destination, + @NonNull RenderIntent intent) { + super(source, destination, source, destination, intent, null); + mSource = source; + mDestination = destination; + mTransform = computeTransform(source, destination, intent); + } + + @Override + public float[] transform(@NonNull @Size(min = 3) float[] rgb) { + rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]); + rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]); + rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]); + mul3x3Float3(mTransform, rgb); + rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]); + rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]); + rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]); + return rgb; + } + + /** + *Computes the color transform that connects two RGB color spaces.
+ * + *We can only connect color spaces if they use the same profile + * connection space. We assume the connection space is always + * CIE XYZ but we maye need to perform a chromatic adaptation to + * match the white points. If an adaptation is needed, we use the + * CIE standard illuminant D50. The unmatched color space is adapted + * using the von Kries transform and the {@link Adaptation#BRADFORD} + * matrix.
+ * + * @param source The source color space, cannot be null + * @param destination The destination color space, cannot be null + * @param intent The render intent to use when compressing gamuts + * @return An array of 9 floats containing the 3x3 matrix transform + */ + @NonNull + @Size(9) + private static float[] computeTransform( + @NonNull ColorSpace.Rgb source, + @NonNull ColorSpace.Rgb destination, + @NonNull RenderIntent intent) { + if (compare(source.mWhitePoint, destination.mWhitePoint)) { + // RGB->RGB using the PCS of both color spaces since they have the same + return mul3x3(destination.mInverseTransform, source.mTransform); + } else { + // RGB->RGB using CIE XYZ D50 as the PCS + float[] transform = source.mTransform; + float[] inverseTransform = destination.mInverseTransform; + + float[] srcXYZ = xyYToXyz(source.mWhitePoint); + float[] dstXYZ = xyYToXyz(destination.mWhitePoint); + + if (!compare(source.mWhitePoint, ILLUMINANT_D50)) { + float[] srcAdaptation = chromaticAdaptation( + Adaptation.BRADFORD.mTransform, srcXYZ, + Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); + transform = mul3x3(srcAdaptation, source.mTransform); + } + + if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) { + float[] dstAdaptation = chromaticAdaptation( + Adaptation.BRADFORD.mTransform, dstXYZ, + Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); + inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform)); + } + + if (intent == RenderIntent.ABSOLUTE) { + transform = mul3x3Diag( + new float[] { + srcXYZ[0] / dstXYZ[0], + srcXYZ[1] / dstXYZ[1], + srcXYZ[2] / dstXYZ[2], + }, transform); + } + + return mul3x3(inverseTransform, transform); + } + } + } + + /** + * Returns the identity connector for a given color space. + * + * @param source The source and destination color space + * @return A non-null connector that does not perform any transform + * + * @see ColorSpace#connect(ColorSpace, ColorSpace) + */ + static Connector identity(ColorSpace source) { + return new Connector(source, source, RenderIntent.RELATIVE) { + @Override + public float[] transform(@NonNull @Size(min = 3) float[] v) { + return v; + } + }; + } + } +}