diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index cb02472df..49a442fdd 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -21,7 +21,7 @@ internal static class ColorProfileConverterExtensionsIcc 0.9965153F, 0.9965269F, 0.9965208F, 1F, 0.9965153F, 0.9965269F, 0.9965208F, 1F]; - private static readonly float[] PcsV2FromBlackPointAdd = + private static readonly float[] PcsV2FromBlackPointOffset = [0.00336F, 0.0034731F, 0.00287F, 0F, 0.00336F, 0.0034731F, 0.00287F, 0F, 0.00336F, 0.0034731F, 0.00287F, 0F, @@ -33,7 +33,7 @@ internal static class ColorProfileConverterExtensionsIcc 1.0034969F, 1.0034852F, 1.0034913F, 1F, 1.0034969F, 1.0034852F, 1.0034913F, 1F]; - private static readonly float[] PcsV2ToBlackPointAdd = + private static readonly float[] PcsV2ToBlackPointOffset = [0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, @@ -81,7 +81,6 @@ internal static class ColorProfileConverterExtensionsIcc return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); } - // TODO: update to match workflow of the function above internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -528,7 +527,7 @@ internal static class ColorProfileConverterExtensionsIcc { // TODO: Check our constants. They may require scaling. Vector vScale = new(PcsV2FromBlackPointScale.AsSpan()[..Vector.Count]); - Vector vAdd = new(PcsV2FromBlackPointAdd.AsSpan()[..Vector.Count]); + Vector vOffset = new(PcsV2FromBlackPointOffset.AsSpan()[..Vector.Count]); // SIMD loop int i = 0; @@ -538,9 +537,9 @@ internal static class ColorProfileConverterExtensionsIcc // Load the vector from source span Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); - // Scale and add the vector + // Scale and offset the vector v *= vScale; - v += vAdd; + v += vOffset; // Write the vector to the destination span Unsafe.WriteUnaligned(ref Unsafe.As(ref destination[i]), v); @@ -576,7 +575,7 @@ internal static class ColorProfileConverterExtensionsIcc { // TODO: Check our constants. They may require scaling. Vector vScale = new(PcsV2ToBlackPointScale.AsSpan()[..Vector.Count]); - Vector vAdd = new(PcsV2ToBlackPointAdd.AsSpan()[..Vector.Count]); + Vector vOffset = new(PcsV2ToBlackPointOffset.AsSpan()[..Vector.Count]); // SIMD loop int i = 0; @@ -586,9 +585,9 @@ internal static class ColorProfileConverterExtensionsIcc // Load the vector from source span Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i])); - // Scale and add the vector + // Scale and offset the vector v *= vScale; - v += vAdd; + v -= vOffset; // Write the vector to the destination span Unsafe.WriteUnaligned(ref Unsafe.As(ref destination[i]), v); @@ -599,7 +598,7 @@ internal static class ColorProfileConverterExtensionsIcc { Vector4 s = source[i]; s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); - s += new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); + s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); destination[i] = s; } } @@ -610,7 +609,7 @@ internal static class ColorProfileConverterExtensionsIcc { Vector4 s = source[i]; s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F); - s += new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); + s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F); destination[i] = s; } } diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index e2fa0760e..282bc5ba7 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs @@ -13,6 +13,21 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc; public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) { + // for 3-channel spaces, 4th item is ignored + private static readonly List Inputs = + [ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [1, 1, 1, 1], + [0.5f, 0.5f, 0.5f, 0.5f], + [0.199678659f, 0.67982769f, 0.805381715f, 0.982666492f], // requires clipping before source is PCS adjusted for Fogra39 -> sRGBv2 + [0.776568174f, 0.961630166f, 0.31032759f, 0.895294666f], // requires clipping after target is PCS adjusted for Fogra39 -> sRGBv2 + [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()] + ]; + [Theory] [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) @@ -29,30 +44,41 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) public void CanConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005) { - // for 3-channel spaces, 4th item is ignored - List inputs = - [ - [0, 0, 0, 0], - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - [1, 1, 1, 1], - [0.5f, 0.5f, 0.5f, 0.5f], - [0.199678659f, 0.67982769f, 0.805381715f, 0.982666492f], // requires clipping before source is PCS adjusted for Fogra39 -> sRGBv2 - [0.776568174f, 0.961630166f, 0.31032759f, 0.895294666f], // requires clipping after target is PCS adjusted for Fogra39 -> sRGBv2 - [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()] - ]; - - foreach (float[] input in inputs) - { - double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input, testOutputHelper); - testOutputHelper.WriteLine($"Input {string.Join(", ", input)} · Expected output {string.Join(", ", expectedTargetValues)}"); + List actual = Inputs.Select(input => GetActualTargetValues(input, sourceProfile, targetProfile)).ToList(); + AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper); + } + + [Theory] + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit) + [InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2) + [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M) + [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb)] // RGB -> XYZ -> LAB -> RGB (different LUT elements, B-Matrix-M vs B-Matrix-M-CLUT-A) + [InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) + [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, 0.0005)] // CMYK -> LAB -> XYZ -> RGB (different LUT tags, A2B vs TRC) --- tolerance slightly higher due to difference in inverse curve implementation + [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B) + public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005) + { + List actual = GetBulkActualTargetValues(Inputs, sourceProfile, targetProfile); + AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper); + } - Vector4 actualTargetValues = GetActualTargetValues(input, sourceProfile, targetProfile); - for (int i = 0; i < expectedTargetValues.Length; i++) + private static void AssertConversion(string sourceProfile, string targetProfile, List actual, double tolerance, ITestOutputHelper testOutputHelper) + { + List expected = Inputs.Select(input => GetExpectedTargetValues(sourceProfile, targetProfile, input, testOutputHelper)).ToList(); + Assert.Equal(expected.Count, actual.Count); + + for (int i = 0; i < expected.Count; i++) + { + Log(testOutputHelper, Inputs[i], expected[i], actual[i]); + for (int j = 0; j < expected[i].Length; j++) { - Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance); + Assert.Equal(expected[i][j], actual[i][j], tolerance); } } } @@ -147,6 +173,74 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) }; } + private static List GetBulkActualTargetValues(List inputs, string sourceProfile, string targetProfile) + { + ColorProfileConverter converter = new(new ColorConversionOptions + { + SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile), + TargetIccProfile = TestIccProfiles.GetProfile(targetProfile) + }); + + IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace; + IccColorSpaceType targetDataSpace = converter.Options.TargetIccProfile!.Header.DataColorSpace; + + switch (sourceDataSpace) + { + case IccColorSpaceType.Cmyk: + { + Span inputSpan = inputs.Select(x => new Cmyk(new Vector4(x))).ToArray(); + + switch (targetDataSpace) + { + case IccColorSpaceType.Cmyk: + { + Span outputSpan = stackalloc Cmyk[inputs.Count]; + converter.Convert(inputSpan, outputSpan); + return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList(); + } + + case IccColorSpaceType.Rgb: + { + Span outputSpan = stackalloc Rgb[inputs.Count]; + converter.Convert(inputSpan, outputSpan); + return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList(); + } + + default: + throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}"); + } + } + + case IccColorSpaceType.Rgb: + { + Span inputSpan = inputs.Select(x => new Rgb(new Vector3(x))).ToArray(); + + switch (targetDataSpace) + { + case IccColorSpaceType.Cmyk: + { + Span outputSpan = stackalloc Cmyk[inputs.Count]; + converter.Convert(inputSpan, outputSpan); + return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList(); + } + + case IccColorSpaceType.Rgb: + { + Span outputSpan = stackalloc Rgb[inputs.Count]; + converter.Convert(inputSpan, outputSpan); + return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList(); + } + + default: + throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}"); + } + } + + default: + throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}"); + } + } + private static float GetNormalizedRandomValue() { // Generate a random value between 0 (inclusive) and 1 (exclusive). @@ -158,4 +252,11 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) // Clamp the result between 0 and 1 to ensure it does not exceed the bounds. return value == 0 ? 0F : Math.Clamp((float)value + 0.0000001F, 0, 1); } + + private static void Log(ITestOutputHelper testOutputHelper, float[] input, double[] expected, Vector4 actual) + { + string inputText = string.Join(", ", input); + string expectedText = string.Join(", ", expected.Select(x => $"{x:f8}")); + testOutputHelper.WriteLine($"Input {inputText} · Expected output {expectedText} · Actual output {actual}"); + } } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 880bf84aa..ce391fad2 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -50,7 +50,7 @@ - +