diff --git a/src/ImageSharp/ColorProfiles/CieLab.cs b/src/ImageSharp/ColorProfiles/CieLab.cs index d7dd7415d3..72148af45a 100644 --- a/src/ImageSharp/ColorProfiles/CieLab.cs +++ b/src/ImageSharp/ColorProfiles/CieLab.cs @@ -26,8 +26,11 @@ public readonly struct CieLab : IProfileConnectingSpace /// The b (blue - yellow) component. [MethodImpl(MethodImplOptions.AggressiveInlining)] public CieLab(float l, float a, float b) - : this(new Vector3(l, a, b)) { + // Not clamping as documentation about this space only indicates "usual" ranges + this.L = l; + this.A = a; + this.B = b; } /// @@ -38,7 +41,6 @@ public readonly struct CieLab : IProfileConnectingSpace 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; diff --git a/src/ImageSharp/ColorProfiles/CieLuv.cs b/src/ImageSharp/ColorProfiles/CieLuv.cs index 64541e4961..9bf1020579 100644 --- a/src/ImageSharp/ColorProfiles/CieLuv.cs +++ b/src/ImageSharp/ColorProfiles/CieLuv.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.ColorProfiles; @@ -28,6 +29,19 @@ public readonly struct CieLuv : IColorProfile this.V = v; } + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, u, v components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CieLuv(Vector3 vector) + : this() + { + this.L = vector.X; + this.U = vector.Y; + this.V = vector.Z; + } + /// /// Gets the lightness dimension /// A value usually ranging between 0 and 100. diff --git a/src/ImageSharp/ColorProfiles/CieXyz.cs b/src/ImageSharp/ColorProfiles/CieXyz.cs index ec28a232da..fbd3e77d75 100644 --- a/src/ImageSharp/ColorProfiles/CieXyz.cs +++ b/src/ImageSharp/ColorProfiles/CieXyz.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.ColorProfiles; @@ -20,8 +21,11 @@ public readonly struct CieXyz : IProfileConnectingSpace /// 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)) { + // Not clamping as documentation about this space only indicates "usual" ranges + this.X = x; + this.Y = y; + this.Z = z; } /// @@ -31,7 +35,6 @@ public readonly struct CieXyz : IProfileConnectingSpace 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; diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 21575dd85c..697c0fbd84 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -21,6 +21,7 @@ public readonly struct Rgb : IProfileConnectingSpace [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rgb(float r, float g, float b) { + // Not clamping as this space can exceed "usual" ranges this.R = r; this.G = g; this.B = b; @@ -31,7 +32,7 @@ public readonly struct Rgb : IProfileConnectingSpace /// /// The vector representing the r, g, b components. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Rgb(Vector3 source) + public Rgb(Vector3 source) { this.R = source.X; this.G = source.Y; @@ -149,20 +150,32 @@ public readonly struct Rgb : IProfileConnectingSpace public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; /// - /// Initializes the pixel instance from a generic scaled . + /// Initializes the color instance from a generic scaled . /// - /// The vector to load the pixel from. + /// The vector to load the color from. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rgb FromScaledVector3(Vector3 source) => new(source); + public static Rgb FromScaledVector3(Vector3 source) => new(Vector3.Clamp(source, Vector3.Zero, Vector3.One)); /// - /// Initializes the pixel instance from a generic scaled . + /// Initializes the color instance from a generic scaled . /// - /// The vector to load the pixel from. + /// The vector to load the color from. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rgb FromScaledVector4(Vector4 source) => new(source.X, source.Y, source.Z); + public static Rgb FromScaledVector4(Vector4 source) + { + source = Vector4.Clamp(source, Vector4.Zero, Vector4.One); + return new(source.X, source.Y, source.Z); + } + + /// + /// Initializes the color instance for a source clamped between 0 and 1 + /// + /// The source to load the color from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb Clamp(Rgb source) => new(Vector3.Clamp(new(source.R, source.G, source.B), Vector3.Zero, Vector3.One)); /// /// Expands the color into a generic ("scaled") representation @@ -171,7 +184,15 @@ public readonly struct Rgb : IProfileConnectingSpace /// /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector3 ToScaledVector3() => new(this.R, this.G, this.B); + public Vector3 ToScaledVector3() => Clamp(this).ToVector3(); + + /// + /// Expands the color into a generic representation. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 ToVector3() => new(this.R, this.G, this.B); /// /// Expands the color into a generic ("scaled") representation @@ -180,7 +201,7 @@ public readonly struct Rgb : IProfileConnectingSpace /// /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToScaledVector4() => new(this.R, this.G, this.B, 1f); + public Vector4 ToScaledVector4() => new(this.ToScaledVector3(), 1f); private static Matrix4x4 GetCieXyzToRgbMatrix(RgbWorkingSpace workingSpace) { diff --git a/tests/ImageSharp.Tests/ColorProfiles/CieLuvAndHunterLabConversionTests.cs b/tests/ImageSharp.Tests/ColorProfiles/CieLuvAndHunterLabConversionTests.cs index ab44d2dcae..73b605fb62 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/CieLuvAndHunterLabConversionTests.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/CieLuvAndHunterLabConversionTests.cs @@ -15,7 +15,7 @@ public class CieLuvAndHunterLabConversionTests [Theory] [InlineData(0, 0, 0, 0, 0, 0)] [InlineData(36.0555, 93.6901, 10.01514, 30.59289, 48.55542, 9.80487)] - public void Convert_CieLuv_to_HunterLab(float l, float u, float v, float l2, float a, float b) + public void Convert_CieLuv_To_HunterLab(float l, float u, float v, float l2, float a, float b) { // Arrange CieLuv input = new(l, u, v); @@ -44,7 +44,7 @@ public class CieLuvAndHunterLabConversionTests [Theory] [InlineData(0, 0, 0, 0, 0, 0)] [InlineData(30.59289, 48.55542, 9.80487, 36.0555, 93.6901, 10.01514)] - public void Convert_HunterLab_to_CieLuv(float l2, float a, float b, float l, float u, float v) + public void Convert_HunterLab_To_CieLuv(float l2, float a, float b, float l, float u, float v) { // Arrange HunterLab input = new(l2, a, b); diff --git a/tests/ImageSharp.Tests/ColorProfiles/CompandingTests.cs b/tests/ImageSharp.Tests/ColorProfiles/CompandingTests.cs new file mode 100644 index 0000000000..1bdefa1095 --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/CompandingTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles; + +/// +/// Tests various companding algorithms. Expanded numbers are hand calculated from formulas online. +/// +public class CompandingTests +{ + private static readonly ApproximateFloatComparer Comparer = new(.000001F); + + [Fact] + public void Rec2020Companding_IsCorrect() + { + Vector4 input = new(.667F); + Vector4 e = Rec2020Companding.Expand(input); + Vector4 c = Rec2020Companding.Compress(e); + CompandingIsCorrectImpl(e, c, .44847462F, input); + } + + [Fact] + public void Rec709Companding_IsCorrect() + { + Vector4 input = new(.667F); + Vector4 e = Rec709Companding.Expand(input); + Vector4 c = Rec709Companding.Compress(e); + CompandingIsCorrectImpl(e, c, .4483577F, input); + } + + [Fact] + public void SRgbCompanding_IsCorrect() + { + Vector4 input = new(.667F); + Vector4 e = SRgbCompanding.Expand(input); + Vector4 c = SRgbCompanding.Compress(e); + CompandingIsCorrectImpl(e, c, .40242353F, input); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(30)] + public void SRgbCompanding_Expand_VectorSpan(int length) + { + Random rnd = new(42); + Vector4[] source = rnd.GenerateRandomVectorArray(length, 0, 1); + Vector4[] expected = new Vector4[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + expected[i] = SRgbCompanding.Expand(source[i]); + } + + SRgbCompanding.Expand(source); + + Assert.Equal(expected, source, Comparer); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(30)] + public void SRgbCompanding_Compress_VectorSpan(int length) + { + Random rnd = new(42); + Vector4[] source = rnd.GenerateRandomVectorArray(length, 0, 1); + Vector4[] expected = new Vector4[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + expected[i] = SRgbCompanding.Compress(source[i]); + } + + SRgbCompanding.Compress(source); + + Assert.Equal(expected, source, Comparer); + } + + [Fact] + public void GammaCompanding_IsCorrect() + { + const double gamma = 2.2; + Vector4 input = new(.667F); + Vector4 e = GammaCompanding.Expand(input, gamma); + Vector4 c = GammaCompanding.Compress(e, gamma); + CompandingIsCorrectImpl(e, c, .41027668F, input); + } + + [Fact] + public void LCompanding_IsCorrect() + { + Vector4 input = new(.667F); + Vector4 e = LCompanding.Expand(input); + Vector4 c = LCompanding.Compress(e); + CompandingIsCorrectImpl(e, c, .36236193F, input); + } + + private static void CompandingIsCorrectImpl(Vector4 e, Vector4 c, float expanded, Vector4 compressed) + { + // W (alpha) is already the linear representation of the color. + Assert.Equal(new Vector4(expanded, expanded, expanded, e.W), e, Comparer); + Assert.Equal(compressed, c, Comparer); + } +} diff --git a/tests/ImageSharp.Tests/ColorProfiles/RgbAndHslConversionTest.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbAndHslConversionTest.cs index b63b9ca842..0dc95628b9 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/RgbAndHslConversionTest.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/RgbAndHslConversionTest.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.ColorProfiles; -namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion; +namespace SixLabors.ImageSharp.Tests.ColorProfiles; /// /// Tests - conversions. diff --git a/tests/ImageSharp.Tests/ColorProfiles/StringRepresentationTests.cs b/tests/ImageSharp.Tests/ColorProfiles/StringRepresentationTests.cs new file mode 100644 index 0000000000..770c987dba --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/StringRepresentationTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles; + +public class StringRepresentationTests +{ + private static readonly Vector3 One = new(1); + private static readonly Vector3 Zero = new(0); + private static readonly Vector3 Random = new(42.4F, 94.5F, 83.4F); + + public static readonly TheoryData TestData = new() + { + { new CieLab(Zero), "CieLab(0, 0, 0)" }, + { new CieLch(Zero), "CieLch(0, 0, 0)" }, + { new CieLchuv(Zero), "CieLchuv(0, 0, 0)" }, + { new CieLuv(Zero), "CieLuv(0, 0, 0)" }, + { new CieXyz(Zero), "CieXyz(0, 0, 0)" }, + { new CieXyy(Zero), "CieXyy(0, 0, 0)" }, + { new HunterLab(Zero), "HunterLab(0, 0, 0)" }, + { new Lms(Zero), "Lms(0, 0, 0)" }, + { new Rgb(Zero), "Rgb(0, 0, 0)" }, + { new Hsl(Zero), "Hsl(0, 0, 0)" }, + { new Hsv(Zero), "Hsv(0, 0, 0)" }, + { new YCbCr(Zero), "YCbCr(0, 0, 0)" }, + { new CieLab(One), "CieLab(1, 1, 1)" }, + { new CieLch(One), "CieLch(1, 1, 1)" }, + { new CieLchuv(One), "CieLchuv(1, 1, 1)" }, + { new CieLuv(One), "CieLuv(1, 1, 1)" }, + { new CieXyz(One), "CieXyz(1, 1, 1)" }, + { new CieXyy(One), "CieXyy(1, 1, 1)" }, + { new HunterLab(One), "HunterLab(1, 1, 1)" }, + { new Lms(One), "Lms(1, 1, 1)" }, + { new Rgb(One), "Rgb(1, 1, 1)" }, + { new Hsl(One), "Hsl(1, 1, 1)" }, + { new Hsv(One), "Hsv(1, 1, 1)" }, + { new YCbCr(One), "YCbCr(1, 1, 1)" }, + { new CieXyChromaticityCoordinates(1, 1), "CieXyChromaticityCoordinates(1, 1)" }, + { new CieLab(Random), "CieLab(42.4, 94.5, 83.4)" }, + { new CieLch(Random), "CieLch(42.4, 94.5, 83.4)" }, + { new CieLchuv(Random), "CieLchuv(42.4, 94.5, 83.4)" }, + { new CieLuv(Random), "CieLuv(42.4, 94.5, 83.4)" }, + { new CieXyz(Random), "CieXyz(42.4, 94.5, 83.4)" }, + { new CieXyy(Random), "CieXyy(42.4, 94.5, 83.4)" }, + { new HunterLab(Random), "HunterLab(42.4, 94.5, 83.4)" }, + { new Lms(Random), "Lms(42.4, 94.5, 83.4)" }, + { new Rgb(Random), "Rgb(42.4, 94.5, 83.4)" }, + { Rgb.Clamp(new Rgb(Random)), "Rgb(1, 1, 1)" }, + { new Hsl(Random), "Hsl(42.4, 1, 1)" }, // clamping to 1 is expected + { new Hsv(Random), "Hsv(42.4, 1, 1)" }, // clamping to 1 is expected + { new YCbCr(Random), "YCbCr(42.4, 94.5, 83.4)" }, + }; + + [Theory] + [MemberData(nameof(TestData))] + public void StringRepresentationsAreCorrect(object color, string text) => Assert.Equal(text, color.ToString()); +}