/* * 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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1 (Linear)
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB-nl IEC 61966-2-2:2003
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB IEC 61966-2-2:2003
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NameRec. ITU-R BT.709-5
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.7080.1700.1310.3127
y0.2920.7970.0460.3290
PropertyValue
NameRec. ITU-R BT.2020-1
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.314
y0.3200.6900.0600.351
PropertyValue
NameSMPTE RP 431-2-2007 DCI (P3)
CIE standard illuminantN/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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.3127
y0.3200.6900.0600.3290
PropertyValue
NameDisplay P3
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.670.210.140.310
y0.330.710.080.316
PropertyValue
NameNTSC (1953)
CIE standard illuminantC
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.6300.3100.1550.3127
y0.3400.5950.0700.3290
PropertyValue
NameSMPTE-C RGB
CIE standard illuminantD65
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).

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.640.210.150.3127
y0.330.710.060.3290
PropertyValue
NameAdobe RGB (1998)
CIE standard illuminantD65
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.73470.15960.03660.3457
y0.26530.84040.00010.3585
PropertyValue
NameROMM RGB ISO 22028-2:2013
CIE standard illuminantD50
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.734700.000000.000100.32168
y0.265301.00000-0.077000.33767
PropertyValue
NameSMPTE ST 2065-1:2012 ACES
CIE standard illuminantD60
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.

* * * * * * * * * * * * * * * * * * *
ChromaticityRedGreenBlueWhite point
x0.7130.1650.1280.32168
y0.2930.8300.0440.33767
PropertyValue
NameAcademy S-2014-004 ACEScg
CIE standard illuminantD60
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.

* * * * * *
PropertyValue
NameGeneric XYZ
CIE standard illuminantD50
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.

* * * * * *
PropertyValue
NameGeneric L*a*b*
CIE standard illuminantD50
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:

* * *

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:

* *

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:

* * *

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:

* */ 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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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:

* * * @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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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: * * * @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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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: * * * @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:

* * * * * *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
* *

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: * * * @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; } } } }