Browse Source

Flatten JPEG ICC color conversion

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
043c264aef
  1. 33
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs
  2. 32
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs
  3. 14
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs
  4. 30
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs
  5. 32
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs
  6. 35
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs
  7. 34
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs
  8. 35
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs
  9. 140
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs
  10. 9
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
  11. 151
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

33
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 4);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> c3 = values.Component3;
// JPEG CMYK stores inverted components, while the ICC converter consumes normalized conventional CMYK.
PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maximumValue);
Span<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed)[..source.Length];
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter converter = new(options);
converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

32
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 3);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> 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<Y> source = MemoryMarshal.Cast<float, Y>(c0);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed);
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter converter = new(options);
converter.Convert<Y, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

14
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
/// <param name="c2">The third converted component lanes.</param>
/// <param name="c3">The fourth converted component lanes, if used.</param>
public static abstract void ConvertFromRgb(Vector512<float> r, Vector512<float> g, Vector512<float> b, Vector512<float> maximumValue, Vector512<float> halfValue, Vector512<float> scale, out Vector512<float> c0, out Vector512<float> c1, out Vector512<float> c2, out Vector512<float> c3);
/// <summary>
/// Converts JPEG component values to RGB using the supplied ICC profile.
/// </summary>
/// <param name="configuration">The configuration used to allocate temporary storage.</param>
/// <param name="profile">The source ICC profile.</param>
/// <param name="values">The component values to convert.</param>
/// <param name="maximumValue">The maximum component value for the configured precision.</param>
public static abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue);
}
/// <summary>
@ -285,10 +275,6 @@ internal abstract partial class JpegColorConverterBase
}
}
/// <inheritdoc/>
public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile)
=> TOperator.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{

30
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 3);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> 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> rgb = MemoryMarshal.Cast<float, Rgb>(packed);
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter converter = new(options);
converter.Convert<Rgb, Rgb>(rgb, rgb);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..rgb.Length], c0, c1, c2);
}
}
}

32
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 4);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> 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<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed)[..source.Length];
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter converter = new(options);
converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

35
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 4);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> 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<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed);
converter.Convert<YccK, Cmyk>(MemoryMarshal.Cast<Cmyk, YccK>(source), source);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed)[..source.Length];
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

34
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;
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 3);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> 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<YCbCr> source = MemoryMarshal.Cast<float, YCbCr>(packed);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed);
converter.Convert<YCbCr, Rgb>(source, destination);
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new ColorProfileConverter(options);
converter.Convert<Rgb, Rgb>(destination, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

35
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 _);
}
/// <inheritdoc/>
public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maximumValue)
{
using IMemoryOwner<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(values.Component0.Length * 4);
Span<float> packed = memoryOwner.Memory.Span;
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> 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<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed);
converter.Convert<YccK, Cmyk>(MemoryMarshal.Cast<Cmyk, YccK>(source), source);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed)[..source.Length];
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);
}
}
}

140
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
{
/// <summary>
/// Converts planar jpeg component values in <paramref name="values"/> to RGB color space in-place using the given ICC profile.
/// </summary>
/// <param name="configuration">The configuration instance to use for the conversion.</param>
/// <param name="values">The input/output as a stack-only <see cref="ComponentValues"/> struct.</param>
/// <param name="profile">The ICC profile to use for the conversion.</param>
public void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> 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<float> memoryOwner = configuration.MemoryAllocator.Allocate<float>(length * packedComponentCount);
Span<float> 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<Y> source = MemoryMarshal.Cast<float, Y>(c0);
Span<Rgb> destination = MemoryMarshal.Cast<float, Rgb>(packed);
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter converter = new(options);
converter.Convert<Y, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(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> yCbCr = MemoryMarshal.Cast<float, YCbCr>(packed);
Span<Rgb> yCbCrDestination = MemoryMarshal.Cast<float, Rgb>(packed);
yCbCrConverter.Convert<YCbCr, Rgb>(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<Cmyk> yccKCmyk = MemoryMarshal.Cast<float, Cmyk>(packed);
yccKConverter.Convert<YccK, Cmyk>(MemoryMarshal.Cast<Cmyk, YccK>(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<Cmyk> tiffYccKCmyk = MemoryMarshal.Cast<float, Cmyk>(packed);
tiffYccKConverter.Convert<YccK, Cmyk>(MemoryMarshal.Cast<Cmyk, YccK>(tiffYccKCmyk), tiffYccKCmyk);
profileSourceIsCmyk = true;
break;
}
ColorConversionOptions profileOptions = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
ColorProfileConverter profileConverter = new(profileOptions);
Span<Rgb> rgb = MemoryMarshal.Cast<float, Rgb>(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<Cmyk, Rgb>(MemoryMarshal.Cast<float, Cmyk>(packed), rgb);
}
else
{
profileConverter.Convert<Rgb, Rgb>(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<float, Vector3>(packed)[..length], c0, c1, c2);
}
}

9
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
/// <param name="values">The input/output as a stack-only <see cref="ComponentValues"/> struct</param>
public abstract void ConvertToRgbInPlace(in ComponentValues values);
/// <summary>
/// Converts planar jpeg component values in <paramref name="values"/> to RGB color space in-place using the given ICC profile.
/// </summary>
/// <param name="configuration">The configuration instance to use for the conversion.</param>
/// <param name="values">The input/output as a stack-only <see cref="ComponentValues"/> struct.</param>
/// <param name="profile">The ICC profile to use for the conversion.</param>
public abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile);
/// <summary>
/// Converts RGB lanes to jpeg component values.
/// </summary>

151
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
}
}
/// <summary>
/// Verifies the flattened ICC traversal against independently composed color-model and profile conversions.
/// </summary>
/// <param name="colorSpace">The JPEG color space.</param>
/// <param name="componentCount">The number of component planes owned by the color model.</param>
/// <param name="precision">The JPEG sample precision.</param>
[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<Y, Rgb>(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<Rgb, Rgb>(new Rgb(c0, c1, c2));
break;
case JpegColorSpace.YCbCr:
Rgb rgb = modelConverter.Convert<YCbCr, Rgb>(new YCbCr(c0, c1, c2));
expected = profileConverter.Convert<Rgb, Rgb>(rgb);
break;
case JpegColorSpace.Cmyk:
expected = profileConverter.Convert<Cmyk, Rgb>(new Cmyk(1F - c0, 1F - c1, 1F - c2, 1F - c3));
break;
case JpegColorSpace.Ycck:
Cmyk cmyk = modelConverter.Convert<YccK, Cmyk>(new YccK(1F - c0, 1F - c1, 1F - c2, 1F - c3));
expected = profileConverter.Convert<Cmyk, Rgb>(cmyk);
break;
case JpegColorSpace.TiffCmyk:
expected = profileConverter.Convert<Cmyk, Rgb>(new Cmyk(c0, c1, c2, c3));
break;
case JpegColorSpace.TiffYccK:
Cmyk tiffCmyk = modelConverter.Convert<YccK, Cmyk>(new YccK(c0, c1, c2, c3));
expected = profileConverter.Convert<Cmyk, Rgb>(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);
}
}
}
/// <summary>
/// Verifies that the shared converter retains its scalar behavior when hardware intrinsics are disabled.
/// </summary>
@ -400,6 +530,27 @@ public class JpegColorConverterTests
return values;
}
/// <summary>
/// Creates an identity-transfer grayscale profile for the one-channel ICC test path.
/// </summary>
/// <returns>The grayscale ICC profile.</returns>
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);
}
/// <summary>
/// Compares one converted sample with an independent definition of its JPEG color model.
/// </summary>

Loading…
Cancel
Save