Browse Source

Can translate between profiles.

pull/2739/head
James Jackson-South 2 years ago
parent
commit
fac508c4cb
  1. 21
      src/ImageSharp/ColorProfiles/CieConstants.cs
  2. 177
      src/ImageSharp/ColorProfiles/CieLab.cs
  3. 124
      src/ImageSharp/ColorProfiles/CieXyz.cs
  4. 51
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  5. 30
      src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
  6. 52
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  7. 52
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  8. 47
      src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs
  9. 18
      src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs
  10. 71
      src/ImageSharp/ColorProfiles/Illuminants.cs
  11. 136
      src/ImageSharp/ColorProfiles/Lms.cs
  12. 132
      src/ImageSharp/ColorProfiles/LmsAdaptationMatrix.cs
  13. 90
      src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
  14. 2
      src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs
  15. 42
      tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
  16. 86
      tests/ImageSharp.Tests/ColorProfiles/CieXyzAndCieLabConversionTest.cs
  17. 81
      tests/ImageSharp.Tests/ColorProfiles/CieXyzAndLmsConversionTest.cs
  18. 2
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj

21
src/ImageSharp/ColorProfiles/CieConstants.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Constants use for Cie conversion calculations
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html"/>
/// </summary>
internal static class CieConstants
{
/// <summary>
/// 216F / 24389F
/// </summary>
public const float Epsilon = 0.008856452F;
/// <summary>
/// 24389F / 27F
/// </summary>
public const float Kappa = 903.2963F;
}

177
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;
/// <summary>
/// Represents a CIE L*a*b* 1976 color.
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space"/>
/// </summary>
public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
{
/// <summary>
/// D50 standard illuminant.
/// Used when reference white is not specified explicitly.
/// </summary>
public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="l">The lightness dimension.</param>
/// <param name="a">The a (green - magenta) component.</param>
/// <param name="b">The b (blue - yellow) component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLab(float l, float a, float b)
: this(new Vector3(l, a, b))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLab"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, a, b components.</param>
[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;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the a color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is green, positive magenta.</remarks>
/// </summary>
public readonly float A { get; }
/// <summary>
/// Gets the b color component.
/// <remarks>A value usually ranging from -100 to 100. Negative is blue, positive is yellow</remarks>
/// </summary>
public readonly float B { get; }
/// <summary>
/// Compares two <see cref="CieLab"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieLab left, CieLab right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieLab"/> objects for inequality
/// </summary>
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieLab"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLab other) =>
this.L.Equals(other.L)
&& this.A.Equals(other.A)
&& this.B.Equals(other.B);
/// <inheritdoc/>
[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);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLab> 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);
}
}
/// <inheritdoc/>
[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);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLab lab = source[i];
destination[i] = lab.ToProfileConnectingSpace(options);
}
}
}

124
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;
/// <summary>
/// Represents an CIE XYZ 1931 color
/// <see href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"/>
/// </summary>
public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
/// </summary>
/// <param name="x">X is a mix (a linear combination) of cone response curves chosen to be nonnegative</param>
/// <param name="y">The y luminance component.</param>
/// <param name="z">Z is quasi-equal to blue stimulation, or the S cone of the human eye.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyz(float x, float y, float z)
: this(new Vector3(x, y, z))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
/// </summary>
/// <param name="vector">The vector representing the x, y, z components.</param>
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;
}
/// <summary>
/// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float X { get; }
/// <summary>
/// Gets the Y luminance component.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Y { get; }
/// <summary>
/// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response.
/// <remarks>A value usually ranging between 0 and 1.</remarks>
/// </summary>
public float Z { get; }
/// <summary>
/// Compares two <see cref="CieXyz"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyz"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieXyz left, CieXyz right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="CieXyz"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="CieXyz"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyz left, CieXyz right) => !left.Equals(right);
/// <summary>
/// Returns a new <see cref="Vector3"/> representing this instance.
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.X, this.Y, this.Z);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieXyz({this.X:#0.##}, {this.Y:#0.##}, {this.Z:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyz other)
=> this.X.Equals(other.X)
&& this.Y.Equals(other.Y)
&& this.Z.Equals(other.Z);
/// <inheritdoc/>
public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
=> new(source.X, source.Y, source.Z);
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
source.CopyTo(destination[..source.Length]);
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
=> new(this.X, this.Y, this.Z);
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
source.CopyTo(destination[..source.Length]);
}
}

