From 74d475d4c4e1be9699c536047b69302374b05d0f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 16 May 2025 23:20:12 +1000 Subject: [PATCH] Use ICC profile when available for JPEG decoding color transforms. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 10 +- ...e.Generated.cs => CompactSrgbV4Profile.cs} | 19 ++-- .../Formats/ColorProfileHandling.cs | 7 +- src/ImageSharp/Formats/DecoderOptions.cs | 43 +++++++- src/ImageSharp/Formats/ImageDecoder.cs | 22 +++++ .../JpegColorConverter.CmykScalar.cs | 37 +++++++ .../JpegColorConverter.CmykVector128.cs | 7 +- .../JpegColorConverter.CmykVector256.cs | 7 +- .../JpegColorConverter.CmykVector512.cs | 7 +- .../JpegColorConverter.GrayScaleScalar.cs | 61 ++++++++++-- .../JpegColorConverter.GrayScaleVector128.cs | 16 ++- .../JpegColorConverter.GrayScaleVector256.cs | 16 ++- .../JpegColorConverter.GrayScaleVector512.cs | 18 +++- .../JpegColorConverter.RgbScalar.cs | 57 ++++++++++- .../JpegColorConverter.RgbVector128.cs | 7 +- .../JpegColorConverter.RgbVector256.cs | 7 +- .../JpegColorConverter.RgbVector512.cs | 7 +- .../JpegColorConverter.YCbCrScalar.cs | 49 ++++++++++ .../JpegColorConverter.YCbCrVector128.cs | 5 + .../JpegColorConverter.YCbCrVector256.cs | 5 + .../JpegColorConverter.YCbCrVector512.cs | 5 + .../JpegColorConverter.YccKScalar.cs | 49 ++++++++++ .../JpegColorConverter.YccKVector128.cs | 7 +- .../JpegColorConverter.YccKVector256.cs | 7 +- .../JpegColorConverter.YccKVector512.cs | 5 + .../ColorConverters/JpegColorConverterBase.cs | 97 +++++++++++++++++++ .../Decoder/ArithmeticScanDecoder.cs | 42 ++++---- .../Components/Decoder/HuffmanScanDecoder.cs | 33 ++++--- .../Components/Decoder/IJpegScanDecoder.cs | 19 ++-- .../Jpeg/Components/Decoder/JpegBitReader.cs | 6 +- .../Components/Decoder/SpectralConverter.cs | 16 ++- .../Decoder/SpectralConverter{TPixel}.cs | 23 +++-- .../Formats/Jpeg/JpegDecoderCore.cs | 12 ++- .../Decompressors/JpegTiffCompression.cs | 13 ++- .../Decompressors/OldJpegTiffCompression.cs | 15 ++- .../Metadata/Profiles/ICC/IccProfile.cs | 2 +- .../Metadata/Profiles/ICC/IccProfileHeader.cs | 40 +++++++- .../Codecs/Jpeg/DecodeJpegParseStreamOnly.cs | 3 +- .../Icc/ColorProfileConverterTests.Icc.cs | 1 + .../ColorProfiles/Icc/TestIccProfiles.cs | 6 ++ .../Formats/Jpg/JpegDecoderTests.cs | 36 ++++++- .../Formats/Jpg/SpectralJpegTests.cs | 3 +- .../Jpg/SpectralToPixelConversionTests.cs | 2 +- .../TestDataIcc/Profiles/issue-129.icc | 3 + .../Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png | 3 + ..._ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png | 3 + ...RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png | 3 + ...de_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png | 3 + 51 files changed, 761 insertions(+), 112 deletions(-) rename src/ImageSharp/ColorProfiles/Icc/{SrgbV4Profile.Generated.cs => CompactSrgbV4Profile.cs} (89%) create mode 100644 tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 23bf85cf3d..fef49bffd4 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -277,11 +277,11 @@ internal static class AotCompilerTools private static void AotCompileSpectralConverter() where TPixel : unmanaged, IPixel { - default(SpectralConverter).GetPixelBuffer(default); - default(GrayJpegSpectralConverter).GetPixelBuffer(default); - default(RgbJpegSpectralConverter).GetPixelBuffer(default); - default(TiffJpegSpectralConverter).GetPixelBuffer(default); - default(TiffOldJpegSpectralConverter).GetPixelBuffer(default); + default(SpectralConverter).GetPixelBuffer(default, default); + default(GrayJpegSpectralConverter).GetPixelBuffer(default, default); + default(RgbJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffOldJpegSpectralConverter).GetPixelBuffer(default, default); } /// diff --git a/src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs b/src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs similarity index 89% rename from src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs rename to src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs index a4d673488e..29e30b53e2 100644 --- a/src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs +++ b/src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs @@ -1,17 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// - using SixLabors.ImageSharp.Metadata.Profiles.Icc; -namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +namespace SixLabors.ImageSharp.ColorProfiles.Icc; -internal static class SrgbV4Profile +internal static class CompactSrgbV4Profile { + private static readonly Lazy LazyIccProfile = new(GetIccProfile); + // Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles - private static ReadOnlySpan Data => new byte[] - { + private static ReadOnlySpan Data => + [ 0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0, 20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171, @@ -29,11 +29,9 @@ internal static class SrgbV4Profile 3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105, 0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91, - }; + ]; - private static readonly Lazy LazyIccProfile = new(() => GetIccProfile()); - - public static IccProfile GetProfile() => LazyIccProfile.Value; + public static IccProfile Profile => LazyIccProfile.Value; private static IccProfile GetIccProfile() { @@ -42,4 +40,3 @@ internal static class SrgbV4Profile return new IccProfile(buffer); } } - diff --git a/src/ImageSharp/Formats/ColorProfileHandling.cs b/src/ImageSharp/Formats/ColorProfileHandling.cs index e6f4b0a6a0..661e6c4bc3 100644 --- a/src/ImageSharp/Formats/ColorProfileHandling.cs +++ b/src/ImageSharp/Formats/ColorProfileHandling.cs @@ -13,9 +13,14 @@ public enum ColorProfileHandling /// Preserve, + /// + /// Removes any embedded Standard sRGB ICC color profiles without transforming the pixels of the image. + /// + Compact, + /// /// Transforms the pixels of the image based on the conversion of any embedded ICC color profiles to sRGB V4 profile. - /// The original profile is then replaced. + /// The original profile is then removed. /// Convert } diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index 8c6b8fc225..8a365682a0 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -62,9 +64,46 @@ public sealed class DecoderOptions /// /// Gets a value that controls how ICC profiles are handled during decode. - /// TODO: Implement this. /// - internal ColorProfileHandling ColorProfileHandling { get; init; } + public ColorProfileHandling ColorProfileHandling { get; init; } internal void SetConfiguration(Configuration configuration) => this.configuration = configuration; + + internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWhen(true)] out IccProfile? value) + { + value = null; + + if (profile is null) + { + return false; + } + + if (IccProfileHeader.IsLikelySrgb(profile.Header)) + { + return false; + } + + if (this.ColorProfileHandling == ColorProfileHandling.Preserve) + { + return false; + } + + value = profile; + return true; + } + + internal bool CanRemoveIccProfile(IccProfile? profile) + { + if (profile is null) + { + return false; + } + + if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header)) + { + return true; + } + + return this.ColorProfileHandling == ColorProfileHandling.Convert; + } } diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index dd148dfedd..c18fc663b5 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -24,6 +24,7 @@ public abstract class ImageDecoder : IImageDecoder s => this.Decode(options, s, default)); this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); return image; } @@ -37,6 +38,7 @@ public abstract class ImageDecoder : IImageDecoder s => this.Decode(options, s, default)); this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); return image; } @@ -52,6 +54,7 @@ public abstract class ImageDecoder : IImageDecoder cancellationToken).ConfigureAwait(false); this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); return image; } @@ -66,6 +69,7 @@ public abstract class ImageDecoder : IImageDecoder cancellationToken).ConfigureAwait(false); this.SetDecoderFormat(options.Configuration, image); + HandleIccProfile(options, image); return image; } @@ -79,6 +83,7 @@ public abstract class ImageDecoder : IImageDecoder s => this.Identify(options, s, default)); this.SetDecoderFormat(options.Configuration, info); + HandleIccProfile(options, info); return info; } @@ -93,6 +98,7 @@ public abstract class ImageDecoder : IImageDecoder cancellationToken).ConfigureAwait(false); this.SetDecoderFormat(options.Configuration, info); + HandleIccProfile(options, info); return info; } @@ -315,4 +321,20 @@ public abstract class ImageDecoder : IImageDecoder } } } + + private static void HandleIccProfile(DecoderOptions options, Image image) + { + if (options.CanRemoveIccProfile(image.Metadata.IccProfile)) + { + image.Metadata.IccProfile = null; + } + } + + private static void HandleIccProfile(DecoderOptions options, ImageInfo image) + { + if (options.CanRemoveIccProfile(image.Metadata.IccProfile)) + { + image.Metadata.IccProfile = null; + } + } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs index 380d3d6cca..ebaa7c4b0b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs @@ -1,6 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -16,6 +23,10 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgbInPlace(values, this.MaximumValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); @@ -75,5 +86,31 @@ internal abstract partial class JpegColorConverterBase k[i] = maxValue - ktmp; } } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + 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; + + PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + 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.CmykVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs index 0a935cca4f..14addafc1d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -46,6 +47,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs index 3cef262ec0..98bda53d20 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -46,6 +47,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs index f57ad43521..c72af2faf2 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -16,6 +17,10 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs index f710b73650..2016cb81db 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs @@ -1,8 +1,13 @@ // 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 SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,21 +22,65 @@ internal abstract partial class JpegColorConverterBase /// public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(values.Component0, this.MaximumValue); + => ConvertToRgbInPlace(in values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgbScalar(values, rLane, gLane, bLane); - internal static void ConvertToRgbInPlace(Span values, float maxValue) + internal static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) + { + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + ref float c0 = ref Unsafe.Add(ref c0Base, i); + c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) { - ref float valuesRef = ref MemoryMarshal.GetReference(values); - float scale = 1 / maxValue; + 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; + + ref float c0Base = ref MemoryMarshal.GetReference(c0); + ref float c1Base = ref MemoryMarshal.GetReference(c1); + ref float c2Base = ref MemoryMarshal.GetReference(c2); - for (nuint i = 0; i < (uint)values.Length; i++) + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) { - Unsafe.Add(ref valuesRef, i) *= scale; + ref float c = ref Unsafe.Add(ref c0Base, i); + c *= scale; } + + Span source = MemoryMarshal.Cast(values.Component0); + 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); } internal static void ConvertFromRgbScalar(in ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs index f3a6f7d372..877988176f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. 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; @@ -17,12 +18,22 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertToRgbInPlace(in ComponentValues values) { ref Vector128 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector128 scale = Vector128.Create(1 / this.MaximumValue); @@ -31,6 +42,9 @@ internal abstract partial class JpegColorConverterBase { ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs index 139ffc549a..6ae20bba89 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -23,6 +24,12 @@ internal abstract partial class JpegColorConverterBase ref Vector256 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector256 scale = Vector256.Create(1 / this.MaximumValue); @@ -31,9 +38,16 @@ internal abstract partial class JpegColorConverterBase { ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.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.GrayScaleVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs index 21d5eaa6f8..f56093df99 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,12 +18,22 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) { ref Vector512 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector512 scale = Vector512.Create(1 / this.MaximumValue); @@ -31,6 +42,9 @@ internal abstract partial class JpegColorConverterBase { ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } @@ -66,7 +80,7 @@ internal abstract partial class JpegColorConverterBase /// protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => GrayScaleScalar.ConvertToRgbInPlace(values.Component0, this.MaximumValue); + => GrayScaleScalar.ConvertToRgbInPlace(in values, this.MaximumValue); /// protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs index 23825b06e3..770709d7f9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs @@ -1,6 +1,14 @@ // 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 SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -16,15 +24,58 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgbInPlace(values, this.MaximumValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, rLane, gLane, bLane); + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + 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; + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + 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); + } + internal static void ConvertToRgbInPlace(ComponentValues values, float maxValue) { - GrayScaleScalar.ConvertToRgbInPlace(values.Component0, maxValue); - GrayScaleScalar.ConvertToRgbInPlace(values.Component1, maxValue); - GrayScaleScalar.ConvertToRgbInPlace(values.Component2, maxValue); + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + ref float c0 = ref Unsafe.Add(ref c0Base, i); + c0 *= scale; + + ref float c1 = ref Unsafe.Add(ref c1Base, i); + c1 *= scale; + + ref float c2 = ref Unsafe.Add(ref c2Base, i); + c2 *= scale; + } } internal static void ConvertFromRgb(ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs index 47aa4281b1..6cbbc7c7cf 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -40,6 +41,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.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.RgbVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs index 02448d724b..10bc2be5f8 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -40,6 +41,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.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.RgbVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs index 76745f6654..6e01ad7cb0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -40,6 +41,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs index e514a01663..3fca870522 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs @@ -1,6 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -22,6 +29,10 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); @@ -49,6 +60,44 @@ internal abstract partial class JpegColorConverterBase } } + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + 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; + + // Although YCbCr is a defined ICC color space, in practice ICC profiles + // do not implement transforms from it. + // Therefore, we first convert JPEG YCbCr to RGB manually, then perform + // color-managed conversion to the target profile. + + // TODO: The initial YCbCr => RGB conversion is assumed to be in the sRGB working space. + // To perform accurate colorimetric conversion via XYZ, we should derive the working space + // from the source ICC profile (e.g., via header/tags). + // This is a placeholder until that logic is implemented. + ColorProfileConverter converter = new(); + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed); + + converter.Convert(source, destination); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new(options); + converter.Convert(destination, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + public static void ConvertFromRgb(in ComponentValues values, float halfValue, Span rLane, Span gLane, Span bLane) { Span y = values.Component0; diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs index 8cecd39562..9bce944fbe 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector128_ = SixLabors.ImageSharp.Common.Helpers.Vector128Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -66,6 +67,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.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.YCbCrVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs index f8517e0867..079a6de2d1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -66,6 +67,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.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.YCbCrVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs index 7598a64b2b..637c6ef1e4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,6 +18,10 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs index bb545ec76b..14a3656781 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs @@ -1,6 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -22,6 +29,10 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgpInPlace(values, this.MaximumValue, this.HalfValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); @@ -73,5 +84,43 @@ internal abstract partial class JpegColorConverterBase y[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); } } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + 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; + + PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + ColorProfileConverter converter = new(); + Span source = MemoryMarshal.Cast(packed); + + // YccK is not a defined ICC color space — it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. + // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. + // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. + // + // TODO: The intermediate YccK => RGB step assumes a working space with sRGB primaries and D65 white point. + // To perform accurate colorimetric conversion via XYZ, we should derive the working space + // from the source ICC profile (e.g., via header/tags). + // This is a placeholder until that logic is implemented. + converter.Convert(MemoryMarshal.Cast(source), source); + + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + 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.YccKVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs index 5bb2c5e5b9..bca7d71145 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector128_ = SixLabors.ImageSharp.Common.Helpers.Vector128Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.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.YccKVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs index 27f2ce035a..3c19112046 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.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.YccKVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs index 42d89a2314..b4ddfc0466 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) => YccKScalar.ConvertToRgpInPlace(values, this.MaximumValue, this.HalfValue); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs index 8cb3045dc9..c7cc8e9713 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs @@ -2,7 +2,11 @@ // Licensed under the Six Labors Split License. #nullable disable +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -80,6 +84,14 @@ 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. /// @@ -89,6 +101,91 @@ internal abstract partial class JpegColorConverterBase /// Blue colors lane. public abstract void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane); + public static void PackedNormalizeInterleave3( + ReadOnlySpan xLane, + ReadOnlySpan yLane, + ReadOnlySpan zLane, + Span packed, + float scale) + { + DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed)); + + // TODO: Investigate SIMD version of this. + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint baseIdx = i * 3; + Unsafe.Add(ref packedRef, baseIdx) = Unsafe.Add(ref xLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 1) = Unsafe.Add(ref yLaneRef, i) * scale; + Unsafe.Add(ref packedRef, baseIdx + 2) = Unsafe.Add(ref zLaneRef, i) * scale; + } + } + + public static void UnpackDeinterleave3( + ReadOnlySpan packed, + Span xLane, + Span yLane, + Span zLane) + { + DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!"); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + + // TODO: Investigate SIMD version of this. + ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(packed)); + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + + for (nuint i = 0; i < (nuint)packed.Length; i++) + { + nuint baseIdx = i * 3; + Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx); + Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 1); + Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 2); + } + } + + public static void PackedInvertNormalizeInterleave4( + ReadOnlySpan xLane, + ReadOnlySpan yLane, + ReadOnlySpan zLane, + ReadOnlySpan wLane, + Span packed, + float maxValue) + { + DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); + + float scale = 1F / maxValue; + + // TODO: Investigate SIMD version of this. + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint baseIdx = i * 4; + Unsafe.Add(ref packedRef, baseIdx) = (maxValue - Unsafe.Add(ref xLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 1) = (maxValue - Unsafe.Add(ref yLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 2) = (maxValue - Unsafe.Add(ref zLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, baseIdx + 3) = (maxValue - Unsafe.Add(ref wLaneRef, i)) * scale; + } + } + /// /// Returns the s for all supported color spaces and precisions. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index 02a346ff07..6e83f5b2b4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -54,12 +55,12 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder private ArithmeticDecodingTable[] acDecodingTables; // Don't make this a ReadOnlySpan, as the values need to get updated. - private readonly byte[] fixedBin = { 113, 0, 0, 0 }; + private readonly byte[] fixedBin = [113, 0, 0, 0]; private readonly CancellationToken cancellationToken; private static readonly int[] ArithmeticTable = - { + [ Pack(0x5a1d, 1, 1, 1), Pack(0x2586, 14, 2, 0), Pack(0x1114, 16, 3, 0), @@ -177,9 +178,9 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder // This last entry is used for fixed probability estimate of 0.5 // as suggested in Section 10.3 Table 5 of ITU-T Rec. T.851. Pack(0x5a1d, 113, 113, 0) - }; + ]; - private readonly List statistics = new(); + private readonly List statistics = []; /// /// Initializes a new instance of the class. @@ -234,11 +235,8 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder private ref byte GetFixedBinReference() => ref MemoryMarshal.GetArrayDataReference(this.fixedBin); - /// - /// Decodes the entropy coded data. - /// - /// Component count in the current scan. - public void ParseEntropyCodedData(int scanComponentCount) + /// + public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -254,7 +252,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder } else { - this.ParseBaselineData(); + this.ParseBaselineData(iccProfile); } if (this.scanBuffer.HasBadMarker()) @@ -310,7 +308,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder return statistic; } - private void ParseBaselineData() + private void ParseBaselineData(IccProfile iccProfile) { for (int i = 0; i < this.components.Length; i++) { @@ -326,13 +324,13 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder if (this.scanComponentCount != 1) { this.spectralConverter.PrepareForDecoding(); - this.ParseBaselineDataInterleaved(); + this.ParseBaselineDataInterleaved(iccProfile); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { this.spectralConverter.PrepareForDecoding(); - this.ParseBaselineDataSingleComponent(); + this.ParseBaselineDataSingleComponent(iccProfile); this.spectralConverter.CommitConversion(); } else @@ -345,8 +343,9 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder { this.CheckProgressiveData(); - foreach (ArithmeticDecodingComponent component in this.components) + for (int i = 0; i < this.components.Length; i++) { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[i]; if (this.SpectralStart == 0 && this.SuccessiveHigh == 0) { component.DcPredictor = 0; @@ -422,7 +421,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder } } - private void ParseBaselineDataInterleaved() + private void ParseBaselineDataInterleaved(IccProfile iccProfile) { int mcu = 0; int mcusPerColumn = this.frame.McusPerColumn; @@ -463,7 +462,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder { // It is very likely that some spectral data was decoded before we've encountered 'end of scan' // so we need to decode what's left and return (or maybe throw?) - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); return; } @@ -485,11 +484,11 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder } // Convert from spectral to actual pixels via given converter. - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); } } - private void ParseBaselineDataSingleComponent() + private void ParseBaselineDataSingleComponent(IccProfile iccProfile) { ArithmeticDecodingComponent component = this.frame.Components[0] as ArithmeticDecodingComponent; int mcuLines = this.frame.McusPerColumn; @@ -516,7 +515,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder { // It is very likely that some spectral data was decoded before we've encountered 'end of scan' // so we need to decode what's left and return (or maybe throw?) - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); return; } @@ -531,7 +530,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder } // Convert from spectral to actual pixels via given converter. - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); } } @@ -1108,8 +1107,9 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder this.todo = this.restartInterval; - foreach (ArithmeticDecodingComponent component in this.components) + for (int i = 0; i < this.components.Length; i++) { + ArithmeticDecodingComponent component = (ArithmeticDecodingComponent)this.components[i]; component.DcPredictor = 0; component.DcContext = 0; component.DcStatistics?.Reset(); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 56e0f1e985..9ee43a2c83 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -109,7 +110,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder public int SuccessiveLow { get; set; } /// - public void ParseEntropyCodedData(int scanComponentCount) + public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -123,7 +124,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder if (!this.frame.Progressive) { - this.ParseBaselineData(); + this.ParseBaselineData(iccProfile); } else { @@ -145,18 +146,18 @@ internal class HuffmanScanDecoder : IJpegScanDecoder this.spectralConverter.InjectFrameData(frame, jpegData); } - private void ParseBaselineData() + private void ParseBaselineData(IccProfile iccProfile) { if (this.scanComponentCount != 1) { this.spectralConverter.PrepareForDecoding(); - this.ParseBaselineDataInterleaved(); + this.ParseBaselineDataInterleaved(iccProfile); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { this.spectralConverter.PrepareForDecoding(); - this.ParseBaselineDataSingleComponent(); + this.ParseBaselineDataSingleComponent(iccProfile); this.spectralConverter.CommitConversion(); } else @@ -165,7 +166,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder } } - private void ParseBaselineDataInterleaved() + private void ParseBaselineDataInterleaved(IccProfile iccProfile) { int mcu = 0; int mcusPerColumn = this.frame.McusPerColumn; @@ -184,7 +185,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder for (int k = 0; k < this.scanComponentCount; k++) { int order = this.frame.ComponentOrder[k]; - var component = this.components[order] as JpegComponent; + JpegComponent component = this.components[order] as JpegComponent; ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; @@ -205,7 +206,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder { // It is very likely that some spectral data was decoded before we've encountered 'end of scan' // so we need to decode what's left and return (or maybe throw?) - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); return; } @@ -227,13 +228,13 @@ internal class HuffmanScanDecoder : IJpegScanDecoder } // Convert from spectral to actual pixels via given converter - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); } } private void ParseBaselineDataNonInterleaved() { - var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + JpegComponent component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; ref JpegBitReader buffer = ref this.scanBuffer; int w = component.WidthInBlocks; @@ -266,7 +267,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder } } - private void ParseBaselineDataSingleComponent() + private void ParseBaselineDataSingleComponent(IccProfile iccProfile) { JpegComponent component = this.frame.Components[0]; int mcuLines = this.frame.McusPerColumn; @@ -293,7 +294,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder { // It is very likely that some spectral data was decoded before we've encountered 'end of scan' // so we need to decode what's left and return (or maybe throw?) - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); return; } @@ -308,7 +309,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder } // Convert from spectral to actual pixels via given converter - this.spectralConverter.ConvertStrideBaseline(); + this.spectralConverter.ConvertStrideBaseline(iccProfile); } } @@ -394,7 +395,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder for (int k = 0; k < this.scanComponentCount; k++) { int order = this.frame.ComponentOrder[k]; - var component = this.components[order] as JpegComponent; + JpegComponent component = this.components[order] as JpegComponent; ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; int h = component.HorizontalSamplingFactor; @@ -435,7 +436,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder private void ParseProgressiveDataNonInterleaved() { - var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + JpegComponent component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; ref JpegBitReader buffer = ref this.scanBuffer; int w = component.WidthInBlocks; @@ -772,7 +773,7 @@ internal class HuffmanScanDecoder : IJpegScanDecoder } /// - /// Build the huffman table using code lengths and code values. + /// Build the Huffman table using code lengths and code values. /// /// Table type. /// Table index. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs index ecec723a98..588090deb6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; /// @@ -11,38 +13,41 @@ internal interface IJpegScanDecoder /// /// Sets the reset interval. /// - int ResetInterval { set; } + public int ResetInterval { set; } /// /// Gets or sets the spectral selection start. /// - int SpectralStart { get; set; } + public int SpectralStart { get; set; } /// /// Gets or sets the spectral selection end. /// - int SpectralEnd { get; set; } + public int SpectralEnd { get; set; } /// /// Gets or sets the successive approximation high bit end. /// - int SuccessiveHigh { get; set; } + public int SuccessiveHigh { get; set; } /// /// Gets or sets the successive approximation low bit end. /// - int SuccessiveLow { get; set; } + public int SuccessiveLow { get; set; } /// /// Decodes the entropy coded data. /// /// Component count in the current scan. - void ParseEntropyCodedData(int scanComponentCount); + /// + /// The ICC profile to use for color conversion. If null, the default color space. + /// + public void ParseEntropyCodedData(int scanComponentCount, IccProfile? iccProfile); /// /// Sets the JpegFrame and its components and injects the frame data into the spectral converter. /// /// The frame. /// The raw JPEG data. - void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs index e71d86a1d9..f888a8fb7b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs @@ -76,13 +76,13 @@ internal struct JpegBitReader /// Whether a RST marker has been detected, I.E. One that is between RST0 and RST7 /// [MethodImpl(InliningOptions.ShortMethod)] - public bool HasRestartMarker() => HasRestart(this.Marker); + public readonly bool HasRestartMarker() => HasRestart(this.Marker); /// /// Whether a bad marker has been detected, I.E. One that is not between RST0 and RST7 /// [MethodImpl(InliningOptions.ShortMethod)] - public bool HasBadMarker() => this.Marker != JpegConstants.Markers.XFF && !this.HasRestartMarker(); + public readonly bool HasBadMarker() => this.Marker != JpegConstants.Markers.XFF && !this.HasRestartMarker(); [MethodImpl(InliningOptions.AlwaysInline)] public void FillBuffer() @@ -132,7 +132,7 @@ internal struct JpegBitReader public int GetBits(int nbits) => (int)ExtractBits(this.data, this.remainingBits -= nbits, nbits); [MethodImpl(InliningOptions.ShortMethod)] - public int PeekBits(int nbits) => (int)ExtractBits(this.data, this.remainingBits - nbits, nbits); + public readonly int PeekBits(int nbits) => (int)ExtractBits(this.data, this.remainingBits - nbits, nbits); [MethodImpl(InliningOptions.AlwaysInline)] private static ulong ExtractBits(ulong value, int offset, int size) => (value >> offset) & (ulong)((1 << size) - 1); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs index 51d9bfbced..e652961991 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; /// @@ -11,8 +13,9 @@ internal abstract class SpectralConverter /// /// Supported scaled spectral block sizes for scaled IDCT decoding. /// - private static readonly int[] ScaledBlockSizes = new int[] - { + private static readonly int[] ScaledBlockSizes = + [ + // 8 => 1, 1/8 of the original size 1, @@ -21,7 +24,7 @@ internal abstract class SpectralConverter // 8 => 4, 1/2 of the original size 4, - }; + ]; /// /// Gets a value indicating whether this converter has converted spectral @@ -50,13 +53,16 @@ internal abstract class SpectralConverter /// Converts single spectral jpeg stride to color stride in baseline /// decoding mode. /// + /// + /// The ICC profile to use for color conversion. If , then the default color space is used. + /// /// /// Called once per decoded spectral stride in /// only for baseline interleaved jpeg images. /// Spectral 'stride' doesn't particularly mean 'single stride'. /// Actual stride height depends on the subsampling factor of the given image. /// - public abstract void ConvertStrideBaseline(); + public abstract void ConvertStrideBaseline(IccProfile? iccProfile); /// /// Marks current converter state as 'converted'. @@ -83,7 +89,7 @@ internal abstract class SpectralConverter /// Calculates image size with optional scaling. /// /// - /// Does not apply scalling if is null. + /// Does not apply scaling if is null. /// /// Size of the image. /// Target size of the image. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 561d273e6d..655665e29d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -98,9 +99,10 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// For non-baseline interleaved jpeg this method does a 'lazy' spectral /// conversion from spectral to color. /// + /// Optional ICC profile for color conversion. /// Cancellation token. /// Pixel buffer. - public Buffer2D GetPixelBuffer(CancellationToken cancellationToken) + public Buffer2D GetPixelBuffer(IccProfile iccProfile, CancellationToken cancellationToken) { if (!this.Converted) { @@ -111,7 +113,7 @@ internal class SpectralConverter : SpectralConverter, IDisposable for (int step = 0; step < steps; step++) { cancellationToken.ThrowIfCancellationRequested(); - this.ConvertStride(step); + this.ConvertStride(step, iccProfile); } } @@ -124,7 +126,8 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// Converts single spectral jpeg stride to color stride. /// /// Spectral stride index. - private void ConvertStride(int spectralStep) + /// Optional ICC profile for color conversion. + private void ConvertStride(int spectralStep, IccProfile iccProfile) { int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); @@ -141,9 +144,17 @@ internal class SpectralConverter : SpectralConverter, IDisposable JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); - this.colorConverter.ConvertToRgbInPlace(values); values = values.Slice(0, width); // slice away Jpeg padding + if (iccProfile != null) + { + this.colorConverter.ConvertToRgbInPlaceWithIcc(this.Configuration, in values, iccProfile); + } + else + { + this.colorConverter.ConvertToRgbInPlace(in values); + } + Span r = this.rgbBuffer.Slice(0, width); Span g = this.rgbBuffer.Slice(width, width); Span b = this.rgbBuffer.Slice(width * 2, width); @@ -222,11 +233,11 @@ internal class SpectralConverter : SpectralConverter, IDisposable } /// - public override void ConvertStrideBaseline() + public override void ConvertStrideBaseline(IccProfile iccProfile) { // Convert next pixel stride using single spectral `stride' // Note that zero passing eliminates extra virtual call - this.ConvertStride(spectralStep: 0); + this.ConvertStride(spectralStep: 0, iccProfile); foreach (ComponentProcessor cpp in this.componentProcessors) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 707baa1a88..9198a5239a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -127,7 +127,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData /// Gets the only supported precisions /// // Refers to assembly's static data segment, no allocation occurs. - private static ReadOnlySpan SupportedPrecisions => new byte[] { 8, 12 }; + private static ReadOnlySpan SupportedPrecisions => [8, 12]; /// /// Gets the frame @@ -201,9 +201,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData this.InitXmpProfile(); this.InitDerivedMetadataProperties(); + _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); + return new Image( this.configuration, - spectralConverter.GetPixelBuffer(cancellationToken), + spectralConverter.GetPixelBuffer(profile, cancellationToken), this.Metadata); } @@ -666,7 +668,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData /// private void InitIccProfile() { - if (this.hasIcc) + if (this.hasIcc && this.Metadata.IccProfile == null) { IccProfile profile = new(this.iccData); if (profile.CheckIsValid()) @@ -1512,7 +1514,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables); } - this.scanDecoder.ParseEntropyCodedData(selectorsCount); + this.InitIccProfile(); + _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); + this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile); } /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index 82b26232af..1df55b8b5f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; @@ -73,7 +74,11 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); - using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken); + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(profile, cancellationToken); JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); break; } @@ -87,7 +92,11 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor jpegDecoder.LoadTables(this.jpegTables, scanDecoder); jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); - using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken); + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(profile, cancellationToken); JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer); break; } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs index b58183ff60..13257dd63a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; @@ -57,7 +58,13 @@ internal sealed class OldJpegTiffCompression : TiffBaseDecompressor jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); - using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken); + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer( + profile, + cancellationToken); JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); break; } @@ -69,7 +76,11 @@ internal sealed class OldJpegTiffCompression : TiffBaseDecompressor jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); - using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken); + _ = this.options.GeneralOptions.TryGetIccProfileForColorConversion( + jpegDecoder.Metadata?.IccProfile, + out IccProfile? profile); + + using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(profile, cancellationToken); JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer); break; } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index da015b2b07..392ccb3062 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -202,7 +202,7 @@ public sealed class IccProfile : IDeepCloneable if (this.data is null) { - this.entries = Array.Empty(); + this.entries = []; return; } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs index 4da5a6aa56..6f9b8167e4 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. #nullable disable @@ -11,6 +11,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; /// public sealed class IccProfileHeader { + private static readonly Vector3 TruncatedD50 = new(0.9642029F, 1F, 0.8249054F); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D); + + // sRGB v4 Appearance + private static readonly IccProfileId StandardRgbV4A = new(0xDF1132A1, 0x746E97B0, 0xAD85719, 0xBE711E08); + /// /// Gets or sets the profile size in bytes (will be ignored when writing a profile). /// @@ -97,4 +108,31 @@ public sealed class IccProfileHeader /// Gets or sets the profile ID (hash). /// public IccProfileId Id { get; set; } + + internal static bool IsLikelySrgb(IccProfileHeader header) + { + // Reject known perceptual-appearance profile + // This profile employs perceptual rendering intents to maintain color appearance across different + // devices and media, which can lead to variations from standard sRGB representations. + if (header.Id == StandardRgbV4A) + { + return false; + } + + // Accept known sRGB profile IDs + if (header.Id == StandardRgbV2 || header.Id == StandardRgbV4) + { + return true; + } + + // Fallback: best-guess heuristic + return + header.FileSignature == "acsp" && + header.DataColorSpace == IccColorSpaceType.Rgb && + (header.ProfileConnectionSpace == IccColorSpaceType.CieXyz || header.ProfileConnectionSpace == IccColorSpaceType.CieLab) && + (header.Class == IccProfileClass.DisplayDevice || header.Class == IccProfileClass.ColorSpace) && + header.PcsIlluminant == TruncatedD50 && + (header.Version.Major == 2 || header.Version.Major == 4) && + !string.Equals(header.CmmType, "ADBE", StringComparison.Ordinal); + } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 5f6dbbdf72..91ee821362 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -5,6 +5,7 @@ using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Tests; using SDSize = System.Drawing.Size; @@ -50,7 +51,7 @@ public class DecodeJpegParseStreamOnly // There's no way to eliminate it as spectral conversion is built into the scan decoding loop for memory footprint reduction private sealed class NoopSpectralConverter : SpectralConverter { - public override void ConvertStrideBaseline() + public override void ConvertStrideBaseline(IccProfile iccProfile) { } diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index 7e01a629bc..6c56dc682d 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs @@ -62,6 +62,7 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper) [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) + [InlineData(TestIccProfiles.Issue129, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> -> XYZ -> RGB public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005) { List actual = GetBulkActualTargetValues(Inputs, sourceProfile, targetProfile); diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs index 3fc36a91b0..3e3bb4d498 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs @@ -16,6 +16,7 @@ internal static class TestIccProfiles /// v2 CMYK -> LAB, output, lut16 /// public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; + /// /// v2 CMYK -> LAB, output, lut16 /// @@ -46,6 +47,11 @@ internal static class TestIccProfiles /// public const string StandardRgbV4 = "sRGB_v4_ICC_preference.icc"; + /// + /// v2 CMYK -> LAB, output + /// + public const string Issue129 = "issue-129.icc"; + /// /// v2 RGB -> XYZ, display, TRCs /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 950265bd56..4b46829201 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -365,4 +364,39 @@ public partial class JpegDecoderTests image.DebugSave(provider); image.CompareToOriginal(provider); } + + [Theory] + [WithFile(TestImages.Jpeg.ICC.CMYK, PixelTypes.Rgba32)] + public void Decode_CMYK_ICC_Jpeg(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestImages.Jpeg.ICC.SRgb, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.AdobeRgb, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.ColorMatch, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.ProPhoto, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.WideRGB, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.AppleRGB, PixelTypes.Rgba32)] + public void Decode_RGB_ICC_Jpeg(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 805ee586a8..499cf79916 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; using Xunit.Abstractions; @@ -164,7 +165,7 @@ public class SpectralJpegTests } } - public override void ConvertStrideBaseline() + public override void ConvertStrideBaseline(IccProfile iccProfile) { // This would be called only for baseline non-interleaved images // We must copy spectral strides here diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs index 25929182fb..b9f4a90b6d 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs @@ -48,7 +48,7 @@ public class SpectralToPixelConversionTests provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; // Comparison - using var image = new Image(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata()); + using var image = new Image(Configuration.Default, converter.GetPixelBuffer(null, CancellationToken.None), new ImageMetadata()); using Image referenceImage = provider.GetReferenceOutputImage(appendPixelTypeToFileName: false); ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); diff --git a/tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc b/tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc new file mode 100644 index 0000000000..9dee961653 --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35f401731df11a4eba3502af632e51d68bc394bcb7d34632a331c1ba3f4a0bf6 +size 557168 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png new file mode 100644 index 0000000000..77a9d0d9cb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215cba73dfb0e19f75f6dc0c3fefca474bd65f57684a207a11d896e1637bb643 +size 1240827 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png new file mode 100644 index 0000000000..0963e90b74 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c942b534baa51b8e46e88bd38d1ced319bccf1b55a5711ae5761697b7437fe4e +size 458321 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png new file mode 100644 index 0000000000..1e5eaca4da --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:163711226bdfa2f102b314435baea9f69ad1be1b11ef5ad8348358cd09a029ae +size 482963 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png new file mode 100644 index 0000000000..06bee00d7a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f7981f40bab5bffff3e7c9ea1676d224c173fabbaa6e7a920d7a9dabd58655f +size 464917 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png new file mode 100644 index 0000000000..3ae12c657a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11b02b982c024e295915e88f56a428ad73068217a7ae625f705127ab8c35a4bf +size 477061 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png new file mode 100644 index 0000000000..0a2eb91cc5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a49967c20cccf824df4de3f105f5ddb44d7a602c072ce22caa38939f21f62505 +size 469827 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png new file mode 100644 index 0000000000..fa554484bb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b33fc8fd03142aaaf8aabc39e084acd9e82e9222292e281953d92d65edcc1a7 +size 436111