diff --git a/src/ImageSharp/ColorProfiles/CieXyy.cs b/src/ImageSharp/ColorProfiles/CieXyy.cs
new file mode 100644
index 0000000000..9a1953ac84
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieXyy.cs
@@ -0,0 +1,156 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Represents an CIE xyY 1931 color
+///
+///
+public readonly struct CieXyy : IColorProfile
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The x chroma component.
+ /// The y chroma component.
+ /// The y luminance component.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public CieXyy(float x, float y, float yl)
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.X = x;
+ this.Y = y;
+ this.Yl = yl;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the x, y, Y components.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public CieXyy(Vector3 vector)
+ : this()
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.X = vector.X;
+ this.Y = vector.Y;
+ this.Yl = vector.Z;
+ }
+
+ ///
+ /// Gets the X chrominance component.
+ /// A value usually ranging between 0 and 1.
+ ///
+ public readonly float X { get; }
+
+ ///
+ /// Gets the Y chrominance component.
+ /// A value usually ranging between 0 and 1.
+ ///
+ public readonly float Y { get; }
+
+ ///
+ /// Gets the Y luminance component.
+ /// A value usually ranging between 0 and 1.
+ ///
+ public readonly float Yl { 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(InliningOptions.ShortMethod)]
+ public static bool operator ==(CieXyy left, CieXyy 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(InliningOptions.ShortMethod)]
+ public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right);
+
+ ///
+ 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 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)]
+ public bool Equals(CieXyy other)
+ => this.X.Equals(other.X)
+ && this.Y.Equals(other.Y)
+ && this.Yl.Equals(other.Yl);
+}
diff --git a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
index f2c1d20a37..81aac269d4 100644
--- a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
+++ b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
@@ -17,7 +17,8 @@ internal readonly struct ApproximateColorProfileComparer :
IEqualityComparer,
IEqualityComparer,
IEqualityComparer,
- IEqualityComparer
+ IEqualityComparer,
+ IEqualityComparer
{
private readonly float epsilon;
@@ -43,6 +44,8 @@ internal readonly struct ApproximateColorProfileComparer :
public bool Equals(CieLuv x, CieLuv y) => this.Equals(x.L, y.L) && this.Equals(x.U, y.U) && this.Equals(x.V, y.V);
+ public bool Equals(CieXyy x, CieXyy y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Yl, y.Yl);
+
public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
@@ -59,6 +62,8 @@ internal readonly struct ApproximateColorProfileComparer :
public int GetHashCode([DisallowNull] CieLuv obj) => obj.GetHashCode();
+ public int GetHashCode([DisallowNull] CieXyy obj) => obj.GetHashCode();
+
private bool Equals(float x, float y)
{
float d = x - y;
diff --git a/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieXyyConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieXyyConversionTest.cs
new file mode 100644
index 0000000000..c4fa554a62
--- /dev/null
+++ b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieXyyConversionTest.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion;
+
+///
+/// Tests - conversions.
+///
+///
+/// Test data generated using:
+///
+///
+public class CieXyzAndCieXyyConversionTest
+{
+ private static readonly ApproximateColorProfileComparer Comparer = new(.0001F);
+
+ [Theory]
+ [InlineData(0.436075, 0.222504, 0.013932, 0.648427, 0.330856, 0.222504)]
+ [InlineData(0.964220, 1.000000, 0.825210, 0.345669, 0.358496, 1.000000)]
+ [InlineData(0.434119, 0.356820, 0.369447, 0.374116, 0.307501, 0.356820)]
+ [InlineData(0, 0, 0, 0.538842, 0.000000, 0.000000)]
+ public void Convert_Xyy_To_Xyz(float xyzX, float xyzY, float xyzZ, float x, float y, float yl)
+ {
+ CieXyy input = new(x, y, yl);
+ CieXyz expected = new(xyzX, xyzY, xyzZ);
+ ColorProfileConverter converter = new();
+
+ Span inputSpan = new CieXyy[5];
+ inputSpan.Fill(input);
+
+ Span actualSpan = new CieXyz[5];
+
+ // Act
+ CieXyz actual = converter.Convert(input);
+ converter.Convert(inputSpan, actualSpan);
+
+ // Assert
+ Assert.Equal(expected, actual, Comparer);
+
+ for (int i = 0; i < actualSpan.Length; i++)
+ {
+ Assert.Equal(expected, actualSpan[i], Comparer);
+ }
+ }
+
+ [Theory]
+ [InlineData(0.436075, 0.222504, 0.013932, 0.648427, 0.330856, 0.222504)]
+ [InlineData(0.964220, 1.000000, 0.825210, 0.345669, 0.358496, 1.000000)]
+ [InlineData(0.434119, 0.356820, 0.369447, 0.374116, 0.307501, 0.356820)]
+ [InlineData(0.231809, 0, 0.077528, 0.749374, 0.000000, 0.000000)]
+ public void Convert_Xyz_to_Xyy(float xyzX, float xyzY, float xyzZ, float x, float y, float yl)
+ {
+ CieXyz input = new(xyzX, xyzY, xyzZ);
+ CieXyy expected = new(x, y, yl);
+ ColorProfileConverter converter = new();
+
+ Span inputSpan = new CieXyz[5];
+ inputSpan.Fill(input);
+
+ Span actualSpan = new CieXyy[5];
+
+ // Act
+ CieXyy actual = converter.Convert(input);
+ converter.Convert(inputSpan, actualSpan);
+
+ // Assert
+ Assert.Equal(expected, actual, Comparer);
+
+ for (int i = 0; i < actualSpan.Length; i++)
+ {
+ Assert.Equal(expected, actualSpan[i], Comparer);
+ }
+ }
+}