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/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs index 44c7d2ac77..882d246a76 100644 --- a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs +++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.ColorProfiles; public class ColorConversionOptions { private Matrix4x4 adaptationMatrix; - private YCbCrMatrix yCbCrMatrix; + private YCbCrTransform yCbCrTransform; /// /// Initializes a new instance of the class. @@ -22,7 +22,7 @@ public class ColorConversionOptions public ColorConversionOptions() { this.AdaptationMatrix = KnownChromaticAdaptationMatrices.Bradford; - this.YCbCrMatrix = KnownYCbCrMatrices.BT601; + this.YCbCrTransform = KnownYCbCrMatrices.BT601; } /// @@ -53,13 +53,13 @@ public class ColorConversionOptions /// /// Gets the YCbCr matrix to used to perform conversions from/to RGB. /// - public YCbCrMatrix YCbCrMatrix + public YCbCrTransform YCbCrTransform { - get => this.yCbCrMatrix; + get => this.yCbCrTransform; init { - this.yCbCrMatrix = value; - this.TransposedYCbCrMatrix = value.Transpose(); + this.yCbCrTransform = value; + this.TransposedYCbCrTransform = value.Transpose(); } } @@ -88,7 +88,7 @@ public class ColorConversionOptions } } - internal YCbCrMatrix TransposedYCbCrMatrix { get; private set; } + internal YCbCrTransform TransposedYCbCrTransform { get; private set; } internal Matrix4x4 InverseAdaptationMatrix { get; private set; } } 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/ColorProfiles/KnownYCbCrMatrices.cs b/src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs index e2b7bf1026..d32833a382 100644 --- a/src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs +++ b/src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs @@ -15,7 +15,7 @@ public static class KnownYCbCrMatrices /// /// ITU-R BT.601 (SD video standard). /// - public static readonly YCbCrMatrix BT601 = new( + public static readonly YCbCrTransform BT601 = new( new Matrix4x4( 0.299000F, 0.587000F, 0.114000F, 0F, -0.168736F, -0.331264F, 0.500000F, 0F, @@ -31,7 +31,7 @@ public static class KnownYCbCrMatrices /// /// ITU-R BT.709 (HD video, sRGB standard). /// - public static readonly YCbCrMatrix BT709 = new( + public static readonly YCbCrTransform BT709 = new( new Matrix4x4( 0.212600F, 0.715200F, 0.072200F, 0F, -0.114572F, -0.385428F, 0.500000F, 0F, @@ -47,7 +47,7 @@ public static class KnownYCbCrMatrices /// /// ITU-R BT.2020 (UHD/4K video standard). /// - public static readonly YCbCrMatrix BT2020 = new( + public static readonly YCbCrTransform BT2020 = new( new Matrix4x4( 0.262700F, 0.678000F, 0.059300F, 0F, -0.139630F, -0.360370F, 0.500000F, 0F, diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 9b95278bc9..42e502592c 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -98,7 +98,7 @@ public readonly struct Rgb : IProfileConnectingSpace /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector4 ToScaledVector4() - => new(this.ToScaledVector3(), 1F); + => new(this.AsVector3Unsafe(), 1F); /// public static void ToScaledVector4(ReadOnlySpan source, Span destination) @@ -154,7 +154,7 @@ public readonly struct Rgb : IProfileConnectingSpace Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(this.ToScaledVector4())); // Then convert to xyz - return new CieXyz(Vector3.Transform(linear.ToScaledVector3(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace))); + return new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), GetRgbToCieXyzMatrix(options.SourceRgbWorkingSpace))); } /// @@ -171,7 +171,7 @@ public readonly struct Rgb : IProfileConnectingSpace Rgb linear = FromScaledVector4(options.SourceRgbWorkingSpace.Expand(rgb.ToScaledVector4())); // Then convert to xyz - destination[i] = new CieXyz(Vector3.Transform(linear.ToScaledVector3(), matrix)); + destination[i] = new CieXyz(Vector3.Transform(linear.AsVector3Unsafe(), matrix)); } } diff --git a/src/ImageSharp/ColorProfiles/Y.cs b/src/ImageSharp/ColorProfiles/Y.cs index 960bf46991..83321a0851 100644 --- a/src/ImageSharp/ColorProfiles/Y.cs +++ b/src/ImageSharp/ColorProfiles/Y.cs @@ -90,8 +90,8 @@ public readonly struct Y : IColorProfile /// public static Y FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) { - Matrix4x4 m = options.YCbCrMatrix.Forward; - float offset = options.YCbCrMatrix.Offset.X; + Matrix4x4 m = options.YCbCrTransform.Forward; + float offset = options.YCbCrTransform.Offset.X; return new(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset); } diff --git a/src/ImageSharp/ColorProfiles/YCbCr.cs b/src/ImageSharp/ColorProfiles/YCbCr.cs index 8e7dc57d74..22d629373b 100644 --- a/src/ImageSharp/ColorProfiles/YCbCr.cs +++ b/src/ImageSharp/ColorProfiles/YCbCr.cs @@ -130,8 +130,8 @@ public readonly struct YCbCr : IColorProfile public static YCbCr FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) { Vector3 rgb = source.AsVector3Unsafe(); - Matrix4x4 m = options.TransposedYCbCrMatrix.Forward; - Vector3 offset = options.TransposedYCbCrMatrix.Offset; + Matrix4x4 m = options.TransposedYCbCrTransform.Forward; + Vector3 offset = options.TransposedYCbCrTransform.Offset; return new YCbCr(Vector3.Transform(rgb, m) + offset, true); } @@ -152,8 +152,8 @@ public readonly struct YCbCr : IColorProfile /// public Rgb ToProfileConnectingSpace(ColorConversionOptions options) { - Matrix4x4 m = options.TransposedYCbCrMatrix.Inverse; - Vector3 offset = options.TransposedYCbCrMatrix.Offset; + Matrix4x4 m = options.TransposedYCbCrTransform.Inverse; + Vector3 offset = options.TransposedYCbCrTransform.Offset; Vector3 normalized = this.AsVector3Unsafe() - offset; return Rgb.FromScaledVector3(Vector3.Transform(normalized, m)); diff --git a/src/ImageSharp/ColorProfiles/YcbCrMatrix.cs b/src/ImageSharp/ColorProfiles/YCbCrTransform.cs similarity index 85% rename from src/ImageSharp/ColorProfiles/YcbCrMatrix.cs rename to src/ImageSharp/ColorProfiles/YCbCrTransform.cs index ccb4ea9861..c90b90708d 100644 --- a/src/ImageSharp/ColorProfiles/YcbCrMatrix.cs +++ b/src/ImageSharp/ColorProfiles/YCbCrTransform.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.ColorProfiles; /// /// -/// Represents a YCbCr color matrix containing forward and inverse transformation matrices, +/// Represents a YCbCr color transform containing forward and inverse transformation matrices, /// and the chrominance offsets to apply for full-range encoding /// /// @@ -17,10 +17,10 @@ namespace SixLabors.ImageSharp.ColorProfiles; /// working spaces will produce incorrect conversions. /// /// -public readonly struct YCbCrMatrix +public readonly struct YCbCrTransform { /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// /// The forward transformation matrix from RGB to YCbCr. The matrix must include the @@ -34,7 +34,7 @@ public readonly struct YCbCrMatrix /// The chrominance offsets to be added after the forward conversion, /// and subtracted before the inverse conversion. Usually (0, 0.5, 0.5). /// - public YCbCrMatrix(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset) + public YCbCrTransform(Matrix4x4 forward, Matrix4x4 inverse, Vector3 offset) { this.Forward = forward; this.Inverse = inverse; @@ -56,6 +56,6 @@ public readonly struct YCbCrMatrix /// public Vector3 Offset { get; } - internal YCbCrMatrix Transpose() + internal YCbCrTransform Transpose() => new(Matrix4x4.Transpose(this.Forward), Matrix4x4.Transpose(this.Inverse), this.Offset); } diff --git a/src/ImageSharp/ColorProfiles/YccK.cs b/src/ImageSharp/ColorProfiles/YccK.cs index f05e1431b2..df5eb48947 100644 --- a/src/ImageSharp/ColorProfiles/YccK.cs +++ b/src/ImageSharp/ColorProfiles/YccK.cs @@ -131,8 +131,8 @@ public readonly struct YccK : IColorProfile /// public Rgb ToProfileConnectingSpace(ColorConversionOptions options) { - Matrix4x4 m = options.TransposedYCbCrMatrix.Inverse; - Vector3 offset = options.TransposedYCbCrMatrix.Offset; + Matrix4x4 m = options.TransposedYCbCrTransform.Inverse; + Vector3 offset = options.TransposedYCbCrTransform.Offset; Vector3 normalized = this.AsVector3Unsafe() - offset; return Rgb.FromScaledVector3(Vector3.Transform(normalized, m) * (1F - this.K)); @@ -141,8 +141,8 @@ public readonly struct YccK : IColorProfile /// public static YccK FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source) { - Matrix4x4 m = options.TransposedYCbCrMatrix.Forward; - Vector3 offset = options.TransposedYCbCrMatrix.Offset; + Matrix4x4 m = options.TransposedYCbCrTransform.Forward; + Vector3 offset = options.TransposedYCbCrTransform.Offset; Vector3 rgb = source.AsVector3Unsafe(); float k = 1F - MathF.Max(rgb.X, MathF.Max(rgb.Y, rgb.Z)); 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..74869c93ca 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++) + { + float c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; + } + } + + 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 4b350f6f38..633080706b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs @@ -5,6 +5,7 @@ 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,20 +18,33 @@ 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); nuint n = values.Component0.Vector128Count(); for (nuint i = 0; i < n; i++) { - ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); - c0 *= scale; + Vector128 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs index 94b897e075..0b2b8a119f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs @@ -5,6 +5,7 @@ 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; @@ -23,17 +24,30 @@ 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); nuint n = values.Component0.Vector256Count(); for (nuint i = 0; i < n; i++) { - ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); - c0 *= scale; + Vector256 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; } } + /// + 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 638f4278b6..0a58b0196b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs @@ -5,6 +5,7 @@ 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,20 +18,33 @@ 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); nuint n = values.Component0.Vector512Count(); for (nuint i = 0; i < n; i++) { - ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); - c0 *= scale; + Vector512 c = Unsafe.Add(ref c0Base, i) * scale; + + Unsafe.Add(ref c0Base, i) = c; + Unsafe.Add(ref c1Base, i) = c; + Unsafe.Add(ref c2Base, i) = c; } } @@ -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..4b2abc0ac5 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,53 @@ 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++) + { + Unsafe.Add(ref c0Base, i) *= scale; + Unsafe.Add(ref c1Base, i) *= scale; + Unsafe.Add(ref c2Base, i) *= 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..4fa18b88ab 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,43 @@ 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. + // + // The YCbCr => RGB conversion is based on BT.601 and is independent of any embedded ICC profile. + // Since the same RGB working space is used during conversion to and from XYZ, + // colorimetric accuracy is preserved. + 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 6eabb3ee0e..01c8508edc 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs @@ -5,6 +5,7 @@ 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; @@ -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 233437da99..8fdf1004d8 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs @@ -5,6 +5,7 @@ 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; @@ -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 44c0bcf2b4..33cb2496c6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs @@ -5,6 +5,7 @@ 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,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..136a9bf90e 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,42 @@ 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. + // + // The YccK => Cmyk conversion is independent of any embedded ICC profile. + // Since the same RGB working space is used during conversion to and from XYZ, + // colorimetric accuracy is preserved. + 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 e36683deec..67b7ee0dc6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs @@ -5,6 +5,7 @@ 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; @@ -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 b1228ba012..3a586d25fa 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs @@ -5,6 +5,7 @@ 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; @@ -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 0db081c6f3..213ea34cc9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs @@ -5,6 +5,7 @@ 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; @@ -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..2bd4b95fdd 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,7 +144,15 @@ internal class SpectralConverter : SpectralConverter, IDisposable JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); - this.colorConverter.ConvertToRgbInPlace(values); + if (iccProfile != null) + { + this.colorConverter.ConvertToRgbInPlaceWithIcc(this.Configuration, in values, iccProfile); + } + else + { + this.colorConverter.ConvertToRgbInPlace(in values); + } + values = values.Slice(0, width); // slice away Jpeg padding Span r = this.rgbBuffer.Slice(0, 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..b50885d025 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 v2 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/ColorProfiles/RbgAndYConversionTests.cs b/tests/ImageSharp.Tests/ColorProfiles/RbgAndYConversionTests.cs index 1cd6f6cf41..017ba78d0b 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/RbgAndYConversionTests.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/RbgAndYConversionTests.cs @@ -23,7 +23,7 @@ public class RbgAndYConversionTests { ColorConversionOptions options = new() { - YCbCrMatrix = KnownYCbCrMatrices.BT601 + YCbCrTransform = KnownYCbCrMatrices.BT601 }; Convert_Rgb_To_Y_Core(r, g, b, y, options); @@ -37,7 +37,7 @@ public class RbgAndYConversionTests { ColorConversionOptions options = new() { - YCbCrMatrix = KnownYCbCrMatrices.BT709 + YCbCrTransform = KnownYCbCrMatrices.BT709 }; Convert_Rgb_To_Y_Core(r, g, b, y, options); @@ -51,7 +51,7 @@ public class RbgAndYConversionTests { ColorConversionOptions options = new() { - YCbCrMatrix = KnownYCbCrMatrices.BT2020 + YCbCrTransform = KnownYCbCrMatrices.BT2020 }; Convert_Rgb_To_Y_Core(r, g, b, y, options); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 950265bd56..6df3542765 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,54 @@ 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.YCCK, PixelTypes.Rgba32)] + public void Decode_YCCK_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/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e49d95ef6a..8b5529ac1a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -215,6 +215,7 @@ public static class TestImages public const string WideRGB = "Jpg/icc-profiles/Momiji-WideRGB-yes.jpg"; public const string AppleRGB = "Jpg/icc-profiles/Momiji-AppleRGB-yes.jpg"; public const string CMYK = "Jpg/icc-profiles/issue-129.jpg"; + public const string YCCK = "Jpg/icc-profiles/issue_2723.jpg"; } public static class Progressive 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 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png new file mode 100644 index 0000000000..bf95566838 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cef85199e8560d6669766c094d078831024e44ac7fc537f8696f802c8e06138b +size 420141 diff --git a/tests/Images/Input/Jpg/icc-profiles/issue_2723.jpg b/tests/Images/Input/Jpg/icc-profiles/issue_2723.jpg new file mode 100644 index 0000000000..4a342e07ed --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/issue_2723.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56dc6400d8f4c42c505ec562e4ade0b23e613b2c16e1666eccbbaa8e997efcc7 +size 766523