51
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;
/// <summary>
/// Provides options for color profile conversion.
/// </summary>
public class ColorConversionOptions
{
private Matrix4x4 adaptationMatrix;
/// <summary>
/// Initializes a new instance of the <see cref="ColorConversionOptions"/> class.
/// </summary>
public ColorConversionOptions() => this.AdaptationMatrix = LmsAdaptationMatrix.Bradford;
/// <summary>
/// Gets the memory allocator.
/// </summary>
public MemoryAllocator MemoryAllocator { get; init; } = MemoryAllocator.Default;
/// <summary>
/// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space.
/// </summary>
public CieXyz WhitePoint { get; init; } = Illuminants.D50;
/// <summary>
/// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
/// </summary>
public CieXyz TargetWhitePoint { get; init; } = Illuminants.D50;
/// <summary>
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
/// </summary>
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; }
}

30
src/ImageSharp/ColorProfiles/ColorProfileConverter.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Allows the conversion of color profiles.
/// </summary>
public class ColorProfileConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary>
public ColorProfileConverter()
: this(new())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
public ColorProfileConverter(ColorConversionOptions options)
=> this.Options = options;
/// <summary>
/// Gets the color profile conversion options.
/// </summary>
public ColorConversionOptions Options { get; }
}

52
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<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
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<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS.
using IMemoryOwner<CieLab> pcsFromOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Convert between PCS.
using IMemoryOwner<CieXyz> pcsToOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length * 2);
Span<CieXyz> 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);
}
}

52
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<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
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<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS.
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Adapt to target white point
VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
// Convert between PCS.
using IMemoryOwner<CieXyz> pcsToOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsTo = pcsToOwner.GetSpan();
CieXyz.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

47
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;
/// <summary>
/// Defines the contract for all color profiles.
/// </summary>
/// <typeparam name="TSelf">The type of color profile.</typeparam>
/// <typeparam name="TProfileSpace">The type of color profile connecting space.</typeparam>
public interface IColorProfile<TSelf, TProfileSpace> : IEquatable<TSelf>
where TSelf : IColorProfile<TSelf, TProfileSpace>
where TProfileSpace : struct, IProfileConnectingSpace
{
/// <summary>
/// Converts the color to the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <returns>The <typeparamref name="TProfileSpace"/>.</returns>
public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options);
#pragma warning disable CA1000 // Do not declare static members on generic types
/// <summary>
/// Converts the color from the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color profile connecting space.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
public static abstract TSelf FromProfileConnectingSpace(ColorConversionOptions options, in TProfileSpace source);
/// <summary>
/// Converts the span of colors to the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color span to convert from.</param>
/// <param name="destination">The color profile span to write the results to.</param>
public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<TSelf> source, Span<TProfileSpace> destination);
/// <summary>
/// Converts the span of colors from the profile connection space.
/// </summary>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The color profile span to convert from.</param>
/// <param name="destination">The color span to write the results to.</param>
public static abstract void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<TProfileSpace> source, Span<TSelf> destination);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

18
src/ImageSharp/ColorProfiles/IProfileConnectingSpace.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Defines the contract for all color profile connection spaces.
/// </summary>
public interface IProfileConnectingSpace;
/// <summary>
/// Defines the contract for all color profile connection spaces.
/// </summary>
/// <typeparam name="TSelf">The type of color profile.</typeparam>
/// <typeparam name="TProfileSpace">The type of color profile connecting space.</typeparam>
public interface IProfileConnectingSpace<TSelf, TProfileSpace> : IColorProfile<TSelf, TProfileSpace>, IProfileConnectingSpace
where TSelf : struct, IColorProfile<TSelf, TProfileSpace>, IProfileConnectingSpace
where TProfileSpace : struct, IProfileConnectingSpace;

