diff --git a/src/ImageSharp/ColorProfiles/CieXyy.cs b/src/ImageSharp/ColorProfiles/CieXyy.cs index 9a1953ac84..4dcb582d6e 100644 --- a/src/ImageSharp/ColorProfiles/CieXyy.cs +++ b/src/ImageSharp/ColorProfiles/CieXyy.cs @@ -18,7 +18,7 @@ public readonly struct CieXyy : IColorProfile /// The x chroma component. /// The y chroma component. /// The y luminance component. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CieXyy(float x, float y, float yl) { // Not clamping as documentation about this space only indicates "usual" ranges @@ -31,7 +31,7 @@ public readonly struct CieXyy : IColorProfile /// Initializes a new instance of the struct. /// /// The vector representing the x, y, Y components. - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CieXyy(Vector3 vector) : this() { @@ -67,7 +67,7 @@ public readonly struct CieXyy : IColorProfile /// /// True if the current left is equal to the parameter; otherwise, false. /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(CieXyy left, CieXyy right) => left.Equals(right); /// @@ -78,7 +78,7 @@ public readonly struct CieXyy : IColorProfile /// /// True if the current left is unequal to the parameter; otherwise, false. /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right); /// @@ -148,7 +148,7 @@ public readonly struct CieXyy : IColorProfile public override bool Equals(object? obj) => obj is CieXyy other && this.Equals(other); /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(CieXyy other) => this.X.Equals(other.X) && this.Y.Equals(other.Y) diff --git a/src/ImageSharp/ColorProfiles/Cmyk.cs b/src/ImageSharp/ColorProfiles/Cmyk.cs new file mode 100644 index 0000000000..d586ab6d45 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Cmyk.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Represents an CMYK (cyan, magenta, yellow, keyline) color. +/// +public readonly struct Cmyk : IColorProfile +{ + private static readonly Vector4 Min = Vector4.Zero; + private static readonly Vector4 Max = Vector4.One; + + /// + /// Initializes a new instance of the struct. + /// + /// The cyan component. + /// The magenta component. + /// The yellow component. + /// The keyline black component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmyk(float c, float m, float y, float k) + : this(new Vector4(c, m, y, k)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the c, m, y, k components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmyk(Vector4 vector) + { + vector = Numerics.Clamp(vector, Min, Max); + this.C = vector.X; + this.M = vector.Y; + this.Y = vector.Z; + this.K = vector.W; + } + + /// + /// Gets the cyan color component. + /// A value ranging between 0 and 1. + /// + public readonly float C { get; } + + /// + /// Gets the magenta color component. + /// A value ranging between 0 and 1. + /// + public readonly float M { get; } + + /// + /// Gets the yellow color component. + /// A value ranging between 0 and 1. + /// + public readonly float Y { get; } + + /// + /// Gets the keyline black color component. + /// A value ranging between 0 and 1. + /// + public readonly float K { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Cmyk left, Cmyk right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Cmyk left, Cmyk right) => !left.Equals(right); + + /// + public static Cmyk FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) + { + // To CMY + Vector3 cmy = Vector3.One - source.ToScaledVector3(); + + // To CMYK + Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z))); + + if (MathF.Abs(k.X - 1F) < Constants.Epsilon) + { + return new Cmyk(0, 0, 0, 1F); + } + + cmy = (cmy - k) / (Vector3.One - k); + + return new Cmyk(cmy.X, cmy.Y, cmy.Z, k.X); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // TODO: We can optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + destination[i] = FromProfileConnectingSpace(options, in rgb); + } + } + + /// + public Rgb ToProfileConnectingSpace(ColorConversionOptions options) + { + Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (Vector3.One - new Vector3(this.K)); + return Rgb.FromScaledVector3(rgb); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + // TODO: We can possibly optimize this by using SIMD + for (int i = 0; i < source.Length; i++) + { + Cmyk cmyk = source[i]; + destination[i] = cmyk.ToProfileConnectingSpace(options); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() + => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + => HashCode.Combine(this.C, this.M, this.Y, this.K); + + /// + public override string ToString() + => FormattableString.Invariant($"Cmyk({this.C:#0.##}, {this.M:#0.##}, {this.Y:#0.##}, {this.K:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is Cmyk other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Cmyk other) + => this.C.Equals(other.C) + && this.M.Equals(other.M) + && this.Y.Equals(other.Y) + && this.K.Equals(other.K); +} diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs new file mode 100644 index 0000000000..51c68531de --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsCieLabCieLab +{ + public static TTo Convert(this ColorProfileConverter converter, TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + CieLab pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + pcsFromB = VonKriesChromaticAdaptation.Transform(options, in pcsFromB); + + // Convert between PCS + CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB); + + // 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 pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromTo = pcsFromToOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromTo); + + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + CieLab.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom); + + // Adapt to target white point + VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); + + // Convert between PCS. + CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsFromTo, destination); + } +} diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs new file mode 100644 index 0000000000..248a54026d --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsRgbRgb +{ + public static TTo Convert(this ColorProfileConverter converter, TFrom source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile + { + ColorConversionOptions options = converter.Options; + + // Convert to input PCS + Rgb pcsFromA = source.ToProfileConnectingSpace(options); + CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options); + + // Adapt to target white point + pcsFromB = VonKriesChromaticAdaptation.Transform(options, in pcsFromB); + + // Convert between PCS + Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB); + + // 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 pcsFromToOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFromTo = pcsFromToOwner.GetSpan(); + TFrom.ToProfileConnectionSpace(options, source, pcsFromTo); + + using IMemoryOwner pcsFromOwner = options.MemoryAllocator.Allocate(source.Length); + Span pcsFrom = pcsFromOwner.GetSpan(); + Rgb.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom); + + // Adapt to target white point + VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); + + // Convert between PCS. + Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo); + + // Convert to output from PCS + TTo.FromProfileConnectionSpace(options, pcsFromTo, destination); + } +} diff --git a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs index 81aac269d4..f67f47222d 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs @@ -18,7 +18,8 @@ internal readonly struct ApproximateColorProfileComparer : IEqualityComparer, IEqualityComparer, IEqualityComparer, - IEqualityComparer + IEqualityComparer, + IEqualityComparer { private readonly float epsilon; @@ -46,6 +47,8 @@ internal readonly struct ApproximateColorProfileComparer : 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 bool Equals(Cmyk x, Cmyk y) => this.Equals(x.C, y.C) && this.Equals(x.M, y.M) && this.Equals(x.Y, y.Y) && this.Equals(x.K, y.K); + public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode(); public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode(); @@ -64,6 +67,8 @@ internal readonly struct ApproximateColorProfileComparer : public int GetHashCode([DisallowNull] CieXyy obj) => obj.GetHashCode(); + public int GetHashCode([DisallowNull] Cmyk obj) => obj.GetHashCode(); + private bool Equals(float x, float y) { float d = x - y; diff --git a/tests/ImageSharp.Tests/ColorProfiles/CieLabAndCmykConversionTests.cs b/tests/ImageSharp.Tests/ColorProfiles/CieLabAndCmykConversionTests.cs new file mode 100644 index 0000000000..a13ad7d157 --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/CieLabAndCmykConversionTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion; + +/// +/// Tests - conversions. +/// +public class CieLabAndCmykConversionTests +{ + private static readonly ApproximateColorProfileComparer Comparer = new(.0002F); + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0)] + [InlineData(0, 1, 0.6156551, 5.960464E-08, 55.063, 82.54871, 23.16506)] + public void Convert_Cmyk_to_CieLab(float c, float m, float y, float k, float l, float a, float b) + { + // Arrange + Cmyk input = new(c, m, y, k); + CieLab expected = new(l, a, b); + ColorProfileConverter converter = new(); + + Span inputSpan = new Cmyk[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); + } + } + + [Theory] + [InlineData(0, 0, 0, 0, 0, 0, 1)] + [InlineData(36.0555, 303.6901, 10.01514, 0, 1, 0.597665966, 0)] + public void Convert_CieLab_to_Cmyk(float l, float a, float b, float c, float m, float y, float k) + { + // Arrange + CieLab input = new(l, a, b); + Cmyk expected = new(c, m, y, k); + ColorProfileConverter converter = new(); + + Span inputSpan = new CieLab[5]; + inputSpan.Fill(input); + + Span actualSpan = new Cmyk[5]; + + // Act + Cmyk 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/CmykAndYCbCrConversionTests.cs b/tests/ImageSharp.Tests/ColorProfiles/CmykAndYCbCrConversionTests.cs new file mode 100644 index 0000000000..ef3305ec70 --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/CmykAndYCbCrConversionTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion; + +/// +/// Tests - conversions. +/// +public class CmykAndYCbCrConversionTests +{ + private static readonly ApproximateColorProfileComparer Comparer = new(.0002F); + + [Theory] + [InlineData(0, 0, 0, 0, 255, 128, 128)] + [InlineData(0.360555, 0.1036901, 0.818514, 0.274615, 136.5134, 69.90555, 114.9948)] + public void Convert_Cmyk_to_YCbCr(float c, float m, float y, float k, float y2, float cb, float cr) + { + // Arrange + Cmyk input = new(c, m, y, k); + YCbCr expected = new(y2, cb, cr); + ColorProfileConverter converter = new(); + + Span inputSpan = new Cmyk[5]; + inputSpan.Fill(input); + + Span actualSpan = new YCbCr[5]; + + // Act + YCbCr 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(255, 128, 128, 0, 0, 0, 5.960464E-08)] + [InlineData(136.5134, 69.90555, 114.9948, 0.2891567, 0, 0.7951807, 0.3490196)] + public void Convert_YCbCr_to_Cmyk(float y2, float cb, float cr, float c, float m, float y, float k) + { + // Arrange + YCbCr input = new(y2, cb, cr); + Cmyk expected = new(c, m, y, k); + ColorProfileConverter converter = new(); + + Span inputSpan = new YCbCr[5]; + inputSpan.Fill(input); + + Span actualSpan = new Cmyk[5]; + + // Act + Cmyk 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/RgbAndCmykConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbAndCmykConversionTest.cs new file mode 100644 index 0000000000..3ae2996f2a --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/RgbAndCmykConversionTest.cs @@ -0,0 +1,77 @@ +// 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 RgbAndCmykConversionTest +{ + private static readonly ApproximateColorProfileComparer Comparer = new(.0001F); + + [Theory] + [InlineData(1, 1, 1, 1, 0, 0, 0)] + [InlineData(0, 0, 0, 0, 1, 1, 1)] + [InlineData(0, 0.84, 0.037, 0.365, 0.635, 0.1016, 0.6115)] + public void Convert_Cmyk_To_Rgb(float c, float m, float y, float k, float r, float g, float b) + { + // Arrange + Cmyk input = new(c, m, y, k); + Rgb expected = new(r, g, b); + ColorProfileConverter converter = new(); + + Span inputSpan = new Cmyk[5]; + inputSpan.Fill(input); + + Span actualSpan = new Rgb[5]; + + // Act + Rgb 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(1, 1, 1, 0, 0, 0, 0)] + [InlineData(0, 0, 0, 0, 0, 0, 1)] + [InlineData(0.635, 0.1016, 0.6115, 0, 0.84, 0.037, 0.365)] + public void Convert_Rgb_To_Cmyk(float r, float g, float b, float c, float m, float y, float k) + { + // Arrange + Rgb input = new(r, g, b); + Cmyk expected = new(c, m, y, k); + ColorProfileConverter converter = new(); + + Span inputSpan = new Rgb[5]; + inputSpan.Fill(input); + + Span actualSpan = new Cmyk[5]; + + // Act + Cmyk 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); + } + } +}