diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs
index 2f67e0f53..03469b2e6 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs
@@ -1,14 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -170,32 +164,5 @@ internal abstract partial class JpegColorConverterBase
c2 = maximumValue - (y * maximumValue);
c3 = maximumValue - k;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4);
- Span packed = memoryOwner.Memory.Span;
-
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
- Span c3 = values.Component3;
-
- // JPEG CMYK stores inverted components, while the ICC converter consumes normalized conventional CMYK.
- PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maximumValue);
-
- Span source = MemoryMarshal.Cast(packed);
- Span destination = MemoryMarshal.Cast(packed)[..source.Length];
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- ColorProfileConverter converter = new(options);
- converter.Convert(source, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs
index 8522fbf5c..6e5753d2f 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs
@@ -1,15 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -119,31 +113,5 @@ internal abstract partial class JpegColorConverterBase
c2 = default;
c3 = default;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
- float scale = 1F / maximumValue;
-
- // ICC luminance values are normalized, so the source plane is scaled in place before conversion.
- TensorPrimitives_.Multiply(c0, scale, c0);
-
- Span source = MemoryMarshal.Cast(c0);
- Span destination = MemoryMarshal.Cast(packed);
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- ColorProfileConverter converter = new(options);
- converter.Convert(source, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs
index 0058a6213..3b2ab914b 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs
@@ -5,7 +5,6 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -137,15 +136,6 @@ internal abstract partial class JpegColorConverterBase
/// The third converted component lanes.
/// The fourth converted component lanes, if used.
public static abstract void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3);
-
- ///
- /// Converts JPEG component values to RGB using the supplied ICC profile.
- ///
- /// The configuration used to allocate temporary storage.
- /// The source ICC profile.
- /// The component values to convert.
- /// The maximum component value for the configured precision.
- public static abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue);
}
///
@@ -285,10 +275,6 @@ internal abstract partial class JpegColorConverterBase
}
}
- ///
- public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile)
- => TOperator.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue);
-
///
public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane)
{
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs
index a97144de9..7f05a5d08 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs
@@ -1,14 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -109,29 +103,5 @@ internal abstract partial class JpegColorConverterBase
c2 = b;
c3 = default;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
-
- // JPEG planes use the integer sample domain, while ICC RGB values are normalized and interleaved.
- PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maximumValue);
-
- Span rgb = MemoryMarshal.Cast(packed);
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- ColorProfileConverter converter = new(options);
- converter.Convert(rgb, rgb);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..rgb.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs
index f2c20959d..a3ba9ecb7 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs
@@ -1,14 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -156,31 +150,5 @@ internal abstract partial class JpegColorConverterBase
c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue;
c3 = k;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
- Span c3 = values.Component3;
-
- // TIFF CMYK is already non-inverted, so only normalization and interleaving precede ICC conversion.
- PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maximumValue);
-
- Span source = MemoryMarshal.Cast(packed);
- Span destination = MemoryMarshal.Cast(packed)[..source.Length];
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- ColorProfileConverter converter = new(options);
- converter.Convert(source, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs
index 63e14fb88..90cc84048 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs
@@ -1,15 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -184,34 +178,5 @@ internal abstract partial class JpegColorConverterBase
c2 = halfValue + (Vector512_.MultiplyAddEstimate(Vector512.Create(0.5F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b)) * maximumValue);
c3 = k * maximumValue;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
- Span c3 = values.Component3;
-
- // TIFF YccK is non-inverted, so normalize directly before converting its JPEG-specific model to CMYK.
- PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maximumValue);
-
- ColorProfileConverter converter = new();
- Span source = MemoryMarshal.Cast(packed);
- converter.Convert(MemoryMarshal.Cast(source), source);
-
- Span destination = MemoryMarshal.Cast(packed)[..source.Length];
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- converter = new ColorProfileConverter(options);
- converter.Convert(source, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs
index 9ceb7b44b..e5bb6db03 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs
@@ -1,15 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -172,33 +166,5 @@ internal abstract partial class JpegColorConverterBase
c2 = halfValue + Vector512_.MultiplyAddEstimate(Vector512.Create(0.5F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b));
c3 = default;
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
-
- // ICC profiles rarely expose YCbCr transforms, so BT.601 first produces RGB in the profile's source space.
- PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maximumValue);
-
- ColorProfileConverter converter = new();
- Span source = MemoryMarshal.Cast(packed);
- Span destination = MemoryMarshal.Cast(packed);
- converter.Convert(source, destination);
-
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- converter = new ColorProfileConverter(options);
- converter.Convert(destination, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs
index 003c77227..90bee0c94 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs
@@ -1,15 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using SixLabors.ImageSharp.ColorProfiles;
-using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -136,34 +130,5 @@ internal abstract partial class JpegColorConverterBase
YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _);
}
-
- ///
- public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
- {
- using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4);
- Span packed = memoryOwner.Memory.Span;
- Span c0 = values.Component0;
- Span c1 = values.Component1;
- Span c2 = values.Component2;
- Span c3 = values.Component3;
-
- // Adobe-style JPEG YccK is inverted; normalize it before applying the format-defined YccK-to-CMYK transform.
- PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maximumValue);
-
- ColorProfileConverter converter = new();
- Span source = MemoryMarshal.Cast(packed);
- converter.Convert(MemoryMarshal.Cast(source), source);
-
- Span destination = MemoryMarshal.Cast(packed)[..source.Length];
- ColorConversionOptions options = new()
- {
- SourceIccProfile = profile,
- TargetIccProfile = CompactSrgbV4Profile.Profile,
- };
-
- converter = new ColorProfileConverter(options);
- converter.Convert(source, destination);
- UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs
new file mode 100644
index 000000000..37f1297a6
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+#nullable disable
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
+using SixLabors.ImageSharp.Common.Helpers;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
+
+internal abstract partial class JpegColorConverterBase
+{
+ ///
+ /// Converts planar jpeg component values in to RGB color space in-place using the given ICC profile.
+ ///
+ /// The configuration instance to use for the conversion.
+ /// The input/output as a stack-only struct.
+ /// The ICC profile to use for the conversion.
+ public void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile)
+ {
+ Span c0 = values.Component0;
+ Span c1 = values.Component1;
+ Span c2 = values.Component2;
+ int length = c0.Length;
+
+ // Four-component JPEG models need room for an interleaved CMYK or YccK source. Every conversion
+ // finishes with three packed RGB floats, which safely occupy the start of the same temporary buffer.
+ bool hasFourthComponent = this.ColorSpace is JpegColorSpace.Ycck
+ or JpegColorSpace.Cmyk
+ or JpegColorSpace.TiffYccK
+ or JpegColorSpace.TiffCmyk;
+ int packedComponentCount = hasFourthComponent ? 4 : 3;
+
+ using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(length * packedComponentCount);
+ Span packed = memoryOwner.Memory.Span;
+
+ if (this.ColorSpace == JpegColorSpace.Grayscale)
+ {
+ // The single luminance plane is the ICC source, so it is normalized in place. The temporary
+ // buffer is still RGB-sized because the profile conversion expands each Y sample to three lanes.
+ TensorPrimitives_.Multiply(c0, 1F / this.MaximumValue, c0);
+
+ Span source = MemoryMarshal.Cast(c0);
+ Span destination = MemoryMarshal.Cast(packed);
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = profile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ };
+
+ ColorProfileConverter converter = new(options);
+ converter.Convert(source, destination);
+ UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..length], c0, c1, c2);
+ return;
+ }
+
+ // RGB and YCbCr become packed RGB before the profile transform. CMYK models remain packed CMYK,
+ // because their source profile describes that four-component space rather than an intermediate RGB space.
+ bool profileSourceIsCmyk = false;
+
+ switch (this.ColorSpace)
+ {
+ case JpegColorSpace.RGB:
+ // JPEG RGB planes use the integer sample domain; ICC RGB values use normalized interleaved lanes.
+ PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / this.MaximumValue);
+ break;
+
+ case JpegColorSpace.YCbCr:
+ // ICC profiles rarely expose YCbCr transforms. BT.601 therefore produces RGB in the profile's
+ // source space first, and that packed RGB becomes the input to the profile transform below.
+ PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / this.MaximumValue);
+
+ ColorProfileConverter yCbCrConverter = new();
+ Span yCbCr = MemoryMarshal.Cast(packed);
+ Span yCbCrDestination = MemoryMarshal.Cast(packed);
+ yCbCrConverter.Convert(yCbCr, yCbCrDestination);
+ break;
+
+ case JpegColorSpace.Cmyk:
+ // Adobe-style JPEG CMYK stores inverted samples, while ICC consumes conventional normalized CMYK.
+ PackedInvertNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue);
+ profileSourceIsCmyk = true;
+ break;
+
+ case JpegColorSpace.TiffCmyk:
+ // TIFF JPEG CMYK is already non-inverted, so only normalization and interleaving are required.
+ PackedNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue);
+ profileSourceIsCmyk = true;
+ break;
+
+ case JpegColorSpace.Ycck:
+ // Adobe-style JPEG YccK is inverted before its format-defined YccK-to-CMYK transform.
+ PackedInvertNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue);
+
+ ColorProfileConverter yccKConverter = new();
+ Span yccKCmyk = MemoryMarshal.Cast(packed);
+ yccKConverter.Convert(MemoryMarshal.Cast(yccKCmyk), yccKCmyk);
+ profileSourceIsCmyk = true;
+ break;
+
+ case JpegColorSpace.TiffYccK:
+ // TIFF JPEG YccK is non-inverted, but otherwise uses the same YccK-to-CMYK transform.
+ PackedNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue);
+
+ ColorProfileConverter tiffYccKConverter = new();
+ Span tiffYccKCmyk = MemoryMarshal.Cast(packed);
+ tiffYccKConverter.Convert(MemoryMarshal.Cast(tiffYccKCmyk), tiffYccKCmyk);
+ profileSourceIsCmyk = true;
+ break;
+ }
+
+ ColorConversionOptions profileOptions = new()
+ {
+ SourceIccProfile = profile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ };
+
+ ColorProfileConverter profileConverter = new(profileOptions);
+ Span rgb = MemoryMarshal.Cast(packed)[..length];
+
+ if (profileSourceIsCmyk)
+ {
+ // The destination aliases the first three floats of each four-float source item. The converter
+ // supports this established in-place contraction, and the source span retains its original length.
+ profileConverter.Convert(MemoryMarshal.Cast(packed), rgb);
+ }
+ else
+ {
+ profileConverter.Convert(rgb, rgb);
+ }
+
+ // Only the packed RGB prefix is meaningful after four-component conversion; scatter it back to the
+ // decoder's three planar output buffers while leaving the fourth source component untouched.
+ UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..length], c0, c1, c2);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
index 36adf8f75..2b1271c82 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
@@ -3,7 +3,6 @@
#nullable disable
using SixLabors.ImageSharp.Memory;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -81,14 +80,6 @@ internal abstract partial class JpegColorConverterBase
/// The input/output as a stack-only struct
public abstract void ConvertToRgbInPlace(in ComponentValues values);
- ///
- /// Converts planar jpeg component values in to RGB color space in-place using the given ICC profile.
- ///
- /// The configuration instance to use for the conversion.
- /// The input/output as a stack-only struct.
- /// The ICC profile to use for the conversion.
- public abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile);
-
///
/// Converts RGB lanes to jpeg component values.
///
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
index 76cd2a794..de42e85d7 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
@@ -2,8 +2,11 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Tests.ColorProfiles;
+using SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg;
@@ -168,6 +171,133 @@ public class JpegColorConverterTests
}
}
+ ///
+ /// Verifies the flattened ICC traversal against independently composed color-model and profile conversions.
+ ///
+ /// The JPEG color space.
+ /// The number of component planes owned by the color model.
+ /// The JPEG sample precision.
+ [Theory]
+ [InlineData(JpegColorSpace.Grayscale, 1, 8)]
+ [InlineData(JpegColorSpace.Grayscale, 1, 12)]
+ [InlineData(JpegColorSpace.RGB, 3, 8)]
+ [InlineData(JpegColorSpace.RGB, 3, 12)]
+ [InlineData(JpegColorSpace.YCbCr, 3, 8)]
+ [InlineData(JpegColorSpace.YCbCr, 3, 12)]
+ [InlineData(JpegColorSpace.Cmyk, 4, 8)]
+ [InlineData(JpegColorSpace.Cmyk, 4, 12)]
+ [InlineData(JpegColorSpace.Ycck, 4, 8)]
+ [InlineData(JpegColorSpace.Ycck, 4, 12)]
+ [InlineData(JpegColorSpace.TiffCmyk, 4, 8)]
+ [InlineData(JpegColorSpace.TiffCmyk, 4, 12)]
+ [InlineData(JpegColorSpace.TiffYccK, 4, 8)]
+ [InlineData(JpegColorSpace.TiffYccK, 4, 12)]
+ internal void ConvertToRgbWithIccMatchesColorProfileDefinition(JpegColorSpace colorSpace, int componentCount, int precision)
+ {
+ const int length = 8;
+ JpegColorConverterBase.ComponentValues source = CreateRandomValues(length, componentCount, precision);
+ JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision);
+ JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision);
+
+ // YccK and CMYK profiles describe the conventional CMYK values produced after JPEG inversion
+ // and any YccK model transform. Three-component models use an RGB profile, while grayscale
+ // needs a one-channel profile so the profile's declared data space agrees with the Y input.
+ IccProfile profile = colorSpace switch
+ {
+ JpegColorSpace.Grayscale => CreateGrayProfile(),
+ JpegColorSpace.Cmyk or JpegColorSpace.Ycck or JpegColorSpace.TiffCmyk or JpegColorSpace.TiffYccK
+ => TestIccProfiles.GetProfile(TestIccProfiles.Fogra39),
+ _ => CompactSrgbV4Profile.Profile,
+ };
+
+ converter.ConvertToRgbInPlaceWithIcc(Configuration.Default, actual, profile);
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = profile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ };
+
+ ColorProfileConverter profileConverter = new(options);
+ ColorProfileConverter modelConverter = new();
+ float maximumValue = MathF.Pow(2, precision) - 1;
+ Rgb[] grayscaleExpected = null;
+
+ if (colorSpace == JpegColorSpace.Grayscale)
+ {
+ // The profile converter has distinct scalar and span entry points. Production converts the complete
+ // grayscale row, so the independent reference must exercise that same public span contract.
+ Y[] luminance = new Y[length];
+ grayscaleExpected = new Rgb[length];
+
+ for (int i = 0; i < length; i++)
+ {
+ luminance[i] = new Y(source.Component0[i] / maximumValue);
+ }
+
+ profileConverter.Convert(luminance, grayscaleExpected);
+ }
+
+ for (int i = 0; i < length; i++)
+ {
+ float c0 = source.Component0[i] / maximumValue;
+ float c1 = componentCount >= 2 ? source.Component1[i] / maximumValue : 0;
+ float c2 = componentCount >= 3 ? source.Component2[i] / maximumValue : 0;
+ float c3 = componentCount == 4 ? source.Component3[i] / maximumValue : 0;
+ Rgb expected;
+
+ switch (colorSpace)
+ {
+ case JpegColorSpace.Grayscale:
+ expected = grayscaleExpected[i];
+ break;
+
+ case JpegColorSpace.RGB:
+ expected = profileConverter.Convert(new Rgb(c0, c1, c2));
+ break;
+
+ case JpegColorSpace.YCbCr:
+ Rgb rgb = modelConverter.Convert(new YCbCr(c0, c1, c2));
+ expected = profileConverter.Convert(rgb);
+ break;
+
+ case JpegColorSpace.Cmyk:
+ expected = profileConverter.Convert(new Cmyk(1F - c0, 1F - c1, 1F - c2, 1F - c3));
+ break;
+
+ case JpegColorSpace.Ycck:
+ Cmyk cmyk = modelConverter.Convert(new YccK(1F - c0, 1F - c1, 1F - c2, 1F - c3));
+ expected = profileConverter.Convert(cmyk);
+ break;
+
+ case JpegColorSpace.TiffCmyk:
+ expected = profileConverter.Convert(new Cmyk(c0, c1, c2, c3));
+ break;
+
+ case JpegColorSpace.TiffYccK:
+ Cmyk tiffCmyk = modelConverter.Convert(new YccK(c0, c1, c2, c3));
+ expected = profileConverter.Convert(tiffCmyk);
+ break;
+
+ default:
+ Assert.Fail($"Unexpected JPEG color space: {colorSpace}.");
+ return;
+ }
+
+ if (colorSpace == JpegColorSpace.Grayscale)
+ {
+ // Grayscale exposes one physical component plane through all three component views. Deinterleaving
+ // therefore leaves the final blue write in that plane, matching the established converter contract.
+ Assert.Equal(expected.B, actual.Component0[i], ToRgbTolerance);
+ }
+ else
+ {
+ Assert.Equal(expected.R, actual.Component0[i], ToRgbTolerance);
+ Assert.Equal(expected.G, actual.Component1[i], ToRgbTolerance);
+ Assert.Equal(expected.B, actual.Component2[i], ToRgbTolerance);
+ }
+ }
+ }
+
///
/// Verifies that the shared converter retains its scalar behavior when hardware intrinsics are disabled.
///
@@ -400,6 +530,27 @@ public class JpegColorConverterTests
return values;
}
+ ///
+ /// Creates an identity-transfer grayscale profile for the one-channel ICC test path.
+ ///
+ /// The grayscale ICC profile.
+ private static IccProfile CreateGrayProfile()
+ {
+ IccProfileHeader header = new()
+ {
+ Class = IccProfileClass.InputDevice,
+ DataColorSpace = IccColorSpaceType.Gray,
+ ProfileConnectionSpace = IccColorSpaceType.CieXyz,
+ RenderingIntent = IccRenderingIntent.MediaRelativeColorimetric,
+ Version = new IccVersion(4, 3, 0),
+ };
+
+ // An empty ICC curve is the standard identity transfer function. Tagging it as GrayTrc gives
+ // the profile converter a complete one-channel device-to-PCS path without test-only LUT data.
+ IccTagDataEntry[] entries = [new IccCurveTagDataEntry(IccProfileTag.GrayTrc)];
+ return new IccProfile(header, entries);
+ }
+
///
/// Compares one converted sample with an independent definition of its JPEG color model.
///