android.graphics.ColorSpace Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics;
import android.annotation.AnyThread;
import android.annotation.ColorInt;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Size;
import android.annotation.SuppressAutoDoc;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
/**
* {@usesMathJax}
*
* 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.
*
* Using color spaces
*
* 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.
*
* Color space conversions
* 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 multi-threading
*
* 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
*/
@AnyThread
@SuppressWarnings("StaticInitializerReferencesSubClass")
@SuppressAutoDoc
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 = 63; // 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);
*
*
* The properties of each color space are described below (see {@link #SRGB sRGB}
* for instance). When applicable, the color gamut of each color space is compared
* to the color gamut of sRGB using a CIE 1931 xy chromaticity diagram. This diagram
* shows the location of the color space's primaries and white point.
*
* @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 (OETF)
* \(\begin{equation}
* C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0031308 \\
* 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0031308 \end{cases}
* \end{equation}\)
*
*
*
* Electro-optical transfer function (EOTF)
* \(\begin{equation}
* C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \lt 0.04045 \\
* \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases}
* \end{equation}\)
*
*
* Range \([0..1]\)
*
*
*
* sRGB
*
*/
SRGB,
/**
* {@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 (OETF)
* \(C_{sRGB} = C_{linear}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{sRGB}\)
*
* Range \([0..1]\)
*
*
*
* sRGB
*
*/
LINEAR_SRGB,
/**
* {@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 (OETF)
* \(\begin{equation}
* C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| &
* \left| C_{linear} \right| \lt 0.0031308 \\
* sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 &
* \left| C_{linear} \right| \ge 0.0031308 \end{cases}
* \end{equation}\)
*
*
*
* Electro-optical transfer function (EOTF)
* \(\begin{equation}
* C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} &
* \left| C_{scRGB} \right| \lt 0.04045 \\
* sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} &
* \left| C_{scRGB} \right| \ge 0.04045 \end{cases}
* \end{equation}\)
*
*
* Range \([-0.799..2.399[\)
*
*
*
* Extended sRGB (orange) vs sRGB (white)
*
*/
EXTENDED_SRGB,
/**
* {@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 (OETF)
* \(C_{scRGB} = C_{linear}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{scRGB}\)
*
* Range \([-0.5..7.499[\)
*
*
*
* Extended sRGB (orange) vs sRGB (white)
*
*/
LINEAR_EXTENDED_SRGB,
/**
* {@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 (OETF)
* \(\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 (EOTF)
* \(\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]\)
*
*
*
* BT.709
*
*/
BT709,
/**
* {@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 (OETF)
* \(\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 (EOTF)
* \(\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]\)
*
*
*
* BT.2020 (orange) vs sRGB (white)
*
*/
BT2020,
/**
* {@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 (OETF)
* \(C_{P3} = C_{linear}^{\frac{1}{2.6}}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{P3}^{2.6}\)
*
* Range \([0..1]\)
*
*
*
* DCI-P3 (orange) vs sRGB (white)
*
*/
DCI_P3,
/**
* {@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 (OETF)
* \(\begin{equation}
* C_{DisplayP3} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0030186 \\
* 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0030186 \end{cases}
* \end{equation}\)
*
*
*
* Electro-optical transfer function (EOTF)
* \(\begin{equation}
* C_{linear} = \begin{cases}\frac{C_{DisplayP3}}{12.92} & C_{sRGB} \lt 0.039 \\
* \left( \frac{C_{DisplayP3} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.039 \end{cases}
* \end{equation}\)
*
*
* Range \([0..1]\)
*
*
*
* Display P3 (orange) vs sRGB (white)
*
*/
DISPLAY_P3,
/**
* {@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 (OETF)
* \(\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 (EOTF)
* \(\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]\)
*
*
*
* NTSC 1953 (orange) vs sRGB (white)
*
*/
NTSC_1953,
/**
* {@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 (OETF)
* \(\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 (EOTF)
* \(\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]\)
*
*
*
* SMPTE-C (orange) vs sRGB (white)
*
*/
SMPTE_C,
/**
* {@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 (OETF)
* \(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{RGB}^{2.2}\)
*
* Range \([0..1]\)
*
*
*
* Adobe RGB (orange) vs sRGB (white)
*
*/
ADOBE_RGB,
/**
* {@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 (OETF)
* \(\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 (EOTF)
* \(\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]\)
*
*
*
* ProPhoto RGB (orange) vs sRGB (white)
*
*/
PRO_PHOTO_RGB,
/**
* {@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 (OETF)
* \(C_{ACES} = C_{linear}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{ACES}\)
*
* Range \([-65504.0, 65504.0]\)
*
*
*
* ACES (orange) vs sRGB (white)
*
*/
ACES,
/**
* {@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 (OETF)
* \(C_{ACEScg} = C_{linear}\)
*
*
* Electro-optical transfer function (EOTF)
* \(C_{linear} = C_{ACEScg}\)
*
* Range \([-65504.0, 65504.0]\)
*
*
*
* ACEScg (orange) vs sRGB (white)
*
*/
ACESCG,
/**
* {@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]\)
*
*/
CIE_XYZ,
/**
* {@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]\)
*
*/
CIE_LAB
// Update the initialization block next to #get(Named) when adding new values
}
/**
* 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 chromatic adaptation transform, as defined in the
* CIECAM97s color appearance model.
*/
BRADFORD(new float[] {
0.8951f, -0.7502f, 0.0389f,
0.2664f, 1.7135f, -0.0685f,
-0.1614f, 0.0367f, 1.0296f
}),
/**
* 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
}),
/**
* CIECAT02 chromatic adaption transform, as defined in the
* CIECAM02 color appearance model.
*/
CIECAT02(new float[] {
0.7328f, -0.7036f, 0.0030f,
0.4296f, 1.6975f, 0.0136f,
-0.1624f, 0.0061f, 0.9834f
});
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.
*
* Color space names are recommended to be unique but are not
* guaranteed to be. There is no defined format but the name usually
* falls in one of the following categories:
*
* - Generic names used to identify color spaces in non-RGB
* color models. For instance: {@link Named#CIE_LAB Generic L*a*b*}.
* - Names tied to a particular specification. For instance:
* {@link Named#SRGB sRGB IEC61966-2.1} or
* {@link Named#ACES SMPTE ST 2065-1:2012 ACES}.
* - Ad-hoc names, often generated procedurally or by the user
* during a calibration workflow. These names often contain the
* make and model of the display.
*
*
* Because the format of color space names is not defined, it is
* not recommended to programmatically identify a color space by its
* name alone. Names can be used as a first approximation.
*
* It is however perfectly acceptable to display color space names to
* users in a UI, or in debuggers and logs. When displaying a color space
* name to the user, it is recommended to add extra information to avoid
* ambiguities: color model, a representation of the color space's gamut,
* white point, etc.
*
* @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:
*
* - Its color model is {@link Model#RGB}.
* -
* Its primaries are within 1e-3 of the true
* {@link Named#SRGB sRGB} primaries.
*
* -
* Its white point is withing 1e-3 of the CIE standard
* illuminant {@link #ILLUMINANT_D65 D65}.
*
* - Its opto-electronic transfer function is not linear.
* - Its electro-optical transfer function is not linear.
* - Its range is \([0..1]\).
*
* 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.
*
* The specified array's length must be at least equal to
* to the number of color components as returned by
* {@link Model#getComponentCount()}, and its first 3 values must
* be the XYZ components to convert from.
*
* @param v An array of color components containing the XYZ values
* to convert from, and large enough to hold the number
* of components of this color space's model
* @return The array passed in parameter
*
* @see #fromXyz(float, float, float)
* @see #toXyz(float[])
*/
@NonNull
@Size(min = 3)
public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v);
/**
* Returns a string representation of the object. This method returns
* a string equal to the value of:
*
*
* 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
@NonNull
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.
*
* 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
* @throws IllegalArgumentException If the ID does not match the ID of one of the
* {@link Named named color spaces}
*/
@NonNull
static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) {
if (index < 0 || index > Named.values().length) {
throw new IllegalArgumentException("Invalid ID, must be in the range [0.." +
Named.values().length + "]");
}
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()];
}
/**
* Returns a {@link Named} instance of {@link ColorSpace} that matches
* the specified RGB to CIE XYZ transform and transfer functions. If no
* instance can be found, this method returns null.
*
* The color transform matrix is assumed to target the CIE XYZ space
* a {@link #ILLUMINANT_D50 D50} standard illuminant.
*
* @param toXYZD50 3x3 column-major transform matrix from RGB to the profile
* connection space CIE XYZ as an array of 9 floats, cannot be null
* @param function Parameters for the transfer functions
* @return A non-null {@link ColorSpace} if a match is found, null otherwise
*/
@Nullable
public static ColorSpace match(
@NonNull @Size(9) float[] toXYZD50,
@NonNull Rgb.TransferParameters function) {
for (ColorSpace colorSpace : sNamedColorSpaces) {
if (colorSpace.getModel() == Model.RGB) {
ColorSpace.Rgb rgb = (ColorSpace.Rgb) adapt(colorSpace, ILLUMINANT_D50_XYZ);
if (compare(toXYZD50, rgb.mTransform) &&
compare(function, rgb.mTransferParameters)) {
return colorSpace;
}
}
}
return null;
}
/**
* Creates a new {@link Renderer} that can be used to visualize and
* debug color spaces. See the documentation of {@link Renderer} for
* more information.
*
* @return A new non-null {@link Renderer} instance
*
* @see Renderer
*
* @hide
*/
@NonNull
public static Renderer createRenderer() {
return new Renderer();
}
static {
sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb(
"sRGB IEC61966-2.1",
SRGB_PRIMARIES,
ILLUMINANT_D65,
new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
Named.SRGB.ordinal()
);
sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb(
"sRGB IEC61966-2.1 (Linear)",
SRGB_PRIMARIES,
ILLUMINANT_D65,
1.0,
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, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
x -> absResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
-0.799f, 2.399f,
Named.EXTENDED_SRGB.ordinal()
);
sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
"scRGB IEC 61966-2-2:2003",
SRGB_PRIMARIES,
ILLUMINANT_D65,
1.0,
-0.5f, 7.499f,
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,
new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
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,
new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
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 },
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,
new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.039, 2.4),
Named.DISPLAY_P3.ordinal()
);
sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb(
"NTSC (1953)",
NTSC_1953_PRIMARIES,
ILLUMINANT_C,
new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
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,
new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
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,
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,
new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8),
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,
1.0,
-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,
1.0,
-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 a, double b, double c, double d, double g) {
return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c;
}
// Piecewise gamma response
private static double response(double x, double a, double b, double c, double d, double g) {
return x >= d ? Math.pow(a * x + b, g) : c * x;
}
// Reciprocal piecewise gamma response
private static double rcpResponse(double x, double a, double b, double c, double d,
double e, double f, double g) {
return x >= d * c ? (Math.pow(x - e, 1.0 / g) - b) / a : (x - f) / c;
}
// Piecewise gamma response
private static double response(double x, double a, double b, double c, double d,
double e, double f, double g) {
return x >= d ? Math.pow(a * x + b, g) + e : c * x + f;
}
// 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 a, double b, double c, double d, double g) {
return Math.copySign(rcpResponse(x < 0.0 ? -x : x, a, b, c, d, g), 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 a, double b, double c, double d, double g) {
return Math.copySign(response(x < 0.0 ? -x : x, a, b, c, d, g), x);
}
/**
* Compares two sets of parametric transfer functions parameters with a precision of 1e-3.
*
* @param a The first set of parameters to compare
* @param b The second set of parameters to compare
* @return True if the two sets are equal, false otherwise
*/
private static boolean compare(
@Nullable Rgb.TransferParameters a,
@Nullable Rgb.TransferParameters b) {
//noinspection SimplifiableIfStatement
if (a == null && b == null) return true;
return a != null && b != null &&
Math.abs(a.a - b.a) < 1e-3 &&
Math.abs(a.b - b.b) < 1e-3 &&
Math.abs(a.c - b.c) < 1e-3 &&
Math.abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF
Math.abs(a.e - b.e) < 1e-3 &&
Math.abs(a.f - b.f) < 1e-3 &&
Math.abs(a.g - b.g) < 1e-3;
}
/**
* 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] };
}
/**
* Converts values from CIE xyY to CIE L*u*v*. Y is assumed to be 1 so the
* input xyY array only contains the x and y components. After this method
* returns, the xyY array contains the converted u and v components.
*
* @param xyY The xyY value to convert to XYZ, cannot be null,
* length must be a multiple of 2
*/
private static void xyYToUv(@NonNull @Size(multiple = 2) float[] xyY) {
for (int i = 0; i < xyY.length; i += 2) {
float x = xyY[i];
float y = xyY[i + 1];
float d = -2.0f * x + 12.0f * y + 3;
float u = (4.0f * x) / d;
float v = (9.0f * y) / d;
xyY[i] = u;
xyY[i + 1] = v;
}
}
/**
* Computes the chromatic adaptation transform from the specified
* source white point to the specified destination white point.
*
* The transform is computed using the von Kries 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.
*/
@AnyThread
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.
*/
@AnyThread
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:
*
* - Three chromaticities of the red, green and blue primaries, which
* define the gamut of the color space.
* - A white point chromaticity that defines the stimulus to which
* color space values are normalized (also just called "white").
* - An opto-electronic transfer function, also called opto-electronic
* conversion function or often, and approximately, gamma function.
* - An electro-optical transfer function, also called electo-optical
* conversion function or often, and approximately, gamma function.
* - A range of valid RGB values (most commonly \([0..1]\)).
*
*
* The most commonly used RGB color space is {@link Named#SRGB sRGB}.
*
* Primaries and white point chromaticities
* 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.
*
*
*
* sRGB primaries and white point
*
*
* Transfer functions
* 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})))$$
*
* If the transfer functions of the color space can be expressed as an
* ICC parametric curve as defined in ICC.1:2004-10, the numeric parameters
* can be retrieved by calling {@link #getTransferParameters()}. This can
* be useful to match color spaces for instance.
*
* 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.
*
* Range
* 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]\).
*
*
*
* Extended sRGB and its large range
*
*
* Converting between RGB color spaces
* 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}.
*/
@AnyThread
public static class Rgb extends ColorSpace {
/**
* {@usesMathJax}
*
* Defines the parameters for the ICC parametric curve type 4, as
* defined in ICC.1:2004-10, section 10.15.
*
* The EOTF is of the form:
*
* \(\begin{equation}
* Y = \begin{cases}c X + f & X \lt d \\
* \left( a X + b \right) ^{g} + e & X \ge d \end{cases}
* \end{equation}\)
*
* The corresponding OETF is simply the inverse function.
*
* The parameters defined by this class form a valid transfer
* function only if all the following conditions are met:
*
* - No parameter is a {@link Double#isNaN(double) Not-a-Number}
* - \(d\) is in the range \([0..1]\)
* - The function is not constant
* - The function is positive and increasing
*
*/
public static class TransferParameters {
/** Variable \(a\) in the equation of the EOTF described above. */
public final double a;
/** Variable \(b\) in the equation of the EOTF described above. */
public final double b;
/** Variable \(c\) in the equation of the EOTF described above. */
public final double c;
/** Variable \(d\) in the equation of the EOTF described above. */
public final double d;
/** Variable \(e\) in the equation of the EOTF described above. */
public final double e;
/** Variable \(f\) in the equation of the EOTF described above. */
public final double f;
/** Variable \(g\) in the equation of the EOTF described above. */
public final double g;
/**
* Defines the parameters for the ICC parametric curve type 3, as
* defined in ICC.1:2004-10, section 10.15.
*
* The EOTF is of the form:
*
* \(\begin{equation}
* Y = \begin{cases}c X & X \lt d \\
* \left( a X + b \right) ^{g} & X \ge d \end{cases}
* \end{equation}\)
*
* This constructor is equivalent to setting \(e\) and \(f\) to 0.
*
* @param a The value of \(a\) in the equation of the EOTF described above
* @param b The value of \(b\) in the equation of the EOTF described above
* @param c The value of \(c\) in the equation of the EOTF described above
* @param d The value of \(d\) in the equation of the EOTF described above
* @param g The value of \(g\) in the equation of the EOTF described above
*
* @throws IllegalArgumentException If the parameters form an invalid transfer function
*/
public TransferParameters(double a, double b, double c, double d, double g) {
this(a, b, c, d, 0.0, 0.0, g);
}
/**
* Defines the parameters for the ICC parametric curve type 4, as
* defined in ICC.1:2004-10, section 10.15.
*
* @param a The value of \(a\) in the equation of the EOTF described above
* @param b The value of \(b\) in the equation of the EOTF described above
* @param c The value of \(c\) in the equation of the EOTF described above
* @param d The value of \(d\) in the equation of the EOTF described above
* @param e The value of \(e\) in the equation of the EOTF described above
* @param f The value of \(f\) in the equation of the EOTF described above
* @param g The value of \(g\) in the equation of the EOTF described above
*
* @throws IllegalArgumentException If the parameters form an invalid transfer function
*/
public TransferParameters(double a, double b, double c, double d, double e,
double f, double g) {
if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) ||
Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) ||
Double.isNaN(g)) {
throw new IllegalArgumentException("Parameters cannot be NaN");
}
// Next representable float after 1.0
// We use doubles here but the representation inside our native code is often floats
if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
throw new IllegalArgumentException("Parameter d must be in the range [0..1], " +
"was " + d);
}
if (d == 0.0 && (a == 0.0 || g == 0.0)) {
throw new IllegalArgumentException(
"Parameter a or g is zero, the transfer function is constant");
}
if (d >= 1.0 && c == 0.0) {
throw new IllegalArgumentException(
"Parameter c is zero, the transfer function is constant");
}
if ((a == 0.0 || g == 0.0) && c == 0.0) {
throw new IllegalArgumentException("Parameter a or g is zero," +
" and c is zero, the transfer function is constant");
}
if (c < 0.0) {
throw new IllegalArgumentException("The transfer function must be increasing");
}
if (a < 0.0 || g < 0.0) {
throw new IllegalArgumentException("The transfer function must be " +
"positive or increasing");
}
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
this.g = g;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TransferParameters that = (TransferParameters) o;
if (Double.compare(that.a, a) != 0) return false;
if (Double.compare(that.b, b) != 0) return false;
if (Double.compare(that.c, c) != 0) return false;
if (Double.compare(that.d, d) != 0) return false;
if (Double.compare(that.e, e) != 0) return false;
if (Double.compare(that.f, f) != 0) return false;
return Double.compare(that.g, g) == 0;
}
@Override
public int hashCode() {
int result;
long temp;
temp = Double.doubleToLongBits(a);
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(b);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(c);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(d);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(e);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(f);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(g);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
@NonNull private final float[] mWhitePoint;
@NonNull private final float[] mPrimaries;
@NonNull private final float[] mTransform;
@NonNull private final float[] mInverseTransform;
@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;
private final boolean mIsWideGamut;
private final boolean mIsSrgb;
@Nullable private TransferParameters mTransferParameters;
/**
* 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:
*
* - The name is null or has a length of 0.
* - The OETF is null or the EOTF is null.
* - The minimum valid value is >= the maximum valid value.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(9) float[] toXYZ,
@NonNull DoubleUnaryOperator oetf,
@NonNull DoubleUnaryOperator eotf) {
this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ),
oetf, eotf, 0.0f, 1.0f, MIN_ID);
}
/**
* 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 IllegalArgumentException If any of the following conditions is met:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - The OETF is null or the EOTF is null.
* - The minimum valid value is >= the maximum valid value.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
@NonNull DoubleUnaryOperator oetf,
@NonNull DoubleUnaryOperator eotf,
float min,
float max) {
this(name, primaries, whitePoint, oetf, eotf, min, max, MIN_ID);
}
/**
* 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 function Parameters for the transfer functions
*
* @throws IllegalArgumentException If any of the following conditions is met:
*
* - The name is null or has a length of 0.
* - Gamma is negative.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(9) float[] toXYZ,
@NonNull TransferParameters function) {
this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), function, MIN_ID);
}
/**
* 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 function Parameters for the transfer functions
*
* @throws IllegalArgumentException If any of the following conditions is met:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - The transfer parameters are invalid.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
@NonNull TransferParameters function) {
this(name, primaries, whitePoint, function, MIN_ID);
}
/**
* 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 function Parameters for the transfer functions
* @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:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
* - The transfer parameters are invalid.
*
*
* @see #get(Named)
*/
private Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
@NonNull TransferParameters function,
@IntRange(from = MIN_ID, to = MAX_ID) int id) {
this(name, primaries, whitePoint,
function.e == 0.0 && function.f == 0.0 ?
x -> rcpResponse(x, function.a, function.b,
function.c, function.d, function.g) :
x -> rcpResponse(x, function.a, function.b, function.c,
function.d, function.e, function.f, function.g),
function.e == 0.0 && function.f == 0.0 ?
x -> response(x, function.a, function.b,
function.c, function.d, function.g) :
x -> response(x, function.a, function.b, function.c,
function.d, function.e, function.f, function.g),
0.0f, 1.0f, id);
mTransferParameters = function;
}
/**
* 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 gamma Gamma to use as the transfer function
*
* @throws IllegalArgumentException If any of the following conditions is met:
*
* - The name is null or has a length of 0.
* - Gamma is negative.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(9) float[] toXYZ,
double gamma) {
this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MIN_ID);
}
/**
* 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 gamma Gamma to use as the transfer function
*
* @throws IllegalArgumentException If any of the following conditions is met:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - Gamma is negative.
*
*
* @see #get(Named)
*/
public Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
double gamma) {
this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MIN_ID);
}
/**
* 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 gamma Gamma to use as the transfer function
* @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:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - The minimum valid value is >= the maximum valid value.
* - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
* - Gamma is negative.
*
*
* @see #get(Named)
*/
private Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
double gamma,
float min,
float max,
@IntRange(from = MIN_ID, to = MAX_ID) int id) {
this(name, primaries, whitePoint,
gamma == 1.0 ? DoubleUnaryOperator.identity() :
x -> Math.pow(x < 0.0 ? 0.0 : x, 1 / gamma),
gamma == 1.0 ? DoubleUnaryOperator.identity() :
x -> Math.pow(x < 0.0 ? 0.0 : x, gamma),
min, max, id);
mTransferParameters = gamma == 1.0 ?
new TransferParameters(0.0, 0.0, 1.0, 1.0 + Math.ulp(1.0f), gamma) :
new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma);
}
/**
* 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:
*
* - The name is null or has a length of 0.
* - The primaries array is null or has a length that is neither 6 or 9.
* - The white point array is null or has a length that is neither 2 or 3.
* - The OETF is null or the EOTF is null.
* - The minimum valid value is >= the maximum valid value.
* - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
*
*
* @see #get(Named)
*/
private Rgb(
@NonNull @Size(min = 1) String name,
@NonNull @Size(min = 6, max = 9) float[] primaries,
@NonNull @Size(min = 2, max = 3) float[] whitePoint,
@NonNull DoubleUnaryOperator oetf,
@NonNull DoubleUnaryOperator eotf,
float min,
float max,
@IntRange(from = MIN_ID, to = MAX_ID) int id) {
super(name, Model.RGB, id);
if (primaries == null || (primaries.length != 6 && primaries.length != 9)) {
throw new IllegalArgumentException("The color space's primaries must be " +
"defined as an array of 6 floats in xyY or 9 floats in XYZ");
}
if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) {
throw new IllegalArgumentException("The color space's white point must be " +
"defined as an array of 2 floats in xyY or 3 float in XYZ");
}
if (oetf == null || eotf == null) {
throw new IllegalArgumentException("The transfer functions of a color space " +
"cannot be null");
}
if (min >= max) {
throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max +
"; min must be strictly < max");
}
mWhitePoint = xyWhitePoint(whitePoint);
mPrimaries = xyPrimaries(primaries);
mTransform = computeXYZMatrix(mPrimaries, mWhitePoint);
mInverseTransform = inverse3x3(mTransform);
mOetf = oetf;
mEotf = eotf;
mMin = min;
mMax = max;
DoubleUnaryOperator clamp = this::clamp;
mClampedOetf = oetf.andThen(clamp);
mClampedEotf = clamp.andThen(eotf);
// A color space is wide-gamut if its area is >90% of NTSC 1953 and
// if it entirely contains the Color space definition in xyY
mIsWideGamut = isWideGamut(mPrimaries, min, max);
mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id);
}
/**
* Creates a copy of the specified color space with a new transform.
*
* @param colorSpace The color space to create a copy of
*/
private Rgb(Rgb colorSpace,
@NonNull @Size(9) float[] transform,
@NonNull @Size(min = 2, max = 3) float[] whitePoint) {
super(colorSpace.getName(), Model.RGB, -1);
mWhitePoint = xyWhitePoint(whitePoint);
mPrimaries = colorSpace.mPrimaries;
mTransform = transform;
mInverseTransform = inverse3x3(transform);
mMin = colorSpace.mMin;
mMax = colorSpace.mMax;
mOetf = colorSpace.mOetf;
mEotf = colorSpace.mEotf;
mClampedOetf = colorSpace.mClampedOetf;
mClampedEotf = colorSpace.mClampedEotf;
mIsWideGamut = colorSpace.mIsWideGamut;
mIsSrgb = colorSpace.mIsSrgb;
mTransferParameters = colorSpace.mTransferParameters;
}
/**
* Copies the non-adapted CIE xyY white point of this color space in
* specified array. The Y component is assumed to be 1 and is therefore
* not copied into the destination. The x and y components are written
* in the array at positions 0 and 1 respectively.
*
* @param whitePoint The destination array, cannot be null, its length
* must be >= 2
*
* @return The destination array passed as a parameter
*
* @see #getWhitePoint(float[])
*/
@NonNull
@Size(min = 2)
public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) {
whitePoint[0] = mWhitePoint[0];
whitePoint[1] = mWhitePoint[1];
return whitePoint;
}
/**
* Returns the non-adapted CIE xyY white point of this color space as
* a new array of 2 floats. The Y component is assumed to be 1 and is
* therefore not copied into the destination. The x and y components
* are written in the array at positions 0 and 1 respectively.
*
* @return A new non-null array of 2 floats
*
* @see #getWhitePoint()
*/
@NonNull
@Size(2)
public float[] getWhitePoint() {
return Arrays.copyOf(mWhitePoint, mWhitePoint.length);
}
/**
* Copies the primaries of this color space in specified array. The Y
* component is assumed to be 1 and is therefore not copied into the
* destination. The x and y components of the first primary are written
* in the array at positions 0 and 1 respectively.
*
* @param primaries The destination array, cannot be null, its length
* must be >= 6
*
* @return The destination array passed as a parameter
*
* @see #getPrimaries(float[])
*/
@NonNull
@Size(min = 6)
public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) {
System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length);
return primaries;
}
/**
* Returns the primaries of this color space as a new array of 6 floats.
* The Y component is assumed to be 1 and is therefore not copied into
* the destination. The x and y components of the first primary are
* written in the array at positions 0 and 1 respectively.
*
* @return A new non-null array of 2 floats
*
* @see #getWhitePoint()
*/
@NonNull
@Size(6)
public float[] getPrimaries() {
return Arrays.copyOf(mPrimaries, mPrimaries.length);
}
/**
* 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} OETF uses \(\gamma=2.2\)
* for instance).
*
* @return A transfer function that converts from linear space to "gamma space"
*
* @see #getEotf()
* @see #getTransferParameters()
*/
@NonNull
public DoubleUnaryOperator getOetf() {
return mClampedOetf;
}
/**
* 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()
* @see #getTransferParameters()
*/
@NonNull
public DoubleUnaryOperator getEotf() {
return mClampedEotf;
}
/**
* Returns the parameters used by the {@link #getEotf() electro-optical}
* and {@link #getOetf() opto-electronic} transfer functions. If the transfer
* functions do not match the ICC parametric curves defined in ICC.1:2004-10
* (section 10.15), this method returns null.
*
* See {@link TransferParameters} for a full description of the transfer
* functions.
*
* @return An instance of {@link TransferParameters} or null if this color
* space's transfer functions do not match the equation defined in
* {@link TransferParameters}
*/
@Nullable
public TransferParameters getTransferParameters() {
return mTransferParameters;
}
@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;
if (mTransferParameters != null) {
return mTransferParameters.equals(rgb.mTransferParameters);
} else if (rgb.mTransferParameters == null) {
return true;
}
//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 + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0);
result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0);
result = 31 * result +
(mTransferParameters != null ? mTransferParameters.hashCode() : 0);
if (mTransferParameters == null) {
result = 31 * result + mOetf.hashCode();
result = 31 * result + mEotf.hashCode();
}
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 as an array of 6 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(6) 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
* @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) {
float[] r = mul3x3Float3(toXYZ, new float[] { 1.0f, 0.0f, 0.0f });
float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, 1.0f, 0.0f });
float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, 1.0f });
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
* @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) {
float[] w = mul3x3Float3(toXYZ, new float[] { 1.0f, 1.0f, 1.0f });
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(6)
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)
*/
@AnyThread
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, @Nullable @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.
*/
@Nullable
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 getRenderIntent() {
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;
}
};
}
}
/**
* A color space renderer can be used to visualize and compare the gamut and
* white point of one or more color spaces. The output is an sRGB {@link Bitmap}
* showing a CIE 1931 xyY or a CIE 1976 UCS chromaticity diagram.
*
* The following code snippet shows how to compare the {@link Named#SRGB}
* and {@link Named#DCI_P3} color spaces in a CIE 1931 diagram:
*
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .size(768)
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
*
*
*
* sRGB vs DCI-P3
*
*
* A renderer can also be used to show the location of specific colors,
* associated with a color space, in the CIE 1931 xyY chromaticity diagram.
* See {@link #add(ColorSpace, float, float, float, int)} for more information.
*
* @see ColorSpace#createRenderer()
*
* @hide
*/
public static class Renderer {
private static final int NATIVE_SIZE = 1440;
private static final float UCS_SCALE = 9.0f / 6.0f;
// Number of subdivision of the inside of the spectral locus
private static final int CHROMATICITY_RESOLUTION = 32;
private static final double ONE_THIRD = 1.0 / 3.0;
@IntRange(from = 128, to = Integer.MAX_VALUE)
private int mSize = 1024;
private boolean mShowWhitePoint = true;
private boolean mClip = false;
private boolean mUcs = false;
private final List> mColorSpaces = new ArrayList<>(2);
private final List mPoints = new ArrayList<>(0);
private Renderer() {
}
/**
* Defines whether the chromaticity diagram should be clipped by the first
* registered color space. The default value is false.
*
* The following code snippet and image show the default behavior:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
*
*
*
* Clipping disabled
*
*
* Here is the same example with clipping enabled:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
*
*
*
* Clipping enabled
*
*
* @param clip True to clip the chromaticity diagram to the first registered color space,
* false otherwise
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer clip(boolean clip) {
mClip = clip;
return this;
}
/**
* Defines whether the chromaticity diagram should use the uniform
* chromaticity scale (CIE 1976 UCS). When the uniform chromaticity scale
* is used, the distance between two points on the diagram is approximately
* proportional to the perceived color difference.
*
* The following code snippet shows how to enable the uniform chromaticity
* scale. The image below shows the result:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .uniformChromaticityScale(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
*
*
*
* CIE 1976 UCS diagram
*
*
* @param ucs True to render the chromaticity diagram as the CIE 1976 UCS diagram
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer uniformChromaticityScale(boolean ucs) {
mUcs = ucs;
return this;
}
/**
* Sets the dimensions (width and height) in pixels of the output bitmap.
* The size must be at least 128px and defaults to 1024px.
*
* @param size The size in pixels of the output bitmap
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer size(@IntRange(from = 128, to = Integer.MAX_VALUE) int size) {
mSize = Math.max(128, size);
return this;
}
/**
* Shows or hides the white point of each color space in the output bitmap.
* The default is true.
*
* @param show True to show the white point of each color space, false
* otherwise
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer showWhitePoint(boolean show) {
mShowWhitePoint = show;
return this;
}
/**
* Adds a color space to represent on the output CIE 1931 chromaticity
* diagram. The color space is represented as a triangle showing the
* footprint of its color gamut and, optionally, the location of its
* white point.
*
* Color spaces with a color model that is not RGB are
* accepted but ignored.
*
* The following code snippet and image show an example of calling this
* method to compare {@link Named#SRGB sRGB} and {@link Named#DCI_P3 DCI-P3}:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
*
*
*
* sRGB vs DCI-P3
*
*
* Adding a color space extending beyond the boundaries of the
* spectral locus will alter the size of the diagram within the output
* bitmap as shown in this example:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .add(ColorSpace.get(ColorSpace.Named.ACES), 0xff097ae9)
* .add(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), 0xff000000)
* .render();
*
*
*
* sRGB, DCI-P3, ACES and scRGB
*
*
* @param colorSpace The color space whose gamut to render on the diagram
* @param color The sRGB color to use to render the color space's gamut and white point
* @return This instance of {@link Renderer}
*
* @see #clip(boolean)
* @see #showWhitePoint(boolean)
*/
@NonNull
public Renderer add(@NonNull ColorSpace colorSpace, @ColorInt int color) {
mColorSpaces.add(new Pair<>(colorSpace, color));
return this;
}
/**
* Adds a color to represent as a point on the chromaticity diagram.
* The color is associated with a color space which will be used to
* perform the conversion to CIE XYZ and compute the location of the point
* on the diagram. The point is rendered as a colored circle.
*
* The following code snippet and image show an example of calling this
* method to render the location of several sRGB colors as white circles:
*
* Bitmap bitmap = ColorSpace.createRenderer()
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.0f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.1f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.2f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.3f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.4f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.5f, 0.1f, 0xffffffff)
* .render();
*
*
*
*
* Locating colors on the chromaticity diagram
*
*
*
* @param colorSpace The color space of the color to locate on the diagram
* @param r The first component of the color to locate on the diagram
* @param g The second component of the color to locate on the diagram
* @param b The third component of the color to locate on the diagram
* @param pointColor The sRGB color to use to render the point on the diagram
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer add(@NonNull ColorSpace colorSpace, float r, float g, float b,
@ColorInt int pointColor) {
mPoints.add(new Point(colorSpace, new float[] { r, g, b }, pointColor));
return this;
}
/**
* Renders the {@link #add(ColorSpace, int) color spaces} and
* {@link #add(ColorSpace, float, float, float, int) points} registered
* with this renderer. The output bitmap is an sRGB image with the
* dimensions specified by calling {@link #size(int)} (1204x1024px by
* default).
*
* @return A new non-null {@link Bitmap} with the dimensions specified
* by {@link #size(int)} (1024x1024 by default)
*/
@NonNull
public Bitmap render() {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
float[] primaries = new float[6];
float[] whitePoint = new float[2];
int width = NATIVE_SIZE;
int height = NATIVE_SIZE;
Path path = new Path();
setTransform(canvas, width, height, primaries);
drawBox(canvas, width, height, paint, path);
setUcsTransform(canvas, height);
drawLocus(canvas, width, height, paint, path, primaries);
drawGamuts(canvas, width, height, paint, path, primaries, whitePoint);
drawPoints(canvas, width, height, paint);
return bitmap;
}
/**
* Draws registered points at their correct position in the xyY coordinates.
* Each point is positioned according to its associated color space.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
*/
private void drawPoints(@NonNull Canvas canvas, int width, int height,
@NonNull Paint paint) {
paint.setStyle(Paint.Style.FILL);
float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f);
float[] v = new float[3];
float[] xy = new float[2];
for (final Point point : mPoints) {
v[0] = point.mRgb[0];
v[1] = point.mRgb[1];
v[2] = point.mRgb[2];
point.mColorSpace.toXyz(v);
paint.setColor(point.mColor);
// XYZ to xyY, assuming Y=1.0, then to L*u*v* if needed
float sum = v[0] + v[1] + v[2];
xy[0] = v[0] / sum;
xy[1] = v[1] / sum;
if (mUcs) xyYToUv(xy);
canvas.drawCircle(width * xy[0], height - height * xy[1], radius, paint);
}
}
/**
* Draws the color gamuts and white points of all the registered color
* spaces. Only color spaces with an RGB color model are rendered, the
* others are ignored.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
* @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
* @param whitePoint A pre-allocated array of 2 floats to avoid temporary allocations
*/
private void drawGamuts(
@NonNull Canvas canvas, int width, int height,
@NonNull Paint paint, @NonNull Path path,
@NonNull @Size(6) float[] primaries, @NonNull @Size(2) float[] whitePoint) {
float radius = 4.0f / (mUcs ? UCS_SCALE : 1.0f);
for (final Pair item : mColorSpaces) {
ColorSpace colorSpace = item.first;
int color = item.second;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries, mUcs);
path.rewind();
path.moveTo(width * primaries[0], height - height * primaries[1]);
path.lineTo(width * primaries[2], height - height * primaries[3]);
path.lineTo(width * primaries[4], height - height * primaries[5]);
path.close();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(color);
canvas.drawPath(path, paint);
// Draw the white point
if (mShowWhitePoint) {
rgb.getWhitePoint(whitePoint);
if (mUcs) xyYToUv(whitePoint);
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
canvas.drawCircle(
width * whitePoint[0], height - height * whitePoint[1], radius, paint);
}
}
}
/**
* Returns the primaries of the specified RGB color space. This method handles
* the special case of the {@link Named#EXTENDED_SRGB} family of color spaces.
*
* @param rgb The color space whose primaries to extract
* @param primaries A pre-allocated array of 6 floats that will hold the result
* @param asUcs True if the primaries should be returned in Luv, false for xyY
*/
@NonNull
@Size(6)
private static void getPrimaries(@NonNull Rgb rgb,
@NonNull @Size(6) float[] primaries, boolean asUcs) {
// TODO: We should find a better way to handle these cases
if (rgb.equals(ColorSpace.get(Named.EXTENDED_SRGB)) ||
rgb.equals(ColorSpace.get(Named.LINEAR_EXTENDED_SRGB))) {
primaries[0] = 1.41f;
primaries[1] = 0.33f;
primaries[2] = 0.27f;
primaries[3] = 1.24f;
primaries[4] = -0.23f;
primaries[5] = -0.57f;
} else {
rgb.getPrimaries(primaries);
}
if (asUcs) xyYToUv(primaries);
}
/**
* Draws the CIE 1931 chromaticity diagram: the spectral locus and its inside.
* This method respect the clip parameter.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
* @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
*/
private void drawLocus(
@NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
@NonNull Path path, @NonNull @Size(6) float[] primaries) {
int vertexCount = SPECTRUM_LOCUS_X.length * CHROMATICITY_RESOLUTION * 6;
float[] vertices = new float[vertexCount * 2];
int[] colors = new int[vertices.length];
computeChromaticityMesh(vertices, colors);
if (mUcs) xyYToUv(vertices);
for (int i = 0; i < vertices.length; i += 2) {
vertices[i] *= width;
vertices[i + 1] = height - vertices[i + 1] * height;
}
// Draw the spectral locus
if (mClip && mColorSpaces.size() > 0) {
for (final Pair item : mColorSpaces) {
ColorSpace colorSpace = item.first;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries, mUcs);
break;
}
path.rewind();
path.moveTo(width * primaries[0], height - height * primaries[1]);
path.lineTo(width * primaries[2], height - height * primaries[3]);
path.lineTo(width * primaries[4], height - height * primaries[5]);
path.close();
int[] solid = new int[colors.length];
Arrays.fill(solid, 0xff6c6c6c);
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, solid, 0, null, 0, 0, paint);
canvas.save();
canvas.clipPath(path);
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, colors, 0, null, 0, 0, paint);
canvas.restore();
} else {
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, colors, 0, null, 0, 0, paint);
}
// Draw the non-spectral locus
int index = (CHROMATICITY_RESOLUTION - 1) * 12;
path.reset();
path.moveTo(vertices[index], vertices[index + 1]);
for (int x = 2; x < SPECTRUM_LOCUS_X.length; x++) {
index += CHROMATICITY_RESOLUTION * 12;
path.lineTo(vertices[index], vertices[index + 1]);
}
path.close();
paint.setStrokeWidth(4.0f / (mUcs ? UCS_SCALE : 1.0f));
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xff000000);
canvas.drawPath(path, paint);
}
/**
* Draws the diagram box, including borders, tick marks, grid lines
* and axis labels.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
*/
private void drawBox(@NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
@NonNull Path path) {
int lineCount = 10;
float scale = 1.0f;
if (mUcs) {
lineCount = 7;
scale = UCS_SCALE;
}
// Draw the unit grid
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2.0f);
paint.setColor(0xffc0c0c0);
for (int i = 1; i < lineCount - 1; i++) {
float v = i / 10.0f;
float x = (width * v) * scale;
float y = height - (height * v) * scale;
canvas.drawLine(0.0f, y, 0.9f * width, y, paint);
canvas.drawLine(x, height, x, 0.1f * height, paint);
}
// Draw tick marks
paint.setStrokeWidth(4.0f);
paint.setColor(0xff000000);
for (int i = 1; i < lineCount - 1; i++) {
float v = i / 10.0f;
float x = (width * v) * scale;
float y = height - (height * v) * scale;
canvas.drawLine(0.0f, y, width / 100.0f, y, paint);
canvas.drawLine(x, height, x, height - (height / 100.0f), paint);
}
// Draw the axis labels
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(36.0f);
paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
Rect bounds = new Rect();
for (int i = 1; i < lineCount - 1; i++) {
String text = "0." + i;
paint.getTextBounds(text, 0, text.length(), bounds);
float v = i / 10.0f;
float x = (width * v) * scale;
float y = height - (height * v) * scale;
canvas.drawText(text, -0.05f * width + 10, y + bounds.height() / 2.0f, paint);
canvas.drawText(text, x - bounds.width() / 2.0f,
height + bounds.height() + 16, paint);
}
paint.setStyle(Paint.Style.STROKE);
// Draw the diagram box
path.moveTo(0.0f, height);
path.lineTo(0.9f * width, height);
path.lineTo(0.9f * width, 0.1f * height);
path.lineTo(0.0f, 0.1f * height);
path.close();
canvas.drawPath(path, paint);
}
/**
* Computes and applies the Canvas transforms required to make the color
* gamut of each color space visible in the final image.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param primaries Array of 6 floats used to avoid temporary allocations
*/
private void setTransform(@NonNull Canvas canvas, int width, int height,
@NonNull @Size(6) float[] primaries) {
RectF primariesBounds = new RectF();
for (final Pair item : mColorSpaces) {
ColorSpace colorSpace = item.first;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries, mUcs);
primariesBounds.left = Math.min(primariesBounds.left, primaries[4]);
primariesBounds.top = Math.min(primariesBounds.top, primaries[5]);
primariesBounds.right = Math.max(primariesBounds.right, primaries[0]);
primariesBounds.bottom = Math.max(primariesBounds.bottom, primaries[3]);
}
float max = mUcs ? 0.6f : 0.9f;
primariesBounds.left = Math.min(0.0f, primariesBounds.left);
primariesBounds.top = Math.min(0.0f, primariesBounds.top);
primariesBounds.right = Math.max(max, primariesBounds.right);
primariesBounds.bottom = Math.max(max, primariesBounds.bottom);
float scaleX = max / primariesBounds.width();
float scaleY = max / primariesBounds.height();
float scale = Math.min(scaleX, scaleY);
canvas.scale(mSize / (float) NATIVE_SIZE, mSize / (float) NATIVE_SIZE);
canvas.scale(scale, scale);
canvas.translate(
(primariesBounds.width() - max) * width / 2.0f,
(primariesBounds.height() - max) * height / 2.0f);
// The spectrum extends ~0.85 vertically and ~0.65 horizontally
// We shift the canvas a little bit to get nicer margins
canvas.translate(0.05f * width, -0.05f * height);
}
/**
* Computes and applies the Canvas transforms required to render the CIE
* 197 UCS chromaticity diagram.
*
* @param canvas The canvas to transform
* @param height Height in pixel of the final image
*/
private void setUcsTransform(@NonNull Canvas canvas, int height) {
if (mUcs) {
canvas.translate(0.0f, (height - height * UCS_SCALE));
canvas.scale(UCS_SCALE, UCS_SCALE);
}
}
// X coordinates of the spectral locus in CIE 1931
private static final float[] SPECTRUM_LOCUS_X = {
0.175596f, 0.172787f, 0.170806f, 0.170085f, 0.160343f,
0.146958f, 0.139149f, 0.133536f, 0.126688f, 0.115830f,
0.109616f, 0.099146f, 0.091310f, 0.078130f, 0.068717f,
0.054675f, 0.040763f, 0.027497f, 0.016270f, 0.008169f,
0.004876f, 0.003983f, 0.003859f, 0.004646f, 0.007988f,
0.013870f, 0.022244f, 0.027273f, 0.032820f, 0.038851f,
0.045327f, 0.052175f, 0.059323f, 0.066713f, 0.074299f,
0.089937f, 0.114155f, 0.138695f, 0.154714f, 0.192865f,
0.229607f, 0.265760f, 0.301588f, 0.337346f, 0.373083f,
0.408717f, 0.444043f, 0.478755f, 0.512467f, 0.544767f,
0.575132f, 0.602914f, 0.627018f, 0.648215f, 0.665746f,
0.680061f, 0.691487f, 0.700589f, 0.707901f, 0.714015f,
0.719017f, 0.723016f, 0.734674f, 0.717203f, 0.699732f,
0.682260f, 0.664789f, 0.647318f, 0.629847f, 0.612376f,
0.594905f, 0.577433f, 0.559962f, 0.542491f, 0.525020f,
0.507549f, 0.490077f, 0.472606f, 0.455135f, 0.437664f,
0.420193f, 0.402721f, 0.385250f, 0.367779f, 0.350308f,
0.332837f, 0.315366f, 0.297894f, 0.280423f, 0.262952f,
0.245481f, 0.228010f, 0.210538f, 0.193067f, 0.175596f
};
// Y coordinates of the spectral locus in CIE 1931
private static final float[] SPECTRUM_LOCUS_Y = {
0.005295f, 0.004800f, 0.005472f, 0.005976f, 0.014496f,
0.026643f, 0.035211f, 0.042704f, 0.053441f, 0.073601f,
0.086866f, 0.112037f, 0.132737f, 0.170464f, 0.200773f,
0.254155f, 0.317049f, 0.387997f, 0.463035f, 0.538504f,
0.587196f, 0.610526f, 0.654897f, 0.675970f, 0.715407f,
0.750246f, 0.779682f, 0.792153f, 0.802971f, 0.812059f,
0.819430f, 0.825200f, 0.829460f, 0.832306f, 0.833833f,
0.833316f, 0.826231f, 0.814796f, 0.805884f, 0.781648f,
0.754347f, 0.724342f, 0.692326f, 0.658867f, 0.624470f,
0.589626f, 0.554734f, 0.520222f, 0.486611f, 0.454454f,
0.424252f, 0.396516f, 0.372510f, 0.351413f, 0.334028f,
0.319765f, 0.308359f, 0.299317f, 0.292044f, 0.285945f,
0.280951f, 0.276964f, 0.265326f, 0.257200f, 0.249074f,
0.240948f, 0.232822f, 0.224696f, 0.216570f, 0.208444f,
0.200318f, 0.192192f, 0.184066f, 0.175940f, 0.167814f,
0.159688f, 0.151562f, 0.143436f, 0.135311f, 0.127185f,
0.119059f, 0.110933f, 0.102807f, 0.094681f, 0.086555f,
0.078429f, 0.070303f, 0.062177f, 0.054051f, 0.045925f,
0.037799f, 0.029673f, 0.021547f, 0.013421f, 0.005295f
};
/**
* Computes a 2D mesh representation of the CIE 1931 chromaticity
* diagram.
*
* @param vertices Array of floats that will hold the mesh vertices
* @param colors Array of floats that will hold the mesh colors
*/
private static void computeChromaticityMesh(@NonNull float[] vertices,
@NonNull int[] colors) {
ColorSpace colorSpace = get(Named.SRGB);
float[] color = new float[3];
int vertexIndex = 0;
int colorIndex = 0;
for (int x = 0; x < SPECTRUM_LOCUS_X.length; x++) {
int nextX = (x % (SPECTRUM_LOCUS_X.length - 1)) + 1;
float a1 = (float) Math.atan2(
SPECTRUM_LOCUS_Y[x] - ONE_THIRD,
SPECTRUM_LOCUS_X[x] - ONE_THIRD);
float a2 = (float) Math.atan2(
SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD,
SPECTRUM_LOCUS_X[nextX] - ONE_THIRD);
float radius1 = (float) Math.pow(
sqr(SPECTRUM_LOCUS_X[x] - ONE_THIRD) +
sqr(SPECTRUM_LOCUS_Y[x] - ONE_THIRD),
0.5);
float radius2 = (float) Math.pow(
sqr(SPECTRUM_LOCUS_X[nextX] - ONE_THIRD) +
sqr(SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD),
0.5);
// Compute patches; each patch is a quad with a different
// color associated with each vertex
for (int c = 1; c <= CHROMATICITY_RESOLUTION; c++) {
float f1 = c / (float) CHROMATICITY_RESOLUTION;
float f2 = (c - 1) / (float) CHROMATICITY_RESOLUTION;
double cr1 = radius1 * Math.cos(a1);
double sr1 = radius1 * Math.sin(a1);
double cr2 = radius2 * Math.cos(a2);
double sr2 = radius2 * Math.sin(a2);
// Compute the XYZ coordinates of the 4 vertices of the patch
float v1x = (float) (ONE_THIRD + cr1 * f1);
float v1y = (float) (ONE_THIRD + sr1 * f1);
float v1z = 1 - v1x - v1y;
float v2x = (float) (ONE_THIRD + cr1 * f2);
float v2y = (float) (ONE_THIRD + sr1 * f2);
float v2z = 1 - v2x - v2y;
float v3x = (float) (ONE_THIRD + cr2 * f2);
float v3y = (float) (ONE_THIRD + sr2 * f2);
float v3z = 1 - v3x - v3y;
float v4x = (float) (ONE_THIRD + cr2 * f1);
float v4y = (float) (ONE_THIRD + sr2 * f1);
float v4z = 1 - v4x - v4y;
// Compute the sRGB representation of each XYZ coordinate of the patch
colors[colorIndex ] = computeColor(color, v1x, v1y, v1z, colorSpace);
colors[colorIndex + 1] = computeColor(color, v2x, v2y, v2z, colorSpace);
colors[colorIndex + 2] = computeColor(color, v3x, v3y, v3z, colorSpace);
colors[colorIndex + 3] = colors[colorIndex];
colors[colorIndex + 4] = colors[colorIndex + 2];
colors[colorIndex + 5] = computeColor(color, v4x, v4y, v4z, colorSpace);
colorIndex += 6;
// Flip the mesh upside down to match Canvas' coordinates system
vertices[vertexIndex++] = v1x;
vertices[vertexIndex++] = v1y;
vertices[vertexIndex++] = v2x;
vertices[vertexIndex++] = v2y;
vertices[vertexIndex++] = v3x;
vertices[vertexIndex++] = v3y;
vertices[vertexIndex++] = v1x;
vertices[vertexIndex++] = v1y;
vertices[vertexIndex++] = v3x;
vertices[vertexIndex++] = v3y;
vertices[vertexIndex++] = v4x;
vertices[vertexIndex++] = v4y;
}
}
}
@ColorInt
private static int computeColor(@NonNull @Size(3) float[] color,
float x, float y, float z, @NonNull ColorSpace cs) {
color[0] = x;
color[1] = y;
color[2] = z;
cs.fromXyz(color);
return 0xff000000 |
(((int) (color[0] * 255.0f) & 0xff) << 16) |
(((int) (color[1] * 255.0f) & 0xff) << 8) |
(((int) (color[2] * 255.0f) & 0xff) );
}
private static double sqr(double v) {
return v * v;
}
private static class Point {
@NonNull final ColorSpace mColorSpace;
@NonNull final float[] mRgb;
final int mColor;
Point(@NonNull ColorSpace colorSpace,
@NonNull @Size(3) float[] rgb, @ColorInt int color) {
mColorSpace = colorSpace;
mRgb = rgb;
mColor = color;
}
}
}
}