71
src/ImageSharp/ColorProfiles/Illuminants.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// The well known standard illuminants.
/// Standard illuminants provide a basis for comparing images or colors recorded under different lighting
/// </summary>
/// <remarks>
/// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// <br />
/// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant
/// </remarks>
public static class Illuminants
{
/// <summary>
/// Incandescent / Tungsten
/// </summary>
public static readonly CieXyz A = new(1.09850F, 1F, 0.35585F);
/// <summary>
/// Direct sunlight at noon (obsoleteF)
/// </summary>
public static readonly CieXyz B = new(0.99072F, 1F, 0.85223F);
/// <summary>
/// Average / North sky Daylight (obsoleteF)
/// </summary>
public static readonly CieXyz C = new(0.98074F, 1F, 1.18232F);
/// <summary>
/// Horizon Light. ICC profile PCS
/// </summary>
public static readonly CieXyz D50 = new(0.96422F, 1F, 0.82521F);
/// <summary>
/// Mid-morning / Mid-afternoon Daylight
/// </summary>
public static readonly CieXyz D55 = new(0.95682F, 1F, 0.92149F);
/// <summary>
/// Noon Daylight: TelevisionF, sRGB color space
/// </summary>
public static readonly CieXyz D65 = new(0.95047F, 1F, 1.08883F);
/// <summary>
/// North sky Daylight
/// </summary>
public static readonly CieXyz D75 = new(0.94972F, 1F, 1.22638F);
/// <summary>
/// Equal energy
/// </summary>
public static readonly CieXyz E = new(1F, 1F, 1F);
/// <summary>
/// Cool White Fluorescent
/// </summary>
public static readonly CieXyz F2 = new(0.99186F, 1F, 0.67393F);
/// <summary>
/// D65 simulatorF, Daylight simulator
/// </summary>
public static readonly CieXyz F7 = new(0.95041F, 1F, 1.08747F);
/// <summary>
/// Philips TL84F, Ultralume 40
/// </summary>
public static readonly CieXyz F11 = new(1.00962F, 1F, 0.64350F);
}

136
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<Lms, CieXyz>
{
/// <summary>
/// Initializes a new instance of the <see cref="Lms"/> struct.
/// </summary>
/// <param name="l">L represents the responsivity at long wavelengths.</param>
/// <param name="m">M represents the responsivity at medium wavelengths.</param>
/// <param name="s">S represents the responsivity at short wavelengths.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Lms(float l, float m, float s)
: this(new Vector3(l, m, s))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Lms"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, m, s components.</param>
[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;
}
/// <summary>
/// Gets the L long component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the M medium component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public readonly float M { get; }
/// <summary>
/// Gets the S short component.
/// <remarks>A value usually ranging between -1 and 1.</remarks>
/// </summary>
public readonly float S { get; }
/// <summary>
/// Compares two <see cref="Lms"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Lms"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Lms"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Lms left, Lms right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Lms"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Lms"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Lms"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Lms left, Lms right) => !left.Equals(right);
/// <summary>
/// Returns a new <see cref="Vector3"/> representing this instance.
/// </summary>
/// <returns>The <see cref="Vector3"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3 ToVector3() => new(this.L, this.M, this.S);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.L, this.M, this.S);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"Lms({this.L:#0.##}, {this.M:#0.##}, {this.S:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Lms other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Lms other)
=> this.L.Equals(other.L)
&& this.M.Equals(other.M)
&& this.S.Equals(other.S);
/// <inheritdoc/>
public static Lms FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
{
Vector3 vector = Vector3.Transform(source.ToVector3(), options.AdaptationMatrix);
return new Lms(vector);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<Lms> 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);
}
}
/// <inheritdoc/>
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
{
Vector3 vector = Vector3.Transform(this.ToVector3(), options.InverseAdaptationMatrix);
return new CieXyz(vector);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Lms> source, Span<CieXyz> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
Lms lms = source[i];
destination[i] = lms.ToProfileConnectingSpace(options);
}
}
}

132
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;
/// <summary>
/// Matrices used for transformation from <see cref="CieXyz"/> to <see cref="Lms"/>, defining the cone response domain.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public static class LmsAdaptationMatrix
{
/// <summary>
/// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65)
/// </summary>
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.
});
/// <summary>
/// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy)
/// </summary>
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
});
/// <summary>
/// XYZ scaling chromatic adaptation transform matrix
/// </summary>
public static readonly Matrix4x4 XyzScaling = Matrix4x4.Transpose(Matrix4x4.Identity);
/// <summary>
/// Bradford chromatic adaptation transform matrix (used in CMCCAT97)
/// </summary>
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
});
/// <summary>
/// Spectral sharpening and the Bradford transform
/// </summary>
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
});
/// <summary>
/// CMCCAT2000 (fitted from all available color data sets)
/// </summary>
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
});
/// <summary>
/// CAT02 (optimized for minimizing CIELAB differences)
/// </summary>
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
});
}

