diff --git a/ImageSharp.sln b/ImageSharp.sln
index 162de8416..7ccd92c07 100644
--- a/ImageSharp.sln
+++ b/ImageSharp.sln
@@ -661,6 +661,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-493
tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{95E45DDE-A67D-48AD-BBA8-5FAA151B860D}"
+ ProjectSection(SolutionItems) = preProject
+ tests\Images\Input\Icon\aero_arrow.cur = tests\Images\Input\Icon\aero_arrow.cur
+ tests\Images\Input\Icon\flutter.ico = tests\Images\Input\Icon\flutter.ico
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -714,6 +720,7 @@ Global
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
+ {95E45DDE-A67D-48AD-BBA8-5FAA151B860D} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
diff --git a/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs b/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs
new file mode 100644
index 000000000..7e4a9c413
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Enumerate the possible sources of the white point used in chromatic adaptation.
+///
+public enum ChromaticAdaptionWhitePointSource
+{
+ ///
+ /// The white point of the source color space.
+ ///
+ WhitePoint,
+
+ ///
+ /// The white point of the source working space.
+ ///
+ RgbWorkingSpace
+}
diff --git a/src/ImageSharp/ColorSpaces/Conversion/CieConstants.cs b/src/ImageSharp/ColorProfiles/CieConstants.cs
similarity index 67%
rename from src/ImageSharp/ColorSpaces/Conversion/CieConstants.cs
rename to src/ImageSharp/ColorProfiles/CieConstants.cs
index 7c8794404..d13a84450 100644
--- a/src/ImageSharp/ColorSpaces/Conversion/CieConstants.cs
+++ b/src/ImageSharp/ColorProfiles/CieConstants.cs
@@ -1,7 +1,7 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.ColorSpaces.Conversion;
+namespace SixLabors.ImageSharp.ColorProfiles;
///
/// Constants use for Cie conversion calculations
@@ -12,10 +12,10 @@ internal static class CieConstants
///
/// 216F / 24389F
///
- public const float Epsilon = 0.008856452F;
+ public const float Epsilon = 216f / 24389f;
///
/// 24389F / 27F
///
- public const float Kappa = 903.2963F;
+ public const float Kappa = 24389f / 27f;
}
diff --git a/src/ImageSharp/ColorProfiles/CieLab.cs b/src/ImageSharp/ColorProfiles/CieLab.cs
new file mode 100644
index 000000000..377cc20a9
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieLab.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Represents a CIE L*a*b* 1976 color.
+///
+///
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieLab : IProfileConnectingSpace
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The lightness dimension.
+ /// The a (green - magenta) component.
+ /// The b (blue - yellow) component.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLab(float l, float a, float b)
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.L = l;
+ this.A = a;
+ this.B = b;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the l, a, b components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLab(Vector3 vector)
+ : this()
+ {
+ this.L = vector.X;
+ this.A = vector.Y;
+ this.B = vector.Z;
+ }
+
+ ///
+ /// Gets the lightness dimension.
+ /// A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).
+ ///
+ public float L { get; }
+
+ ///
+ /// Gets the a color component.
+ /// A value usually ranging from -100 to 100. Negative is green, positive magenta.
+ ///
+ public float A { get; }
+
+ ///
+ /// Gets the b color component.
+ /// A value usually ranging from -100 to 100. Negative is blue, positive is yellow
+ ///
+ public float B { get; }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(CieLab left, CieLab right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static CieLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ {
+ // Conversion algorithm described here:
+ // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
+ CieXyz whitePoint = options.TargetWhitePoint;
+ float wx = whitePoint.X, wy = whitePoint.Y, wz = whitePoint.Z;
+
+ float xr = source.X / wx, yr = source.Y / wy, zr = source.Z / wz;
+
+ const float inv116 = 1 / 116F;
+
+ float fx = xr > CieConstants.Epsilon ? MathF.Pow(xr, 0.3333333F) : ((CieConstants.Kappa * xr) + 16F) * inv116;
+ float fy = yr > CieConstants.Epsilon ? MathF.Pow(yr, 0.3333333F) : ((CieConstants.Kappa * yr) + 16F) * inv116;
+ float fz = zr > CieConstants.Epsilon ? MathF.Pow(zr, 0.3333333F) : ((CieConstants.Kappa * zr) + 16F) * inv116;
+
+ float l = (116F * fy) - 16F;
+ float a = 500F * (fx - fy);
+ float b = 200F * (fy - fz);
+
+ return new CieLab(l, a, b);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieXyz xyz = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in xyz);
+ }
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ // Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
+ float l = this.L, a = this.A, b = this.B;
+ float fy = (l + 16) / 116F;
+ float fx = (a / 500F) + fy;
+ float fz = fy - (b / 200F);
+
+ float fx3 = Numerics.Pow3(fx);
+ float fz3 = Numerics.Pow3(fz);
+
+ float xr = fx3 > CieConstants.Epsilon ? fx3 : ((116F * fx) - 16F) / CieConstants.Kappa;
+ float yr = l > CieConstants.Kappa * CieConstants.Epsilon ? Numerics.Pow3((l + 16F) / 116F) : l / CieConstants.Kappa;
+ float zr = fz3 > CieConstants.Epsilon ? fz3 : ((116F * fz) - 16F) / CieConstants.Kappa;
+
+ CieXyz whitePoint = options.WhitePoint;
+ Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
+ Vector3 xyzr = new(xr, yr, zr);
+
+ return new(xyzr * wxyz);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieLab lab = source[i];
+ destination[i] = lab.ToProfileConnectingSpace(options);
+ }
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.WhitePoint;
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B);
+
+ ///
+ public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
+
+ ///
+ public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CieLab other)
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
+}
diff --git a/src/ImageSharp/ColorSpaces/CieLch.cs b/src/ImageSharp/ColorProfiles/CieLch.cs
similarity index 50%
rename from src/ImageSharp/ColorSpaces/CieLch.cs
rename to src/ImageSharp/ColorProfiles/CieLch.cs
index 48e8e2c6d..131978340 100644
--- a/src/ImageSharp/ColorSpaces/CieLch.cs
+++ b/src/ImageSharp/ColorProfiles/CieLch.cs
@@ -3,21 +3,17 @@
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
-namespace SixLabors.ImageSharp.ColorSpaces;
+namespace SixLabors.ImageSharp.ColorProfiles;
///
/// Represents the CIE L*C*h°, cylindrical form of the CIE L*a*b* 1976 color.
///
///
-public readonly struct CieLch : IEquatable
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieLch : IColorProfile
{
- ///
- /// D50 standard illuminant.
- /// Used when reference white is not specified explicitly.
- ///
- public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
-
private static readonly Vector3 Min = new(0, -200, 0);
private static readonly Vector3 Max = new(100, 200, 360);
@@ -27,23 +23,9 @@ public readonly struct CieLch : IEquatable
/// The lightness dimension.
/// The chroma, relative saturation.
/// The hue in degrees.
- /// Uses as white point.
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(float l, float c, float h)
- : this(l, c, h, DefaultWhitePoint)
- {
- }
-
- ///
- /// Initializes a new instance of the struct.
- ///
- /// The lightness dimension.
- /// The chroma, relative saturation.
- /// The hue in degrees.
- /// The reference white point.
- [MethodImpl(InliningOptions.ShortMethod)]
- public CieLch(float l, float c, float h, CieXyz whitePoint)
- : this(new Vector3(l, c, h), whitePoint)
+ : this(new Vector3(l, c, h))
{
}
@@ -51,50 +33,32 @@ public readonly struct CieLch : IEquatable
/// Initializes a new instance of the struct.
///
/// The vector representing the l, c, h components.
- /// Uses as white point.
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(Vector3 vector)
- : this(vector, DefaultWhitePoint)
- {
- }
-
- ///
- /// Initializes a new instance of the struct.
- ///
- /// The vector representing the l, c, h components.
- /// The reference white point.
- [MethodImpl(InliningOptions.ShortMethod)]
- public CieLch(Vector3 vector, CieXyz whitePoint)
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
this.H = vector.Z;
- this.WhitePoint = whitePoint;
}
///
/// Gets the lightness dimension.
/// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).
///
- public readonly float L { get; }
+ public float L { get; }
///
/// Gets the a chroma component.
/// A value ranging from 0 to 200.
///
- public readonly float C { get; }
+ public float C { get; }
///
/// Gets the h° hue component in degrees.
/// A value ranging from 0 to 360.
///
- public readonly float H { get; }
-
- ///
- /// Gets the reference white point of this color
- ///
- public readonly CieXyz WhitePoint { get; }
+ public float H { get; }
///
/// Compares two objects for equality.
@@ -104,7 +68,7 @@ public readonly struct CieLch : IEquatable
///
/// True if the current left is equal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieLch left, CieLch right) => left.Equals(right);
///
@@ -115,45 +79,88 @@ public readonly struct CieLch : IEquatable
///
/// True if the current left is unequal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLch left, CieLch right) => !left.Equals(right);
///
- public override int GetHashCode()
- => HashCode.Combine(this.L, this.C, this.H, this.WhitePoint);
+ public static CieLch FromProfileConnectingSpace(ColorConversionOptions options, in CieLab source)
+ {
+ // Conversion algorithm described here:
+ // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
+ float l = source.L, a = source.A, b = source.B;
+ float c = MathF.Sqrt((a * a) + (b * b));
+ float hRadians = MathF.Atan2(b, a);
+ float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
+
+ // Wrap the angle round at 360.
+ hDegrees %= 360;
+
+ // Make sure it's not negative.
+ while (hDegrees < 0)
+ {
+ hDegrees += 360;
+ }
- ///
- public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
+ return new CieLch(l, c, hDegrees);
+ }
///
- [MethodImpl(InliningOptions.ShortMethod)]
- public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other);
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieLab lab = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in lab);
+ }
+ }
///
- [MethodImpl(InliningOptions.ShortMethod)]
- public bool Equals(CieLch other)
- => this.L.Equals(other.L)
- && this.C.Equals(other.C)
- && this.H.Equals(other.H)
- && this.WhitePoint.Equals(other.WhitePoint);
+ public CieLab ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ // Conversion algorithm described here:
+ // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
+ float l = this.L, c = this.C, hDegrees = this.H;
+ float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
- ///
- /// Computes the saturation of the color (chroma normalized by lightness)
- ///
- ///
- /// A value ranging from 0 to 100.
- ///
- /// The
- [MethodImpl(InliningOptions.ShortMethod)]
- public float Saturation()
+ float a = c * MathF.Cos(hRadians);
+ float b = c * MathF.Sin(hRadians);
+
+ return new CieLab(l, a, b);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
{
- float result = 100 * (this.C / this.L);
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
- if (float.IsNaN(result))
+ for (int i = 0; i < source.Length; i++)
{
- return 0;
+ CieLch lch = source[i];
+ destination[i] = lch.ToProfileConnectingSpace(options);
}
-
- return result;
}
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.WhitePoint;
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(this.L, this.C, this.H);
+
+ ///
+ public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CieLch other)
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
}
diff --git a/src/ImageSharp/ColorProfiles/CieLchuv.cs b/src/ImageSharp/ColorProfiles/CieLchuv.cs
new file mode 100644
index 000000000..7fd95feb1
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieLchuv.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Represents the CIE L*C*h°, cylindrical form of the CIE L*u*v* 1976 color.
+///
+///
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieLchuv : IColorProfile
+{
+ private static readonly Vector3 Min = new(0, -200, 0);
+ private static readonly Vector3 Max = new(100, 200, 360);
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The lightness dimension.
+ /// The chroma, relative saturation.
+ /// The hue in degrees.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLchuv(float l, float c, float h)
+ : this(new Vector3(l, c, h))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the l, c, h components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLchuv(Vector3 vector)
+ : this()
+ {
+ vector = Vector3.Clamp(vector, Min, Max);
+ this.L = vector.X;
+ this.C = vector.Y;
+ this.H = vector.Z;
+ }
+
+ ///
+ /// Gets the lightness dimension.
+ /// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).
+ ///
+ public float L { get; }
+
+ ///
+ /// Gets the a chroma component.
+ /// A value ranging from 0 to 200.
+ ///
+ public float C { get; }
+
+ ///
+ /// Gets the h° hue component in degrees.
+ /// A value ranging from 0 to 360.
+ ///
+ public float H { get; }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ public static bool operator ==(CieLchuv left, CieLchuv right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ public static bool operator !=(CieLchuv left, CieLchuv right) => !left.Equals(right);
+
+ ///
+ public static CieLchuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ {
+ CieLuv luv = CieLuv.FromProfileConnectingSpace(options, source);
+
+ // Conversion algorithm described here:
+ // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
+ float l = luv.L, u = luv.U, v = luv.V;
+ float c = MathF.Sqrt((u * u) + (v * v));
+ float hRadians = MathF.Atan2(v, u);
+ float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
+
+ // Wrap the angle round at 360.
+ hDegrees %= 360;
+
+ // Make sure it's not negative.
+ while (hDegrees < 0)
+ {
+ hDegrees += 360;
+ }
+
+ return new CieLchuv(l, c, hDegrees);
+ }
+
+ ///
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieXyz xyz = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in xyz);
+ }
+ }
+
+ ///
+ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ // Conversion algorithm described here:
+ // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
+ float l = this.L, c = this.C, hDegrees = this.H;
+ float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
+
+ float u = c * MathF.Cos(hRadians);
+ float v = c * MathF.Sin(hRadians);
+
+ CieLuv luv = new(l, u, v);
+ return luv.ToProfileConnectingSpace(options);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieLchuv lch = source[i];
+ destination[i] = lch.ToProfileConnectingSpace(options);
+ }
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.WhitePoint;
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(this.L, this.C, this.H);
+
+ ///
+ public override string ToString()
+ => FormattableString.Invariant($"CieLchuv({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is CieLchuv other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CieLchuv other)
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
+}
diff --git a/src/ImageSharp/ColorProfiles/CieLuv.cs b/src/ImageSharp/ColorProfiles/CieLuv.cs
new file mode 100644
index 000000000..97e2826f7
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieLuv.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// The CIE 1976 (L*, u*, v*) color space, commonly known by its abbreviation CIELUV, is a color space adopted by the International
+/// Commission on Illumination (CIE) in 1976, as a simple-to-compute transformation of the 1931 CIE XYZ color space, but which
+/// attempted perceptual uniformity
+///
+///
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieLuv : IColorProfile
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The lightness dimension.
+ /// The blue-yellow chromaticity coordinate of the given white point.
+ /// The red-green chromaticity coordinate of the given white point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLuv(float l, float u, float v)
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.L = l;
+ this.U = u;
+ this.V = v;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the l, u, v components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLuv(Vector3 vector)
+ : this()
+ {
+ this.L = vector.X;
+ this.U = vector.Y;
+ this.V = vector.Z;
+ }
+
+ ///
+ /// Gets the lightness dimension
+ /// A value usually ranging between 0 and 100.
+ ///
+ public float L { get; }
+
+ ///
+ /// Gets the blue-yellow chromaticity coordinate of the given white point.
+ /// A value usually ranging between -100 and 100.
+ ///
+ public float U { get; }
+
+ ///
+ /// Gets the red-green chromaticity coordinate of the given white point.
+ /// A value usually ranging between -100 and 100.
+ ///
+ public float V { get; }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(CieLuv left, CieLuv right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(CieLuv left, CieLuv right) => !left.Equals(right);
+
+ ///
+ public static CieLuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ {
+ // Use doubles here for accuracy.
+ // Conversion algorithm described here:
+ // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html
+ CieXyz whitePoint = options.TargetWhitePoint;
+
+ double yr = source.Y / whitePoint.Y;
+
+ double den = source.X + (15 * source.Y) + (3 * source.Z);
+ double up = den > 0 ? ComputeU(in source) : 0;
+ double vp = den > 0 ? ComputeV(in source) : 0;
+ double upr = ComputeU(in whitePoint);
+ double vpr = ComputeV(in whitePoint);
+
+ const double e = 1 / 3d;
+ double l = yr > CieConstants.Epsilon
+ ? ((116 * Math.Pow(yr, e)) - 16d)
+ : (CieConstants.Kappa * yr);
+
+ if (double.IsNaN(l) || l == -0d)
+ {
+ l = 0;
+ }
+
+ double u = 13 * l * (up - upr);
+ double v = 13 * l * (vp - vpr);
+
+ if (double.IsNaN(u) || u == -0d)
+ {
+ u = 0;
+ }
+
+ if (double.IsNaN(v) || v == -0d)
+ {
+ v = 0;
+ }
+
+ return new CieLuv((float)l, (float)u, (float)v);
+ }
+
+ ///
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieXyz xyz = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in xyz);
+ }
+ }
+
+ ///
+ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ // Use doubles here for accuracy.
+ // Conversion algorithm described here:
+ // http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html
+ CieXyz whitePoint = options.WhitePoint;
+
+ double l = this.L, u = this.U, v = this.V;
+
+ double u0 = ComputeU(in whitePoint);
+ double v0 = ComputeV(in whitePoint);
+
+ double y = l > CieConstants.Kappa * CieConstants.Epsilon
+ ? Numerics.Pow3((l + 16) / 116d)
+ : l / CieConstants.Kappa;
+
+ double a = ((52 * l / (u + (13 * l * u0))) - 1) / 3;
+ double b = -5 * y;
+ const double c = -1 / 3d;
+ double d = y * ((39 * l / (v + (13 * l * v0))) - 5);
+
+ double x = (d - b) / (a - c);
+ double z = (x * a) + b;
+
+ if (double.IsNaN(x) || x == -0d)
+ {
+ x = 0;
+ }
+
+ if (double.IsNaN(y) || y == -0d)
+ {
+ y = 0;
+ }
+
+ if (double.IsNaN(z) || z == -0d)
+ {
+ z = 0;
+ }
+
+ return new CieXyz((float)x, (float)y, (float)z);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieLuv luv = source[i];
+ destination[i] = luv.ToProfileConnectingSpace(options);
+ }
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.WhitePoint;
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.L, this.U, this.V);
+
+ ///
+ public override string ToString() => FormattableString.Invariant($"CieLuv({this.L:#0.##}, {this.U:#0.##}, {this.V:#0.##})");
+
+ ///
+ public override bool Equals(object? obj) => obj is CieLuv other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CieLuv other)
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double ComputeU(in CieXyz source)
+ => (4 * source.X) / (source.X + (15 * source.Y) + (3 * source.Z));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double ComputeV(in CieXyz source)
+ => (9 * source.Y) / (source.X + (15 * source.Y) + (3 * source.Z));
+}
diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/CieXyChromaticityCoordinates.cs b/src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs
similarity index 88%
rename from src/ImageSharp/ColorSpaces/Conversion/Implementation/CieXyChromaticityCoordinates.cs
rename to src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs
index 2cc785d53..fa12b81d2 100644
--- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/CieXyChromaticityCoordinates.cs
+++ b/src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs
@@ -1,14 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
-// ReSharper disable CompareOfFloatsByEqualityOperator
-namespace SixLabors.ImageSharp.ColorSpaces.Conversion;
+namespace SixLabors.ImageSharp.ColorProfiles;
///
/// Represents the coordinates of CIEXY chromaticity space.
///
+[StructLayout(LayoutKind.Sequential)]
public readonly struct CieXyChromaticityCoordinates : IEquatable
{
///
@@ -29,7 +31,7 @@ public readonly struct CieXyChromaticityCoordinates : IEquatable
/// Ranges usually from 0 to 1.
///
- public readonly float X { get; }
+ public float X { get; }
///
/// Gets the chromaticity Y-coordinate
@@ -37,7 +39,7 @@ public readonly struct CieXyChromaticityCoordinates : IEquatable
/// Ranges usually from 0 to 1.
///
- public readonly float Y { get; }
+ public float Y { get; }
///
/// Compares two objects for equality.
@@ -79,5 +81,7 @@ public readonly struct CieXyChromaticityCoordinates : IEquatable
[MethodImpl(InliningOptions.ShortMethod)]
public bool Equals(CieXyChromaticityCoordinates other)
- => this.X.Equals(other.X) && this.Y.Equals(other.Y);
+ => this.AsVector2Unsafe() == other.AsVector2Unsafe();
+
+ private Vector2 AsVector2Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
}
diff --git a/src/ImageSharp/ColorSpaces/CieXyy.cs b/src/ImageSharp/ColorProfiles/CieXyy.cs
similarity index 51%
rename from src/ImageSharp/ColorSpaces/CieXyy.cs
rename to src/ImageSharp/ColorProfiles/CieXyy.cs
index 6b7d2e6cb..62873df14 100644
--- a/src/ImageSharp/ColorSpaces/CieXyy.cs
+++ b/src/ImageSharp/ColorProfiles/CieXyy.cs
@@ -3,14 +3,16 @@
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
-namespace SixLabors.ImageSharp.ColorSpaces;
+namespace SixLabors.ImageSharp.ColorProfiles;
///
/// Represents an CIE xyY 1931 color
///
///
-public readonly struct CieXyy : IEquatable
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieXyy : IColorProfile
{
///
/// Initializes a new instance of the struct.
@@ -18,7 +20,7 @@ public readonly struct CieXyy : IEquatable
/// The x chroma component.
/// The y chroma component.
/// The y luminance component.
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(float x, float y, float yl)
{
// Not clamping as documentation about this space only indicates "usual" ranges
@@ -31,7 +33,7 @@ public readonly struct CieXyy : IEquatable
/// Initializes a new instance of the struct.
///
/// The vector representing the x, y, Y components.
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(Vector3 vector)
: this()
{
@@ -45,19 +47,19 @@ public readonly struct CieXyy : IEquatable
/// Gets the X chrominance component.
/// A value usually ranging between 0 and 1.
///
- public readonly float X { get; }
+ public float X { get; }
///
/// Gets the Y chrominance component.
/// A value usually ranging between 0 and 1.
///
- public readonly float Y { get; }
+ public float Y { get; }
///
/// Gets the Y luminance component.
/// A value usually ranging between 0 and 1.
///
- public readonly float Yl { get; }
+ public float Yl { get; }
///
/// Compares two objects for equality.
@@ -67,7 +69,7 @@ public readonly struct CieXyy : IEquatable
///
/// True if the current left is equal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieXyy left, CieXyy right) => left.Equals(right);
///
@@ -78,22 +80,79 @@ public readonly struct CieXyy : IEquatable
///
/// True if the current left is unequal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right);
///
- public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Yl);
+ public static CieXyy FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ {
+ float x = source.X / (source.X + source.Y + source.Z);
+ float y = source.Y / (source.X + source.Y + source.Z);
+
+ if (float.IsNaN(x) || float.IsNaN(y))
+ {
+ return new CieXyy(0, 0, source.Y);
+ }
+
+ return new CieXyy(x, y, source.Y);
+ }
+
+ ///
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieXyz xyz = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in xyz);
+ }
+ }
///
- public override string ToString() => FormattableString.Invariant($"CieXyy({this.X:#0.##}, {this.Y:#0.##}, {this.Yl:#0.##})");
+ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ if (MathF.Abs(this.Y) < Constants.Epsilon)
+ {
+ return new CieXyz(0, 0, this.Yl);
+ }
+
+ float x = (this.X * this.Yl) / this.Y;
+ float y = this.Yl;
+ float z = ((1 - this.X - this.Y) * y) / this.Y;
+
+ return new CieXyz(x, y, z);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ for (int i = 0; i < source.Length; i++)
+ {
+ CieXyy xyz = source[i];
+ destination[i] = xyz.ToProfileConnectingSpace(options);
+ }
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.WhitePoint;
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(this.X, this.Y, this.Yl);
+
+ ///
+ public override string ToString()
+ => FormattableString.Invariant($"CieXyy({this.X:#0.##}, {this.Y:#0.##}, {this.Yl:#0.##})");
///
public override bool Equals(object? obj) => obj is CieXyy other && this.Equals(other);
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyy other)
- => this.X.Equals(other.X)
- && this.Y.Equals(other.Y)
- && this.Yl.Equals(other.Yl);
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
}
diff --git a/src/ImageSharp/ColorSpaces/CieXyz.cs b/src/ImageSharp/ColorProfiles/CieXyz.cs
similarity index 64%
rename from src/ImageSharp/ColorSpaces/CieXyz.cs
rename to src/ImageSharp/ColorProfiles/CieXyz.cs
index 2ac9c9f28..07f9b47f9 100644
--- a/src/ImageSharp/ColorSpaces/CieXyz.cs
+++ b/src/ImageSharp/ColorProfiles/CieXyz.cs
@@ -3,14 +3,16 @@
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
-namespace SixLabors.ImageSharp.ColorSpaces;
+namespace SixLabors.ImageSharp.ColorProfiles;
///
/// Represents an CIE XYZ 1931 color
///
///
-public readonly struct CieXyz : IEquatable
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct CieXyz : IProfileConnectingSpace
{
///
/// Initializes a new instance of the struct.
@@ -18,10 +20,13 @@ public readonly struct CieXyz : IEquatable
/// X is a mix (a linear combination) of cone response curves chosen to be nonnegative
/// The y luminance component.
/// Z is quasi-equal to blue stimulation, or the S cone of the human eye.
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyz(float x, float y, float z)
- : this(new Vector3(x, y, z))
{
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.X = x;
+ this.Y = y;
+ this.Z = z;
}
///
@@ -31,7 +36,6 @@ public readonly struct CieXyz : IEquatable
public CieXyz(Vector3 vector)
: this()
{
- // Not clamping as documentation about this space only indicates "usual" ranges
this.X = vector.X;
this.Y = vector.Y;
this.Z = vector.Z;
@@ -41,19 +45,19 @@ public readonly struct CieXyz : IEquatable
/// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative.
/// A value usually ranging between 0 and 1.
///
- public readonly float X { get; }
+ public float X { get; }
///
/// Gets the Y luminance component.
/// A value usually ranging between 0 and 1.
///
- public readonly float Y { get; }
+ public float Y { get; }
///
/// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response.
/// A value usually ranging between 0 and 1.
///
- public readonly float Z { get; }
+ public float Z { get; }
///
/// Compares two objects for equality.
@@ -63,7 +67,7 @@ public readonly struct CieXyz : IEquatable
///
/// True if the current left is equal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieXyz left, CieXyz right) => left.Equals(right);
///
@@ -74,16 +78,41 @@ public readonly struct CieXyz : IEquatable
///
/// True if the current left is unequal to the parameter; otherwise, false.
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyz left, CieXyz right) => !left.Equals(right);
///
/// Returns a new representing this instance.
///
/// The .
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.X, this.Y, this.Z);
+ ///
+ public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ => new(source.X, source.Y, source.Z);
+
+ ///
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ source.CopyTo(destination[..source.Length]);
+ }
+
+ ///
+ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
+ => new(this.X, this.Y, this.Z);
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ source.CopyTo(destination[..source.Length]);
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
+
///
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z);
@@ -94,9 +123,9 @@ public readonly struct CieXyz : IEquatable
public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other);
///
- [MethodImpl(InliningOptions.ShortMethod)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyz other)
- => this.X.Equals(other.X)
- && this.Y.Equals(other.Y)
- && this.Z.Equals(other.Z);
+ => this.AsVector3Unsafe() == other.AsVector3Unsafe();
+
+ private Vector3 AsVector3Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
}
diff --git a/src/ImageSharp/ColorProfiles/Cmyk.cs b/src/ImageSharp/ColorProfiles/Cmyk.cs
new file mode 100644
index 000000000..e92490449
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Cmyk.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Represents an CMYK (cyan, magenta, yellow, keyline) color.
+///
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct Cmyk : IColorProfile
+{
+ private static readonly Vector4 Min = Vector4.Zero;
+ private static readonly Vector4 Max = Vector4.One;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The cyan component.
+ /// The magenta component.
+ /// The yellow component.
+ /// The keyline black component.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Cmyk(float c, float m, float y, float k)
+ : this(new Vector4(c, m, y, k))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the c, m, y, k components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Cmyk(Vector4 vector)
+ {
+ vector = Numerics.Clamp(vector, Min, Max);
+ this.C = vector.X;
+ this.M = vector.Y;
+ this.Y = vector.Z;
+ this.K = vector.W;
+ }
+
+ ///
+ /// Gets the cyan color component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float C { get; }
+
+ ///
+ /// Gets the magenta color component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float M { get; }
+
+ ///
+ /// Gets the yellow color component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float Y { get; }
+
+ ///
+ /// Gets the keyline black color component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float K { get; }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Cmyk left, Cmyk right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Cmyk left, Cmyk right) => !left.Equals(right);
+
+ ///
+ public static Cmyk FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
+ {
+ // To CMY
+ Vector3 cmy = Vector3.One - source.ToScaledVector3();
+
+ // To CMYK
+ Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z)));
+
+ if (MathF.Abs(k.X - 1F) < Constants.Epsilon)
+ {
+ return new Cmyk(0, 0, 0, 1F);
+ }
+
+ cmy = (cmy - k) / (Vector3.One - k);
+
+ return new Cmyk(cmy.X, cmy.Y, cmy.Z, k.X);
+ }
+
+ ///
+ public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ // TODO: We can optimize this by using SIMD
+ for (int i = 0; i < source.Length; i++)
+ {
+ Rgb rgb = source[i];
+ destination[i] = FromProfileConnectingSpace(options, in rgb);
+ }
+ }
+
+ ///
+ public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
+ {
+ Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (Vector3.One - new Vector3(this.K));
+ return Rgb.FromScaledVector3(rgb);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ // TODO: We can possibly optimize this by using SIMD
+ for (int i = 0; i < source.Length; i++)
+ {
+ Cmyk cmyk = source[i];
+ destination[i] = cmyk.ToProfileConnectingSpace(options);
+ }
+ }
+
+ ///
+ public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
+ => ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode()
+ => HashCode.Combine(this.C, this.M, this.Y, this.K);
+
+ ///
+ public override string ToString()
+ => FormattableString.Invariant($"Cmyk({this.C:#0.##}, {this.M:#0.##}, {this.Y:#0.##}, {this.K:#0.##})");
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is Cmyk other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(Cmyk other)
+ => this.AsVector4Unsafe() == other.AsVector4Unsafe();
+
+ private Vector4 AsVector4Unsafe() => Unsafe.As(ref Unsafe.AsRef(in this));
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
new file mode 100644
index 000000000..1eb118834
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Provides options for color profile conversion.
+///
+public class ColorConversionOptions
+{
+ private Matrix4x4 adaptationMatrix;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorConversionOptions() => this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford;
+
+ ///
+ /// Gets the memory allocator.
+ ///
+ public MemoryAllocator MemoryAllocator { get; init; } = MemoryAllocator.Default;
+
+ ///
+ /// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space.
+ ///
+ public CieXyz WhitePoint { get; init; } = KnownIlluminants.D50;
+
+ ///
+ /// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
+ ///
+ public CieXyz TargetWhitePoint { get; init; } = KnownIlluminants.D50;
+
+ ///
+ /// Gets the source working space used for companding in conversions from/to XYZ color space.
+ ///
+ public RgbWorkingSpace RgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb;
+
+ ///
+ /// Gets the destination working space used for companding in conversions from/to XYZ color space.
+ ///
+ public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = KnownRgbWorkingSpaces.SRgb;
+
+ ///
+ /// Gets the transformation matrix used in conversion to perform chromatic adaptation.
+ /// for further information. Default is Bradford.
+ ///
+ public Matrix4x4 AdaptationMatrix
+ {
+ get => this.adaptationMatrix;
+ init
+ {
+ this.adaptationMatrix = value;
+ Matrix4x4.Invert(value, out Matrix4x4 inverted);
+ this.InverseAdaptationMatrix = inverted;
+ }
+ }
+
+ internal Matrix4x4 InverseAdaptationMatrix { get; private set; }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverter.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
new file mode 100644
index 000000000..18b90a622
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Allows the conversion of color profiles.
+///
+public class ColorProfileConverter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorProfileConverter()
+ : this(new())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The color profile conversion options.
+ public ColorProfileConverter(ColorConversionOptions options)
+ => this.Options = options;
+
+ ///
+ /// Gets the color profile conversion options.
+ ///
+ public ColorConversionOptions Options { get; }
+
+ internal (CieXyz From, CieXyz To) GetChromaticAdaptionWhitePoints()
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
+ ? this.Options.WhitePoint
+ : this.Options.RgbWorkingSpace.WhitePoint;
+
+ CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint
+ ? this.Options.TargetWhitePoint
+ : this.Options.TargetRgbWorkingSpace.WhitePoint;
+
+ return (sourceWhitePoint, targetWhitePoint);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
new file mode 100644
index 000000000..41ae4b08f
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieLabCieLab
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieLab pcsFromA = source.ToProfileConnectingSpace(options);
+ CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromTo = pcsFromToOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
+
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ CieLab.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
new file mode 100644
index 000000000..04937e927
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieLabCieXyz
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieLab pcsFrom = source.ToProfileConnectingSpace(options);
+
+ // Convert between PCS
+ CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
new file mode 100644
index 000000000..47e4d2a80
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieLabRgb
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieLab pcsFromA = source.ToProfileConnectingSpace(options);
+ CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromAOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromA = pcsFromAOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFromA);
+
+ using IMemoryOwner pcsFromBOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromB = pcsFromBOwner.GetSpan();
+ CieLab.ToProfileConnectionSpace(options, pcsFromA, pcsFromB);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ Rgb.FromProfileConnectionSpace(options, pcsFromB, pcsTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
new file mode 100644
index 000000000..6b1575d04
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieXyzCieLab
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
new file mode 100644
index 000000000..8f56a5a66
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieXyzCieXyz
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsFrom);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsFrom, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
new file mode 100644
index 000000000..9cc0bd943
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsCieXyzRgb
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFrom = VonKriesChromaticAdaptation.Transform(in pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFrom);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
new file mode 100644
index 000000000..415dd94c3
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsRgbCieLab
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ Rgb pcsFromA = source.ToProfileConnectingSpace(options);
+ CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromAOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromA = pcsFromAOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFromA);
+
+ using IMemoryOwner pcsFromBOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromB = pcsFromBOwner.GetSpan();
+ Rgb.ToProfileConnectionSpace(options, pcsFromA, pcsFromB);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFromB, pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ CieLab.FromProfileConnectionSpace(options, pcsFromB, pcsTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
new file mode 100644
index 000000000..a13f64577
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsRgbCieXyz
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ Rgb pcsFrom = source.ToProfileConnectingSpace(options);
+
+ // Convert between PCS
+ CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsTo = VonKriesChromaticAdaptation.Transform(in pcsTo, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ Rgb.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsTo, pcsTo, whitePoints, options.AdaptationMatrix);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
new file mode 100644
index 000000000..c1c75dea1
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsRgbRgb
+{
+ public static TTo Convert(this ColorProfileConverter converter, in TFrom source)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS
+ Rgb pcsFromA = source.ToProfileConnectingSpace(options);
+ CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ pcsFromB = VonKriesChromaticAdaptation.Transform(in pcsFromB, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS
+ Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, in pcsTo);
+ }
+
+ public static void Convert(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
+ where TFrom : struct, IColorProfile
+ where TTo : struct, IColorProfile
+ {
+ ColorConversionOptions options = converter.Options;
+
+ // Convert to input PCS.
+ using IMemoryOwner pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFromTo = pcsFromToOwner.GetSpan();
+ TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
+
+ using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsFrom = pcsFromOwner.GetSpan();
+ Rgb.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
+
+ // Adapt to target white point
+ (CieXyz From, CieXyz To) whitePoints = converter.GetChromaticAdaptionWhitePoints();
+ VonKriesChromaticAdaptation.Transform(pcsFrom, pcsFrom, whitePoints, options.AdaptationMatrix);
+
+ // Convert between PCS.
+ Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs
new file mode 100644
index 000000000..1970e2d94
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Collections.Concurrent;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+
+namespace SixLabors.ImageSharp.ColorProfiles.Companding;
+
+///
+/// Companding utilities that allow the accelerated compression-expansion of color channels.
+///
+public static class CompandingUtilities
+{
+ private const int Length = Scale + 2; // 256kb @ 16bit precision.
+ private const int Scale = (1 << 16) - 1;
+ private static readonly ConcurrentDictionary<(Type, double), float[]> CompressLookupTables = new();
+ private static readonly ConcurrentDictionary<(Type, double), float[]> ExpandLookupTables = new();
+
+ ///
+ /// Lazily creates and stores a companding compression lookup table using the given function and modifier.
+ ///
+ /// The type of companding function.
+ /// The companding function.
+ /// A modifier to pass to the function.
+ /// The array.
+ public static float[] GetCompressLookupTable(Func compandingFunction, double modifier = 0)
+ => CompressLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2));
+
+ ///
+ /// Lazily creates and stores a companding expanding lookup table using the given function and modifier.
+ ///
+ /// The type of companding function.
+ /// The companding function.
+ /// A modifier to pass to the function.
+ /// The array.
+ public static float[] GetExpandLookupTable(Func compandingFunction, double modifier = 0)
+ => ExpandLookupTables.GetOrAdd((typeof(T), modifier), args => CreateLookupTableImpl(compandingFunction, args.Item2));
+
+ ///
+ /// Creates a companding lookup table using the given function.
+ ///
+ /// The companding function.
+ /// A modifier to pass to the function.
+ /// The array.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float[] CreateLookupTableImpl(Func compandingFunction, double modifier = 0)
+ {
+ float[] result = new float[Length];
+
+ for (int i = 0; i < result.Length; i++)
+ {
+ double d = (double)i / Scale;
+ d = compandingFunction(d, modifier);
+ result[i] = (float)d;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Performs the companding operation on the given vectors using the given table.
+ ///
+ /// The span of vectors.
+ /// The lookup table.
+ public static void Compand(Span vectors, float[] table)
+ {
+ DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
+
+ if (Avx2.IsSupported && vectors.Length >= 2)
+ {
+ CompandAvx2(vectors, table);
+
+ if (Numerics.Modulo2(vectors.Length) != 0)
+ {
+ // Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
+ ref Vector4 last = ref MemoryMarshal.GetReference(vectors[^1..]);
+ last = Compand(last, table);
+ }
+ }
+ else
+ {
+ CompandScalar(vectors, table);
+ }
+ }
+
+ ///
+ /// Performs the companding operation on the given vector using the given table.
+ ///
+ /// The vector.
+ /// The lookup table.
+ /// The
+ public static Vector4 Compand(Vector4 vector, float[] table)
+ {
+ DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
+
+ Vector4 zero = Vector4.Zero;
+ Vector4 scale = new(Scale);
+
+ Vector4 multiplied = Numerics.Clamp(vector * Scale, zero, scale);
+
+ float f0 = multiplied.X;
+ float f1 = multiplied.Y;
+ float f2 = multiplied.Z;
+
+ uint i0 = (uint)f0;
+ uint i1 = (uint)f1;
+ uint i2 = (uint)f2;
+
+ // Alpha is already a linear representation of opacity so we do not want to convert it.
+ vector.X = Numerics.Lerp(table[i0], table[i0 + 1], f0 - (int)i0);
+ vector.Y = Numerics.Lerp(table[i1], table[i1 + 1], f1 - (int)i1);
+ vector.Z = Numerics.Lerp(table[i2], table[i2 + 1], f2 - (int)i2);
+
+ return vector;
+ }
+
+ private static unsafe void CompandAvx2(Span vectors, float[] table)
+ {
+ fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
+ {
+ Vector256 scale = Vector256.Create((float)Scale);
+ Vector256 zero = Vector256.Zero;
+ Vector256 offset = Vector256.Create(1);
+
+ // Divide by 2 as 4 elements per Vector4 and 8 per Vector256
+ ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
+
+ while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
+ {
+ Vector256 multiplied = Avx.Multiply(scale, vectorsBase);
+ multiplied = Avx.Min(Avx.Max(zero, multiplied), scale);
+
+ Vector256 truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied);
+ Vector256 truncatedF = Avx.ConvertToVector256Single(truncated);
+
+ Vector256 low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float));
+ Vector256 high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float));
+
+ // Alpha is already a linear representation of opacity so we do not want to convert it.
+ Vector256 companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF));
+ vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl);
+ vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
+ }
+ }
+ }
+
+ private static unsafe void CompandScalar(Span vectors, float[] table)
+ {
+ fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
+ {
+ Vector4 zero = Vector4.Zero;
+ Vector4 scale = new(Scale);
+ ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors);
+ ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
+
+ while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
+ {
+ Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale);
+
+ float f0 = multiplied.X;
+ float f1 = multiplied.Y;
+ float f2 = multiplied.Z;
+
+ uint i0 = (uint)f0;
+ uint i1 = (uint)f1;
+ uint i2 = (uint)f2;
+
+ // Alpha is already a linear representation of opacity so we do not want to convert it.
+ vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0);
+ vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1);
+ vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2);
+
+ vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs b/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs
new file mode 100644
index 000000000..34ca8bf5e
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.ColorProfiles.Companding;
+
+///
+/// Implements gamma companding.
+///
+///
+///
+///
+///
+public static class GammaCompanding
+{
+ private static Func CompressFunction => (d, m) => Math.Pow(d, 1 / m);
+
+ private static Func ExpandFunction => Math.Pow;
+
+ ///
+ /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ /// The gamma value.
+ public static void Compress(Span vectors, double gamma)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction, gamma));
+
+ ///
+ /// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ /// The gamma value.
+ public static void Expand(Span vectors, double gamma)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction, gamma));
+
+ ///
+ /// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The gamma value.
+ /// The .
+ public static Vector4 Compress(Vector4 vector, double gamma)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction, gamma));
+
+ ///
+ /// Expands the nonlinear vector to its linear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The gamma value.
+ /// The .
+ public static Vector4 Expand(Vector4 vector, double gamma)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction, gamma));
+
+ private class GammaCompandingKey;
+}
diff --git a/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs b/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs
new file mode 100644
index 000000000..4f5383038
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.ColorProfiles.Companding;
+
+///
+/// Implements L* companding.
+///
+///
+/// For more info see:
+///
+///
+///
+public static class LCompanding
+{
+ private static Func CompressFunction
+ => (d, _) =>
+ {
+ if (d <= CieConstants.Epsilon)
+ {
+ return (d * CieConstants.Kappa) / 100;
+ }
+
+ return (1.16 * Math.Pow(d, 0.3333333)) - 0.16;
+ };
+
+ private static Func ExpandFunction
+ => (d, _) =>
+ {
+ if (d <= 0.08)
+ {
+ return (100 * d) / CieConstants.Kappa;
+ }
+
+ return Numerics.Pow3(((float)(d + 0.16f)) / 1.16f);
+ };
+
+ ///
+ /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Compress(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction));
+
+ ///
+ /// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Expand(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction));
+
+ ///
+ /// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The .
+ public static Vector4 Compress(Vector4 vector)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction));
+
+ ///
+ /// Expands the nonlinear vector to its linear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The .
+ public static Vector4 Expand(Vector4 vector)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction));
+
+ private class LCompandingKey;
+}
diff --git a/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs b/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs
new file mode 100644
index 000000000..1901e6471
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.ColorProfiles.Companding;
+
+///
+/// Implements Rec. 2020 companding function.
+///
+///
+///
+///
+public static class Rec2020Companding
+{
+ private const double Alpha = 1.09929682680944;
+ private const double AlphaMinusOne = Alpha - 1;
+ private const double Beta = 0.018053968510807;
+ private const double InverseBeta = Beta * 4.5;
+ private const double Epsilon = 1 / 0.45;
+
+ private static Func CompressFunction
+ => (d, _) =>
+ {
+ if (d < Beta)
+ {
+ return 4.5 * d;
+ }
+
+ return (Alpha * Math.Pow(d, 0.45)) - AlphaMinusOne;
+ };
+
+ private static Func ExpandFunction
+ => (d, _) =>
+ {
+ if (d < InverseBeta)
+ {
+ return d / 4.5;
+ }
+
+ return Math.Pow((d + AlphaMinusOne) / Alpha, Epsilon);
+ };
+
+ ///
+ /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Compress(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction));
+
+ ///
+ /// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Expand(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable(ExpandFunction));
+
+ ///
+ /// Compresses the linear vector to its nonlinear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The .
+ public static Vector4 Compress(Vector4 vector)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetCompressLookupTable(CompressFunction));
+
+ ///
+ /// Expands the nonlinear vector to its linear equivalent with respect to the energy.
+ ///
+ /// The vector.
+ /// The .
+ public static Vector4 Expand(Vector4 vector)
+ => CompandingUtilities.Compand(vector, CompandingUtilities.GetExpandLookupTable(ExpandFunction));
+
+ private class Rec2020CompandingKey;
+}
diff --git a/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs b/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs
new file mode 100644
index 000000000..94b17d8d0
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.ColorProfiles.Companding;
+
+///
+/// Implements the Rec. 709 companding function.
+///
+///
+/// http://en.wikipedia.org/wiki/Rec._709
+///
+public static class Rec709Companding
+{
+ private const double Epsilon = 1 / 0.45;
+
+ private static Func CompressFunction
+ => (d, _) =>
+ {
+ if (d < 0.018)
+ {
+ return 4.5 * d;
+ }
+
+ return (1.099 * Math.Pow(d, 0.45)) - 0.099;
+ };
+
+ private static Func ExpandFunction
+ => (d, _) =>
+ {
+ if (d < 0.081)
+ {
+ return d / 4.5;
+ }
+
+ return Math.Pow((d + 0.099) / 1.099, Epsilon);
+ };
+
+ ///
+ /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Compress(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetCompressLookupTable(CompressFunction));
+
+ ///
+ /// Expands the nonlinear vectors to their linear equivalents with respect to the energy.
+ ///
+ /// The span of vectors.
+ public static void Expand(Span vectors)
+ => CompandingUtilities.Compand(vectors, CompandingUtilities.GetExpandLookupTable