diff --git a/src/ImageSharp/ColorProfiles/CieConstants.cs b/src/ImageSharp/ColorProfiles/CieConstants.cs
new file mode 100644
index 0000000000..f4b74eaa1d
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieConstants.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Constants use for Cie conversion calculations
+///
+///
+internal static class CieConstants
+{
+ ///
+ /// 216F / 24389F
+ ///
+ public const float Epsilon = 0.008856452F;
+
+ ///
+ /// 24389F / 27F
+ ///
+ public const float Kappa = 903.2963F;
+}
diff --git a/src/ImageSharp/ColorProfiles/CieLab.cs b/src/ImageSharp/ColorProfiles/CieLab.cs
new file mode 100644
index 0000000000..c06fe765c2
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieLab.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Represents a CIE L*a*b* 1976 color.
+///
+///
+public readonly struct CieLab : IProfileConnectingSpace
+{
+ ///
+ /// D50 standard illuminant.
+ /// Used when reference white is not specified explicitly.
+ ///
+ public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
+
+ ///
+ /// 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)
+ : this(new Vector3(l, a, b))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the l, a, b components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public CieLab(Vector3 vector)
+ : this()
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ 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 readonly float L { get; }
+
+ ///
+ /// Gets the a color component.
+ /// A value usually ranging from -100 to 100. Negative is green, positive magenta.
+ ///
+ public readonly float A { get; }
+
+ ///
+ /// Gets the b color component.
+ /// A value usually ranging from -100 to 100. Negative is blue, positive is yellow
+ ///
+ public readonly 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);
+
+ ///
+ 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.L.Equals(other.L)
+ && this.A.Equals(other.A)
+ && this.B.Equals(other.B);
+
+ ///
+ [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);
+
+ // Avoids XYZ coordinates out range (restricted by 0 and XYZ reference white)
+ Vector3 xyzr = Vector3.Clamp(new Vector3(xr, yr, zr), Vector3.Zero, Vector3.One);
+
+ 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);
+ }
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/CieXyz.cs b/src/ImageSharp/ColorProfiles/CieXyz.cs
new file mode 100644
index 0000000000..b22ab16f92
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/CieXyz.cs
@@ -0,0 +1,124 @@
+// 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 XYZ 1931 color
+///
+///
+public readonly struct CieXyz : IProfileConnectingSpace
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// 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(MethodImplOptions.AggressiveInlining)]
+ public CieXyz(float x, float y, float z)
+ : this(new Vector3(x, y, z))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the x, y, z components.
+ 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;
+ }
+
+ ///
+ /// 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 float X { get; }
+
+ ///
+ /// Gets the Y luminance component.
+ /// A value usually ranging between 0 and 1.
+ ///
+ 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 float Z { 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 ==(CieXyz left, CieXyz 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 !=(CieXyz left, CieXyz right) => !left.Equals(right);
+
+ ///
+ /// Returns a new representing this instance.
+ ///
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 ToVector3() => new(this.X, this.Y, this.Z);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z);
+
+ ///
+ public override string ToString() => FormattableString.Invariant($"CieXyz({this.X:#0.##}, {this.Y:#0.##}, {this.Z:#0.##})");
+
+ ///
+ public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CieXyz other)
+ => this.X.Equals(other.X)
+ && this.Y.Equals(other.Y)
+ && this.Z.Equals(other.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]);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
new file mode 100644
index 0000000000..de32aa54d3
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+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 = LmsAdaptationMatrix.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; } = Illuminants.D50;
+
+ ///
+ /// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
+ ///
+ public CieXyz TargetWhitePoint { get; init; } = Illuminants.D50;
+
+ ///
+ /// Gets the transformation matrix used in conversion to perform chromatic adaptation.
+ ///
+ 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 0000000000..af9ab0af82
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
@@ -0,0 +1,30 @@
+// 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; }
+}
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
new file mode 100644
index 0000000000..481280b85e
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
@@ -0,0 +1,52 @@
+// 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, 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
+ VonKriesChromaticAdaptation.Transform(options, in pcsTo);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, 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 * 2);
+ Span pcsTo = pcsToOwner.GetSpan()[..source.Length];
+ CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Adapt to target white point
+ VonKriesChromaticAdaptation.Transform(options, pcsTo, 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 0000000000..ebe07003e9
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
@@ -0,0 +1,52 @@
+// 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, 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
+ VonKriesChromaticAdaptation.Transform(options, in pcsFrom);
+
+ // Convert between PCS
+ CieXyz pcsTo = CieXyz.FromProfileConnectingSpace(options, in pcsFrom);
+
+ // Convert to output from PCS
+ return TTo.FromProfileConnectingSpace(options, 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
+ VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
+
+ // Convert between PCS.
+ using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length);
+ Span pcsTo = pcsToOwner.GetSpan();
+ CieXyz.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
+
+ // Convert to output from PCS
+ TTo.FromProfileConnectionSpace(options, pcsTo, destination);
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs b/src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs
new file mode 100644
index 0000000000..e1d4cae355
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Defines the contract for all color profiles.
+///
+/// The type of color profile.
+/// The type of color profile connecting space.
+public interface IColorProfile : IEquatable
+ where TSelf : IColorProfile
+ where TProfileSpace : struct, IProfileConnectingSpace
+{
+ ///
+ /// Converts the color to the profile connection space.
+ ///
+ /// The color profile conversion options.
+ /// The .
+ public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options);
+
+#pragma warning disable CA1000 // Do not declare static members on generic types
+ ///
+ /// Converts the color from the profile connection space.
+ ///
+ /// The color profile conversion options.
+ /// The color profile connecting space.
+ /// The .
+ public static abstract TSelf FromProfileConnectingSpace(ColorConversionOptions options, in TProfileSpace source);
+
+ ///
+ /// Converts the span of colors to the profile connection space.
+ ///
+ /// The color profile conversion options.
+ /// The color span to convert from.
+ /// The color profile span to write the results to.
+ public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination);
+
+ ///
+ /// Converts the span of colors from the profile connection space.
+ ///
+ /// The color profile conversion options.
+ /// The color profile span to convert from.
+ /// The color span to write the results to.
+ public static abstract void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination);
+#pragma warning restore CA1000 // Do not declare static members on generic types
+}
diff --git a/src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs b/src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs
new file mode 100644
index 0000000000..2ac736f444
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Defines the contract for all color profile connection spaces.
+///
+public interface IProfileConnectingSpace;
+
+///
+/// Defines the contract for all color profile connection spaces.
+///
+/// The type of color profile.
+/// The type of color profile connecting space.
+public interface IProfileConnectingSpace : IColorProfile, IProfileConnectingSpace
+ where TSelf : struct, IColorProfile, IProfileConnectingSpace
+ where TProfileSpace : struct, IProfileConnectingSpace;
diff --git a/src/ImageSharp/ColorProfiles/Illuminants.cs b/src/ImageSharp/ColorProfiles/Illuminants.cs
new file mode 100644
index 0000000000..0295dc484e
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Illuminants.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// The well known standard illuminants.
+/// Standard illuminants provide a basis for comparing images or colors recorded under different lighting
+///
+///
+/// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
+///
+/// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant
+///
+public static class Illuminants
+{
+ ///
+ /// Incandescent / Tungsten
+ ///
+ public static readonly CieXyz A = new(1.09850F, 1F, 0.35585F);
+
+ ///
+ /// Direct sunlight at noon (obsoleteF)
+ ///
+ public static readonly CieXyz B = new(0.99072F, 1F, 0.85223F);
+
+ ///
+ /// Average / North sky Daylight (obsoleteF)
+ ///
+ public static readonly CieXyz C = new(0.98074F, 1F, 1.18232F);
+
+ ///
+ /// Horizon Light. ICC profile PCS
+ ///
+ public static readonly CieXyz D50 = new(0.96422F, 1F, 0.82521F);
+
+ ///
+ /// Mid-morning / Mid-afternoon Daylight
+ ///
+ public static readonly CieXyz D55 = new(0.95682F, 1F, 0.92149F);
+
+ ///
+ /// Noon Daylight: TelevisionF, sRGB color space
+ ///
+ public static readonly CieXyz D65 = new(0.95047F, 1F, 1.08883F);
+
+ ///
+ /// North sky Daylight
+ ///
+ public static readonly CieXyz D75 = new(0.94972F, 1F, 1.22638F);
+
+ ///
+ /// Equal energy
+ ///
+ public static readonly CieXyz E = new(1F, 1F, 1F);
+
+ ///
+ /// Cool White Fluorescent
+ ///
+ public static readonly CieXyz F2 = new(0.99186F, 1F, 0.67393F);
+
+ ///
+ /// D65 simulatorF, Daylight simulator
+ ///
+ public static readonly CieXyz F7 = new(0.95041F, 1F, 1.08747F);
+
+ ///
+ /// Philips TL84F, Ultralume 40
+ ///
+ public static readonly CieXyz F11 = new(1.00962F, 1F, 0.64350F);
+}
diff --git a/src/ImageSharp/ColorProfiles/Lms.cs b/src/ImageSharp/ColorProfiles/Lms.cs
new file mode 100644
index 0000000000..e11c1ca87a
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/Lms.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal readonly struct Lms : IColorProfile
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// L represents the responsivity at long wavelengths.
+ /// M represents the responsivity at medium wavelengths.
+ /// S represents the responsivity at short wavelengths.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Lms(float l, float m, float s)
+ : this(new Vector3(l, m, s))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vector representing the l, m, s components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Lms(Vector3 vector)
+ {
+ // Not clamping as documentation about this space only indicates "usual" ranges
+ this.L = vector.X;
+ this.M = vector.Y;
+ this.S = vector.Z;
+ }
+
+ ///
+ /// Gets the L long component.
+ /// A value usually ranging between -1 and 1.
+ ///
+ public readonly float L { get; }
+
+ ///
+ /// Gets the M medium component.
+ /// A value usually ranging between -1 and 1.
+ ///
+ public readonly float M { get; }
+
+ ///
+ /// Gets the S short component.
+ /// A value usually ranging between -1 and 1.
+ ///
+ public readonly float S { 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 ==(Lms left, Lms 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 !=(Lms left, Lms right) => !left.Equals(right);
+
+ ///
+ /// Returns a new representing this instance.
+ ///
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 ToVector3() => new(this.L, this.M, this.S);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.L, this.M, this.S);
+
+ ///
+ public override string ToString() => FormattableString.Invariant($"Lms({this.L:#0.##}, {this.M:#0.##}, {this.S:#0.##})");
+
+ ///
+ public override bool Equals(object? obj) => obj is Lms other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(Lms other)
+ => this.L.Equals(other.L)
+ && this.M.Equals(other.M)
+ && this.S.Equals(other.S);
+
+ ///
+ public static Lms FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
+ {
+ Vector3 vector = Vector3.Transform(source.ToVector3(), options.AdaptationMatrix);
+ return new Lms(vector);
+ }
+
+ ///
+ 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)
+ {
+ Vector3 vector = Vector3.Transform(this.ToVector3(), options.InverseAdaptationMatrix);
+ return new CieXyz(vector);
+ }
+
+ ///
+ public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ Lms lms = source[i];
+ destination[i] = lms.ToProfileConnectingSpace(options);
+ }
+ }
+}
diff --git a/src/ImageSharp/ColorProfiles/LmsAdaptationMatrix.cs b/src/ImageSharp/ColorProfiles/LmsAdaptationMatrix.cs
new file mode 100644
index 0000000000..70bc5aef26
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/LmsAdaptationMatrix.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Matrices used for transformation from to , defining the cone response domain.
+///
+///
+/// Matrix data obtained from:
+/// Two New von Kries Based Chromatic Adaptation Transforms Found by Numerical Optimization
+/// S. Bianco, R. Schettini
+/// DISCo, Department of Informatics, Systems and Communication, University of Milan-Bicocca, viale Sarca 336, 20126 Milan, Italy
+/// https://web.stanford.edu/~sujason/ColorBalancing/Papers/Two%20New%20von%20Kries%20Based%20Chromatic%20Adaptation.pdf
+///
+public static class LmsAdaptationMatrix
+{
+ ///
+ /// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65)
+ ///
+ public static readonly Matrix4x4 VonKriesHPEAdjusted
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 0.40024F,
+ M12 = 0.7076F,
+ M13 = -0.08081F,
+ M21 = -0.2263F,
+ M22 = 1.16532F,
+ M23 = 0.0457F,
+ M31 = 0,
+ M32 = 0,
+ M33 = 0.91822F,
+ M44 = 1F // Important for inverse transforms.
+ });
+
+ ///
+ /// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy)
+ ///
+ public static readonly Matrix4x4 VonKriesHPE
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 0.3897F,
+ M12 = 0.6890F,
+ M13 = -0.0787F,
+ M21 = -0.2298F,
+ M22 = 1.1834F,
+ M23 = 0.0464F,
+ M31 = 0,
+ M32 = 0,
+ M33 = 1F,
+ M44 = 1F
+ });
+
+ ///
+ /// XYZ scaling chromatic adaptation transform matrix
+ ///
+ public static readonly Matrix4x4 XyzScaling = Matrix4x4.Transpose(Matrix4x4.Identity);
+
+ ///
+ /// Bradford chromatic adaptation transform matrix (used in CMCCAT97)
+ ///
+ public static readonly Matrix4x4 Bradford
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 0.8951F,
+ M12 = 0.2664F,
+ M13 = -0.1614F,
+ M21 = -0.7502F,
+ M22 = 1.7135F,
+ M23 = 0.0367F,
+ M31 = 0.0389F,
+ M32 = -0.0685F,
+ M33 = 1.0296F,
+ M44 = 1F
+ });
+
+ ///
+ /// Spectral sharpening and the Bradford transform
+ ///
+ public static readonly Matrix4x4 BradfordSharp
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 1.2694F,
+ M12 = -0.0988F,
+ M13 = -0.1706F,
+ M21 = -0.8364F,
+ M22 = 1.8006F,
+ M23 = 0.0357F,
+ M31 = 0.0297F,
+ M32 = -0.0315F,
+ M33 = 1.0018F,
+ M44 = 1F
+ });
+
+ ///
+ /// CMCCAT2000 (fitted from all available color data sets)
+ ///
+ public static readonly Matrix4x4 CMCCAT2000
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 0.7982F,
+ M12 = 0.3389F,
+ M13 = -0.1371F,
+ M21 = -0.5918F,
+ M22 = 1.5512F,
+ M23 = 0.0406F,
+ M31 = 0.0008F,
+ M32 = 0.239F,
+ M33 = 0.9753F,
+ M44 = 1F
+ });
+
+ ///
+ /// CAT02 (optimized for minimizing CIELAB differences)
+ ///
+ public static readonly Matrix4x4 CAT02
+ = Matrix4x4.Transpose(new Matrix4x4
+ {
+ M11 = 0.7328F,
+ M12 = 0.4296F,
+ M13 = -0.1624F,
+ M21 = -0.7036F,
+ M22 = 1.6975F,
+ M23 = 0.0061F,
+ M31 = 0.0030F,
+ M32 = 0.0136F,
+ M33 = 0.9834F,
+ M44 = 1F
+ });
+}
diff --git a/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs b/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
new file mode 100644
index 0000000000..0505395deb
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
@@ -0,0 +1,90 @@
+// 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;
+
+///
+/// Implementation of the Von Kries chromatic adaptation model.
+///
+///
+/// Transformation described here:
+/// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
+///
+public static class VonKriesChromaticAdaptation
+{
+ ///
+ /// Performs a linear transformation of a source color in to the destination color.
+ ///
+ /// Doesn't crop the resulting color space coordinates (e.g. allows negative values for XYZ coordinates).
+ /// The color profile conversion options.
+ /// The source color.
+ /// The
+ public static CieXyz Transform(ColorConversionOptions options, in CieXyz source)
+ {
+ CieXyz sourceWhitePoint = options.WhitePoint;
+ CieXyz destinationWhitePoint = options.TargetWhitePoint;
+
+ if (sourceWhitePoint.Equals(destinationWhitePoint))
+ {
+ return new(source.X, source.Y, source.Z);
+ }
+
+ Matrix4x4 matrix = options.AdaptationMatrix;
+
+ Vector3 sourceColorLms = Vector3.Transform(source.ToVector3(), matrix);
+ Vector3 sourceWhitePointLms = Vector3.Transform(sourceWhitePoint.ToVector3(), matrix);
+ Vector3 targetWhitePointLms = Vector3.Transform(destinationWhitePoint.ToVector3(), matrix);
+
+ Vector3 vector = targetWhitePointLms / sourceWhitePointLms;
+ Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms);
+
+ Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix);
+ return new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix));
+ }
+
+ ///
+ /// Performs a bulk linear transformation of a source color in to the destination color.
+ ///
+ /// Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates).
+ /// The color profile conversion options.
+ /// The span to the source colors.
+ /// The span to the destination colors.
+ public static void Transform(ColorConversionOptions options, ReadOnlySpan source, Span destination)
+ {
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+ int count = source.Length;
+
+ CieXyz sourceWhitePoint = options.WhitePoint;
+ CieXyz destinationWhitePoint = options.TargetWhitePoint;
+
+ if (sourceWhitePoint.Equals(destinationWhitePoint))
+ {
+ source.CopyTo(destination[..count]);
+ return;
+ }
+
+ Matrix4x4 matrix = options.AdaptationMatrix;
+ Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix);
+
+ ref CieXyz sourceBase = ref MemoryMarshal.GetReference(source);
+ ref CieXyz destinationBase = ref MemoryMarshal.GetReference(destination);
+
+ for (nuint i = 0; i < (uint)count; i++)
+ {
+ ref CieXyz sp = ref Unsafe.Add(ref sourceBase, i);
+ ref CieXyz dp = ref Unsafe.Add(ref destinationBase, i);
+
+ Vector3 sourceColorLms = Vector3.Transform(sp.ToVector3(), matrix);
+ Vector3 sourceWhitePointLms = Vector3.Transform(sourceWhitePoint.ToVector3(), matrix);
+ Vector3 targetWhitePointLms = Vector3.Transform(destinationWhitePoint.ToVector3(), matrix);
+
+ Vector3 vector = targetWhitePointLms / sourceWhitePointLms;
+ Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms);
+ dp = new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix));
+ }
+ }
+}
diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs
index 97e9cee813..8f33e59f5a 100644
--- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs
+++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.ColorSpaces.Conversion;
///
-/// Implementation of the von Kries chromatic adaptation model.
+/// Implementation of the Von Kries chromatic adaptation model.
///
///
/// Transformation described here:
diff --git a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
new file mode 100644
index 0000000000..74fa132163
--- /dev/null
+++ b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Tests.ColorProfiles;
+
+///
+/// Allows the approximate comparison of color profile component values.
+///
+internal readonly struct ApproximateColorProfileComparer :
+ IEqualityComparer,
+ IEqualityComparer,
+ IEqualityComparer
+{
+ private readonly float epsilon;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The comparison error difference epsilon to use.
+ public ApproximateColorProfileComparer(float epsilon = 1f) => this.epsilon = epsilon;
+
+ public bool Equals(CieLab x, CieLab y) => this.Equals(x.L, y.L) && this.Equals(x.A, y.A) && this.Equals(x.B, y.B);
+
+ public bool Equals(CieXyz x, CieXyz y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z);
+
+ public bool Equals(Lms x, Lms y) => this.Equals(x.L, y.L) && this.Equals(x.M, y.M) && this.Equals(x.S, y.S);
+
+ public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
+
+ public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
+
+ public int GetHashCode([DisallowNull] Lms obj) => obj.GetHashCode();
+
+ private bool Equals(float x, float y)
+ {
+ float d = x - y;
+ return d >= -this.epsilon && d <= this.epsilon;
+ }
+}
diff --git a/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieLabConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieLabConversionTest.cs
new file mode 100644
index 0000000000..da2bcbc876
--- /dev/null
+++ b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieLabConversionTest.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Tests.ColorProfiles;
+
+///
+/// Tests - conversions.
+///
+///
+/// Test data generated using:
+///
+///
+public class CieXyzAndCieLabConversionTest
+{
+ private static readonly ApproximateColorProfileComparer Comparer = new(.0001f);
+
+ [Theory]
+ [InlineData(100, 0, 0, 0.95047, 1, 1.08883)]
+ [InlineData(0, 0, 0, 0, 0, 0)]
+ [InlineData(0, 431.0345, 0, 0.95047, 0, 0)]
+ [InlineData(100, -431.0345, 172.4138, 0, 1, 0)]
+ [InlineData(0, 0, -172.4138, 0, 0, 1.08883)]
+ [InlineData(45.6398, 39.8753, 35.2091, 0.216938, 0.150041, 0.048850)]
+ [InlineData(77.1234, -40.1235, 78.1120, 0.358530, 0.517372, 0.076273)]
+ [InlineData(10, -400, 20, 0, 0.011260, 0)]
+ public void Convert_Lab_to_Xyz(float l, float a, float b, float x, float y, float z)
+ {
+ // Arrange
+ CieLab input = new(l, a, b);
+ ColorConversionOptions options = new() { WhitePoint = Illuminants.D65, TargetWhitePoint = Illuminants.D65 };
+ ColorProfileConverter converter = new(options);
+ CieXyz expected = new(x, y, z);
+
+ Span inputSpan = new CieLab[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.95047, 1, 1.08883, 100, 0, 0)]
+ [InlineData(0, 0, 0, 0, 0, 0)]
+ [InlineData(0.95047, 0, 0, 0, 431.0345, 0)]
+ [InlineData(0, 1, 0, 100, -431.0345, 172.4138)]
+ [InlineData(0, 0, 1.08883, 0, 0, -172.4138)]
+ [InlineData(0.216938, 0.150041, 0.048850, 45.6398, 39.8753, 35.2091)]
+ public void Convert_Xyz_to_Lab(float x, float y, float z, float l, float a, float b)
+ {
+ // Arrange
+ CieXyz input = new(x, y, z);
+ ColorConversionOptions options = new() { WhitePoint = Illuminants.D65, TargetWhitePoint = Illuminants.D65 };
+ ColorProfileConverter converter = new(options);
+ CieLab expected = new(l, a, b);
+
+ Span inputSpan = new CieXyz[5];
+ inputSpan.Fill(input);
+
+ Span actualSpan = new CieLab[5];
+
+ // Act
+ CieLab 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);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndLmsConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndLmsConversionTest.cs
new file mode 100644
index 0000000000..60938991e8
--- /dev/null
+++ b/tests/ImageSharp.Tests/ColorProfiles/CieXyzAndLmsConversionTest.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Tests.ColorProfiles;
+
+///
+/// Tests - conversions.
+///
+///
+/// Test data generated using original colorful library.
+///
+public class CieXyzAndLmsConversionTest
+{
+ private static readonly ApproximateColorProfileComparer Comparer = new(.0001f);
+
+ [Theory]
+ [InlineData(0.941428535, 1.040417467, 1.089532651, 0.95047, 1, 1.08883)]
+ [InlineData(0, 0, 0, 0, 0, 0)]
+ [InlineData(0.850765697, -0.713042594, 0.036973283, 0.95047, 0, 0)]
+ [InlineData(0.2664, 1.7135, -0.0685, 0, 1, 0)]
+ [InlineData(-0.175737162, 0.039960061, 1.121059368, 0, 0, 1.08883)]
+ [InlineData(0.2262677362, 0.0961411609, 0.0484570397, 0.216938, 0.150041, 0.048850)]
+ public void Convert_Lms_to_CieXyz(float l, float m, float s, float x, float y, float z)
+ {
+ // Arrange
+ Lms input = new(l, m, s);
+ ColorProfileConverter converter = new();
+ CieXyz expected = new(x, y, z);
+
+ Span inputSpan = new Lms[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.95047, 1, 1.08883, 0.941428535, 1.040417467, 1.089532651)]
+ [InlineData(0, 0, 0, 0, 0, 0)]
+ [InlineData(0.95047, 0, 0, 0.850765697, -0.713042594, 0.036973283)]
+ [InlineData(0, 1, 0, 0.2664, 1.7135, -0.0685)]
+ [InlineData(0, 0, 1.08883, -0.175737162, 0.039960061, 1.121059368)]
+ [InlineData(0.216938, 0.150041, 0.048850, 0.2262677362, 0.0961411609, 0.0484570397)]
+ public void Convert_CieXyz_to_Lms(float x, float y, float z, float l, float m, float s)
+ {
+ // Arrange
+ CieXyz input = new(x, y, z);
+ ColorProfileConverter converter = new();
+ Lms expected = new(l, m, s);
+
+ Span inputSpan = new CieXyz[5];
+ inputSpan.Fill(input);
+
+ Span actualSpan = new Lms[5];
+
+ // Act
+ Lms 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);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
index a389c8ab8c..41e6e525f8 100644
--- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
+++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
@@ -39,7 +39,7 @@
Do not update or consolidate BenchmarkDotNet.
https://github.com/dotnet/arcade/issues/8483
-->
-
+