90
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;
/// <summary>
/// Implementation of the Von Kries chromatic adaptation model.
/// </summary>
/// <remarks>
/// Transformation described here:
/// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// </remarks>
public static class VonKriesChromaticAdaptation
{
/// <summary>
/// Performs a linear transformation of a source color in to the destination color.
/// </summary>
/// <remarks>Doesn't crop the resulting color space coordinates (e.g. allows negative values for XYZ coordinates).</remarks>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The source color.</param>
/// <returns>The <see cref="CieXyz"/></returns>
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));
}
/// <summary>
/// Performs a bulk linear transformation of a source color in to the destination color.
/// </summary>
/// <remarks>Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates).</remarks>
/// <param name="options">The color profile conversion options.</param>
/// <param name="source">The span to the source colors.</param>
/// <param name="destination">The span to the destination colors.</param>
public static void Transform(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> 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));
}
}
}

2
src/ImageSharp/ColorSpaces/Conversion/Implementation/VonKriesChromaticAdaptation.cs

@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.ColorSpaces.Conversion;
/// <summary>
/// Implementation of the von Kries chromatic adaptation model.
/// Implementation of the Von Kries chromatic adaptation model.
/// </summary>
/// <remarks>
/// Transformation described here:

42
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;
/// <summary>
/// Allows the approximate comparison of color profile component values.
/// </summary>
internal readonly struct ApproximateColorProfileComparer :
IEqualityComparer<CieLab>,
IEqualityComparer<CieXyz>,
IEqualityComparer<Lms>
{
private readonly float epsilon;
/// <summary>
/// Initializes a new instance of the <see cref="ApproximateColorProfileComparer"/> struct.
/// </summary>
/// <param name="epsilon">The comparison error difference epsilon to use.</param>
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;
}
}

86
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;
/// <summary>
/// Tests <see cref="CieXyz"/>-<see cref="CieLab"/> conversions.
/// </summary>
/// <remarks>
/// Test data generated using:
/// <see href="http://www.brucelindbloom.com/index.html?ColorCalculator.html"/>
/// </remarks>
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<CieLab> inputSpan = new CieLab[5];
inputSpan.Fill(input);
Span<CieXyz> actualSpan = new CieXyz[5];
// Act
CieXyz actual = converter.Convert<CieLab, CieXyz>(input);
converter.Convert<CieLab, CieXyz>(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<CieXyz> inputSpan = new CieXyz[5];
inputSpan.Fill(input);
Span<CieLab> actualSpan = new CieLab[5];
// Act
CieLab actual = converter.Convert<CieXyz, CieLab>(input);
converter.Convert<CieXyz, CieLab>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}

81
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;
/// <summary>
/// Tests <see cref="CieXyz"/>-<see cref="Lms"/> conversions.
/// </summary>
/// <remarks>
/// Test data generated using original colorful library.
/// </remarks>
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<Lms> inputSpan = new Lms[5];
inputSpan.Fill(input);
Span<CieXyz> actualSpan = new CieXyz[5];
// Act
CieXyz actual = converter.Convert<Lms, CieXyz>(input);
converter.Convert<Lms, CieXyz>(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<CieXyz> inputSpan = new CieXyz[5];
inputSpan.Fill(input);
Span<Lms> actualSpan = new Lms[5];
// Act
Lms actual = converter.Convert<CieXyz, Lms>(input);
converter.Convert<CieXyz, Lms>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}

2
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -39,7 +39,7 @@
Do not update or consolidate BenchmarkDotNet.
https://github.com/dotnet/arcade/issues/8483
-->
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" />
<PackageReference Include="Microsoft.DotNet.RemoteExecutor" />
<PackageReference Include="Microsoft.DotNet.XUnitExtensions" />

Loading…
Cancel
Save