From ea564842c511f252095312d7e3bf950025ff2851 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 14 May 2025 22:12:52 +1000 Subject: [PATCH 1/8] Rename type --- .../ColorProfiles/ColorConversionOptions.cs | 14 +++++++------- src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs | 6 +++--- src/ImageSharp/ColorProfiles/Y.cs | 4 ++-- src/ImageSharp/ColorProfiles/YCbCr.cs | 8 ++++---- .../{YcbCrMatrix.cs => YCbCrTransform.cs} | 10 +++++----- src/ImageSharp/ColorProfiles/YccK.cs | 8 ++++---- .../ColorProfiles/RbgAndYConversionTests.cs | 6 +++--- 7 files changed, 28 insertions(+), 28 deletions(-) rename src/ImageSharp/ColorProfiles/{YcbCrMatrix.cs => YCbCrTransform.cs} (85%) diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs index 44c7d2ac7..882d246a7 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/KnownYCbCrMatrices.cs b/src/ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs index e2b7bf102..d32833a38 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/Y.cs b/src/ImageSharp/ColorProfiles/Y.cs index 960bf4699..83321a085 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 8e7dc57d7..22d629373 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 ccb4ea986..c90b90708 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 f05e1431b..df5eb4894 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/tests/ImageSharp.Tests/ColorProfiles/RbgAndYConversionTests.cs b/tests/ImageSharp.Tests/ColorProfiles/RbgAndYConversionTests.cs index 1cd6f6cf4..017ba78d0 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); From 74d475d4c4e1be9699c536047b69302374b05d0f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 16 May 2025 23:20:12 +1000 Subject: [PATCH 2/8] Use ICC profile when available for JPEG decoding color transforms. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 10 +- ...e.Generated.cs => CompactSrgbV4Profile.cs} | 19 ++-- .../Formats/ColorProfileHandling.cs | 7 +- src/ImageSharp/Formats/DecoderOptions.cs | 43 +++++++- src/ImageSharp/Formats/ImageDecoder.cs | 22 +++++ .../JpegColorConverter.CmykScalar.cs | 37 +++++++ .../JpegColorConverter.CmykVector128.cs | 7 +- .../JpegColorConverter.CmykVector256.cs | 7 +- .../JpegColorConverter.CmykVector512.cs | 7 +- .../JpegColorConverter.GrayScaleScalar.cs | 61 ++++++++++-- .../JpegColorConverter.GrayScaleVector128.cs | 16 ++- .../JpegColorConverter.GrayScaleVector256.cs | 16 ++- .../JpegColorConverter.GrayScaleVector512.cs | 18 +++- .../JpegColorConverter.RgbScalar.cs | 57 ++++++++++- .../JpegColorConverter.RgbVector128.cs | 7 +- .../JpegColorConverter.RgbVector256.cs | 7 +- .../JpegColorConverter.RgbVector512.cs | 7 +- .../JpegColorConverter.YCbCrScalar.cs | 49 ++++++++++ .../JpegColorConverter.YCbCrVector128.cs | 5 + .../JpegColorConverter.YCbCrVector256.cs | 5 + .../JpegColorConverter.YCbCrVector512.cs | 5 + .../JpegColorConverter.YccKScalar.cs | 49 ++++++++++ .../JpegColorConverter.YccKVector128.cs | 7 +- .../JpegColorConverter.YccKVector256.cs | 7 +- .../JpegColorConverter.YccKVector512.cs | 5 + .../ColorConverters/JpegColorConverterBase.cs | 97 +++++++++++++++++++ .../Decoder/ArithmeticScanDecoder.cs | 42 ++++---- .../Components/Decoder/HuffmanScanDecoder.cs | 33 ++++--- .../Components/Decoder/IJpegScanDecoder.cs | 19 ++-- .../Jpeg/Components/Decoder/JpegBitReader.cs | 6 +- .../Components/Decoder/SpectralConverter.cs | 16 ++- .../Decoder/SpectralConverter{TPixel}.cs | 23 +++-- .../Formats/Jpeg/JpegDecoderCore.cs | 12 ++- .../Decompressors/JpegTiffCompression.cs | 13 ++- .../Decompressors/OldJpegTiffCompression.cs | 15 ++- .../Metadata/Profiles/ICC/IccProfile.cs | 2 +- .../Metadata/Profiles/ICC/IccProfileHeader.cs | 40 +++++++- .../Codecs/Jpeg/DecodeJpegParseStreamOnly.cs | 3 +- .../Icc/ColorProfileConverterTests.Icc.cs | 1 + .../ColorProfiles/Icc/TestIccProfiles.cs | 6 ++ .../Formats/Jpg/JpegDecoderTests.cs | 36 ++++++- .../Formats/Jpg/SpectralJpegTests.cs | 3 +- .../Jpg/SpectralToPixelConversionTests.cs | 2 +- .../TestDataIcc/Profiles/issue-129.icc | 3 + .../Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png | 3 + ..._ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png | 3 + ...GB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png | 3 + ...RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png | 3 + ...de_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png | 3 + 51 files changed, 761 insertions(+), 112 deletions(-) rename src/ImageSharp/ColorProfiles/Icc/{SrgbV4Profile.Generated.cs => CompactSrgbV4Profile.cs} (89%) create mode 100644 tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 23bf85cf3..fef49bffd 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -277,11 +277,11 @@ internal static class AotCompilerTools private static void AotCompileSpectralConverter() where TPixel : unmanaged, IPixel { - default(SpectralConverter).GetPixelBuffer(default); - default(GrayJpegSpectralConverter).GetPixelBuffer(default); - default(RgbJpegSpectralConverter).GetPixelBuffer(default); - default(TiffJpegSpectralConverter).GetPixelBuffer(default); - default(TiffOldJpegSpectralConverter).GetPixelBuffer(default); + default(SpectralConverter).GetPixelBuffer(default, default); + default(GrayJpegSpectralConverter).GetPixelBuffer(default, default); + default(RgbJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffJpegSpectralConverter).GetPixelBuffer(default, default); + default(TiffOldJpegSpectralConverter).GetPixelBuffer(default, default); } /// diff --git a/src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs b/src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs similarity index 89% rename from src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs rename to src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs index a4d673488..29e30b53e 100644 --- a/src/ImageSharp/ColorProfiles/Icc/SrgbV4Profile.Generated.cs +++ b/src/ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs @@ -1,17 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// - using SixLabors.ImageSharp.Metadata.Profiles.Icc; -namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; +namespace SixLabors.ImageSharp.ColorProfiles.Icc; -internal static class SrgbV4Profile +internal static class CompactSrgbV4Profile { + private static readonly Lazy LazyIccProfile = new(GetIccProfile); + // Generated using the sRGB-v4.icc profile found at https://github.com/saucecontrol/Compact-ICC-Profiles - private static ReadOnlySpan Data => new byte[] - { + private static ReadOnlySpan Data => + [ 0, 0, 1, 224, 108, 99, 109, 115, 4, 32, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, 226, 0, 3, 0, 20, 0, 9, 0, 14, 0, 29, 97, 99, 115, 112, 77, 83, 70, 84, 0, 0, 0, 0, 115, 97, 119, 115, 99, 116, 114, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 214, 0, 1, 0, 0, 0, 0, 211, 45, 104, 97, 110, 100, 163, 178, 171, @@ -29,11 +29,9 @@ internal static class SrgbV4Profile 3, 143, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, 150, 0, 0, 183, 137, 0, 0, 24, 218, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 36, 160, 0, 0, 15, 133, 0, 0, 182, 196, 112, 97, 114, 97, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 102, 105, 0, 0, 242, 167, 0, 0, 13, 89, 0, 0, 19, 208, 0, 0, 10, 91, - }; + ]; - private static readonly Lazy LazyIccProfile = new(() => GetIccProfile()); - - public static IccProfile GetProfile() => LazyIccProfile.Value; + public static IccProfile Profile => LazyIccProfile.Value; private static IccProfile GetIccProfile() { @@ -42,4 +40,3 @@ internal static class SrgbV4Profile return new IccProfile(buffer); } } - diff --git a/src/ImageSharp/Formats/ColorProfileHandling.cs b/src/ImageSharp/Formats/ColorProfileHandling.cs index e6f4b0a6a..661e6c4bc 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 8c6b8fc22..8a365682a 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 dd148dfed..c18fc663b 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 380d3d6cc..ebaa7c4b0 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 0a935cca4..14addafc1 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 3cef262ec..98bda53d2 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 f57ad4352..c72af2faf 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 f710b7365..2016cb81d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs @@ -1,8 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,21 +22,65 @@ internal abstract partial class JpegColorConverterBase /// public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(values.Component0, this.MaximumValue); + => ConvertToRgbInPlace(in values, this.MaximumValue); + + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgbScalar(values, rLane, gLane, bLane); - internal static void ConvertToRgbInPlace(Span values, float maxValue) + internal static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) + { + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + ref float c0 = ref Unsafe.Add(ref c0Base, i); + c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; + } + } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) { - ref float valuesRef = ref MemoryMarshal.GetReference(values); - float scale = 1 / maxValue; + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + ref float c0Base = ref MemoryMarshal.GetReference(c0); + ref float c1Base = ref MemoryMarshal.GetReference(c1); + ref float c2Base = ref MemoryMarshal.GetReference(c2); - for (nuint i = 0; i < (uint)values.Length; i++) + float scale = 1F / maxValue; + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) { - Unsafe.Add(ref valuesRef, i) *= scale; + ref float c = ref Unsafe.Add(ref c0Base, i); + c *= scale; } + + Span source = MemoryMarshal.Cast(values.Component0); + Span destination = MemoryMarshal.Cast(packed); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); } internal static void ConvertFromRgbScalar(in ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs index f3a6f7d37..877988176 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,12 +18,22 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertToRgbInPlace(in ComponentValues values) { ref Vector128 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector128 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector128 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector128 scale = Vector128.Create(1 / this.MaximumValue); @@ -31,6 +42,9 @@ internal abstract partial class JpegColorConverterBase { ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs index 139ffc549..6ae20bba8 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -23,6 +24,12 @@ internal abstract partial class JpegColorConverterBase ref Vector256 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector256 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector256 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector256 scale = Vector256.Create(1 / this.MaximumValue); @@ -31,9 +38,16 @@ internal abstract partial class JpegColorConverterBase { ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs index 21d5eaa6f..f56093df9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,12 +18,22 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) { ref Vector512 c0Base = ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); + ref Vector512 c1Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); + + ref Vector512 c2Base = + ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); + // Used for the color conversion Vector512 scale = Vector512.Create(1 / this.MaximumValue); @@ -31,6 +42,9 @@ internal abstract partial class JpegColorConverterBase { ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); c0 *= scale; + + Unsafe.Add(ref c1Base, i) = c0; + Unsafe.Add(ref c2Base, i) = c0; } } @@ -66,7 +80,7 @@ internal abstract partial class JpegColorConverterBase /// protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => GrayScaleScalar.ConvertToRgbInPlace(values.Component0, this.MaximumValue); + => GrayScaleScalar.ConvertToRgbInPlace(in values, this.MaximumValue); /// protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs index 23825b06e..770709d7f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs @@ -1,6 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -16,15 +24,58 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgbInPlace(values, this.MaximumValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, rLane, gLane, bLane); + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + internal static void ConvertToRgbInPlace(ComponentValues values, float maxValue) { - GrayScaleScalar.ConvertToRgbInPlace(values.Component0, maxValue); - GrayScaleScalar.ConvertToRgbInPlace(values.Component1, maxValue); - GrayScaleScalar.ConvertToRgbInPlace(values.Component2, maxValue); + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + + float scale = 1F / maxValue; + + for (nuint i = 0; i < (nuint)values.Component0.Length; i++) + { + ref float c0 = ref Unsafe.Add(ref c0Base, i); + c0 *= scale; + + ref float c1 = ref Unsafe.Add(ref c1Base, i); + c1 *= scale; + + ref float c2 = ref Unsafe.Add(ref c2Base, i); + c2 *= scale; + } } internal static void ConvertFromRgb(ComponentValues values, Span rLane, Span gLane, Span bLane) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs index 47aa4281b..6cbbc7c7c 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 02448d724..10bc2be5f 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 76745f665..6e01ad7cb 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 e514a0166..3fca87052 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs @@ -1,6 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -22,6 +29,10 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); @@ -49,6 +60,44 @@ internal abstract partial class JpegColorConverterBase } } + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + + // Although YCbCr is a defined ICC color space, in practice ICC profiles + // do not implement transforms from it. + // Therefore, we first convert JPEG YCbCr to RGB manually, then perform + // color-managed conversion to the target profile. + + // TODO: The initial YCbCr => RGB conversion is assumed to be in the sRGB working space. + // To perform accurate colorimetric conversion via XYZ, we should derive the working space + // from the source ICC profile (e.g., via header/tags). + // This is a placeholder until that logic is implemented. + ColorProfileConverter converter = new(); + + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); + + Span source = MemoryMarshal.Cast(packed); + Span destination = MemoryMarshal.Cast(packed); + + converter.Convert(source, destination); + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new(options); + converter.Convert(destination, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } + public static void ConvertFromRgb(in ComponentValues values, float halfValue, Span rLane, Span gLane, Span bLane) { Span y = values.Component0; diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs index 8cecd3956..9bce944fb 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector128_ = SixLabors.ImageSharp.Common.Helpers.Vector128Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -66,6 +67,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs index f8517e086..079a6de2d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -66,6 +67,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs index 7598a64b2..637c6ef1e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -17,6 +18,10 @@ internal abstract partial class JpegColorConverterBase { } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs index bb545ec76..14a365678 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs @@ -1,6 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal abstract partial class JpegColorConverterBase @@ -22,6 +29,10 @@ internal abstract partial class JpegColorConverterBase public override void ConvertToRgbInPlace(in ComponentValues values) => ConvertToRgpInPlace(values, this.MaximumValue, this.HalfValue); + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); @@ -73,5 +84,43 @@ internal abstract partial class JpegColorConverterBase y[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); } } + + public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) + { + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); + Span packed = memoryOwner.Memory.Span; + + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + Span c3 = values.Component3; + + PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); + + ColorProfileConverter converter = new(); + Span source = MemoryMarshal.Cast(packed); + + // YccK is not a defined ICC color space — it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. + // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. + // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. + // + // TODO: The intermediate YccK => RGB step assumes a working space with sRGB primaries and D65 white point. + // To perform accurate colorimetric conversion via XYZ, we should derive the working space + // from the source ICC profile (e.g., via header/tags). + // This is a placeholder until that logic is implemented. + converter.Convert(MemoryMarshal.Cast(source), source); + + Span destination = MemoryMarshal.Cast(packed)[..source.Length]; + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + converter = new(options); + converter.Convert(source, destination); + + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs index 5bb2c5e5b..bca7d7114 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector128_ = SixLabors.ImageSharp.Common.Helpers.Vector128Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs index 27f2ce035..3c1911204 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs @@ -1,9 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector256_ = SixLabors.ImageSharp.Common.Helpers.Vector256Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs index 42d89a231..b4ddfc046 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Vector512_ = SixLabors.ImageSharp.Common.Helpers.Vector512Utilities; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -75,6 +76,10 @@ internal abstract partial class JpegColorConverterBase } } + /// + public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); + /// protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) => YccKScalar.ConvertToRgpInPlace(values, this.MaximumValue, this.HalfValue); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs index 8cb3045dc..c7cc8e971 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 02a346ff0..6e83f5b2b 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 56e0f1e98..9ee43a2c8 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 ecec723a9..588090deb 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 e71d86a1d..f888a8fb7 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 51d9bfbce..e65296199 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 561d273e6..655665e29 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -98,9 +99,10 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// For non-baseline interleaved jpeg this method does a 'lazy' spectral /// conversion from spectral to color. /// + /// Optional ICC profile for color conversion. /// Cancellation token. /// Pixel buffer. - public Buffer2D GetPixelBuffer(CancellationToken cancellationToken) + public Buffer2D GetPixelBuffer(IccProfile iccProfile, CancellationToken cancellationToken) { if (!this.Converted) { @@ -111,7 +113,7 @@ internal class SpectralConverter : SpectralConverter, IDisposable for (int step = 0; step < steps; step++) { cancellationToken.ThrowIfCancellationRequested(); - this.ConvertStride(step); + this.ConvertStride(step, iccProfile); } } @@ -124,7 +126,8 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// Converts single spectral jpeg stride to color stride. /// /// Spectral stride index. - private void ConvertStride(int spectralStep) + /// Optional ICC profile for color conversion. + private void ConvertStride(int spectralStep, IccProfile iccProfile) { int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); @@ -141,9 +144,17 @@ internal class SpectralConverter : SpectralConverter, IDisposable JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); - this.colorConverter.ConvertToRgbInPlace(values); values = values.Slice(0, width); // slice away Jpeg padding + if (iccProfile != null) + { + this.colorConverter.ConvertToRgbInPlaceWithIcc(this.Configuration, in values, iccProfile); + } + else + { + this.colorConverter.ConvertToRgbInPlace(in values); + } + Span r = this.rgbBuffer.Slice(0, width); Span g = this.rgbBuffer.Slice(width, width); Span b = this.rgbBuffer.Slice(width * 2, width); @@ -222,11 +233,11 @@ internal class SpectralConverter : SpectralConverter, IDisposable } /// - public override void ConvertStrideBaseline() + public override void ConvertStrideBaseline(IccProfile iccProfile) { // Convert next pixel stride using single spectral `stride' // Note that zero passing eliminates extra virtual call - this.ConvertStride(spectralStep: 0); + this.ConvertStride(spectralStep: 0, iccProfile); foreach (ComponentProcessor cpp in this.componentProcessors) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 707baa1a8..9198a5239 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 82b26232a..1df55b8b5 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 b58183ff6..13257dd63 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 da015b2b0..392ccb306 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 4da5a6aa5..6f9b8167e 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. #nullable disable @@ -11,6 +11,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; /// public sealed class IccProfileHeader { + private static readonly Vector3 TruncatedD50 = new(0.9642029F, 1F, 0.8249054F); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); + + // sRGB v4 Preference + private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D); + + // sRGB v4 Appearance + private static readonly IccProfileId StandardRgbV4A = new(0xDF1132A1, 0x746E97B0, 0xAD85719, 0xBE711E08); + /// /// Gets or sets the profile size in bytes (will be ignored when writing a profile). /// @@ -97,4 +108,31 @@ public sealed class IccProfileHeader /// Gets or sets the profile ID (hash). /// public IccProfileId Id { get; set; } + + internal static bool IsLikelySrgb(IccProfileHeader header) + { + // Reject known perceptual-appearance profile + // This profile employs perceptual rendering intents to maintain color appearance across different + // devices and media, which can lead to variations from standard sRGB representations. + if (header.Id == StandardRgbV4A) + { + return false; + } + + // Accept known sRGB profile IDs + if (header.Id == StandardRgbV2 || header.Id == StandardRgbV4) + { + return true; + } + + // Fallback: best-guess heuristic + return + header.FileSignature == "acsp" && + header.DataColorSpace == IccColorSpaceType.Rgb && + (header.ProfileConnectionSpace == IccColorSpaceType.CieXyz || header.ProfileConnectionSpace == IccColorSpaceType.CieLab) && + (header.Class == IccProfileClass.DisplayDevice || header.Class == IccProfileClass.ColorSpace) && + header.PcsIlluminant == TruncatedD50 && + (header.Version.Major == 2 || header.Version.Major == 4) && + !string.Equals(header.CmmType, "ADBE", StringComparison.Ordinal); + } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 5f6dbbdf7..91ee82136 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 7e01a629b..6c56dc682 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 3fc36a91b..3e3bb4d49 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs +++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs @@ -16,6 +16,7 @@ internal static class TestIccProfiles /// v2 CMYK -> LAB, output, lut16 /// public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; + /// /// v2 CMYK -> LAB, output, lut16 /// @@ -46,6 +47,11 @@ internal static class TestIccProfiles /// public const string StandardRgbV4 = "sRGB_v4_ICC_preference.icc"; + /// + /// v2 CMYK -> LAB, output + /// + public const string Issue129 = "issue-129.icc"; + /// /// v2 RGB -> XYZ, display, TRCs /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 950265bd5..4b4682920 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -365,4 +364,39 @@ public partial class JpegDecoderTests image.DebugSave(provider); image.CompareToOriginal(provider); } + + [Theory] + [WithFile(TestImages.Jpeg.ICC.CMYK, PixelTypes.Rgba32)] + public void Decode_CMYK_ICC_Jpeg(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestImages.Jpeg.ICC.SRgb, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.AdobeRgb, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.ColorMatch, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.ProPhoto, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.WideRGB, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.ICC.AppleRGB, PixelTypes.Rgba32)] + public void Decode_RGB_ICC_Jpeg(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 805ee586a..499cf7991 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 25929182f..b9f4a90b6 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 000000000..9dee96165 --- /dev/null +++ b/tests/ImageSharp.Tests/TestDataIcc/Profiles/issue-129.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35f401731df11a4eba3502af632e51d68bc394bcb7d34632a331c1ba3f4a0bf6 +size 557168 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png new file mode 100644 index 000000000..77a9d0d9c --- /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 000000000..0963e90b7 --- /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 000000000..1e5eaca4d --- /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 000000000..06bee00d7 --- /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 000000000..3ae12c657 --- /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 000000000..0a2eb91cc --- /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 000000000..fa554484b --- /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 From a744f937adba8f61a515fddaf4752952eab5bbe2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 16 May 2025 23:27:05 +1000 Subject: [PATCH 3/8] Update Rgb.cs --- src/ImageSharp/ColorProfiles/Rgb.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 9b95278bc..42e502592 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)); } } From e2cde198295cf9b4f88c96e449c91d92262b63d7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 17 May 2025 00:49:32 +1000 Subject: [PATCH 4/8] Tweak grayscale converter code --- .../JpegColorConverter.GrayScaleScalar.cs | 8 ++++---- .../JpegColorConverter.GrayScaleVector128.cs | 8 ++++---- .../JpegColorConverter.GrayScaleVector256.cs | 8 ++++---- .../JpegColorConverter.GrayScaleVector512.cs | 8 ++++---- .../ColorConverters/JpegColorConverter.RgbScalar.cs | 11 +++-------- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs index 2016cb81d..74869c93c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs @@ -41,11 +41,11 @@ internal abstract partial class JpegColorConverterBase float scale = 1F / maxValue; for (nuint i = 0; i < (nuint)values.Component0.Length; i++) { - ref float c0 = ref Unsafe.Add(ref c0Base, i); - c0 *= scale; + float c = Unsafe.Add(ref c0Base, i) * scale; - Unsafe.Add(ref c1Base, i) = c0; - Unsafe.Add(ref c2Base, i) = c0; + 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.GrayScaleVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs index 80093ebd9..633080706 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs @@ -40,11 +40,11 @@ internal abstract partial class JpegColorConverterBase 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 c1Base, i) = c0; - Unsafe.Add(ref c2Base, i) = c0; + 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 d40ed192e..0b2b8a119 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs @@ -36,11 +36,11 @@ internal abstract partial class JpegColorConverterBase 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 c1Base, i) = c0; - Unsafe.Add(ref c2Base, i) = c0; + 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.GrayScaleVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs index 93e0f0dbd..0a58b0196 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs @@ -40,11 +40,11 @@ internal abstract partial class JpegColorConverterBase 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 c1Base, i) = c0; - Unsafe.Add(ref c2Base, i) = c0; + 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.RgbScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs index 770709d7f..4b2abc0ac 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs @@ -67,14 +67,9 @@ internal abstract partial class JpegColorConverterBase for (nuint i = 0; i < (nuint)values.Component0.Length; i++) { - ref float c0 = ref Unsafe.Add(ref c0Base, i); - c0 *= scale; - - ref float c1 = ref Unsafe.Add(ref c1Base, i); - c1 *= scale; - - ref float c2 = ref Unsafe.Add(ref c2Base, i); - c2 *= scale; + Unsafe.Add(ref c0Base, i) *= scale; + Unsafe.Add(ref c1Base, i) *= scale; + Unsafe.Add(ref c2Base, i) *= scale; } } From b10d88abc08192d7cd8158244cff2bd395ccec39 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 17 May 2025 01:09:31 +1000 Subject: [PATCH 5/8] Put that thing back where it came from or so help me! --- .../Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 655665e29..2bd4b95fd 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -144,8 +144,6 @@ internal class SpectralConverter : SpectralConverter, IDisposable JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); - values = values.Slice(0, width); // slice away Jpeg padding - if (iccProfile != null) { this.colorConverter.ConvertToRgbInPlaceWithIcc(this.Configuration, in values, iccProfile); @@ -155,6 +153,8 @@ internal class SpectralConverter : SpectralConverter, IDisposable this.colorConverter.ConvertToRgbInPlace(in values); } + values = values.Slice(0, width); // slice away Jpeg padding + Span r = this.rgbBuffer.Slice(0, width); Span g = this.rgbBuffer.Slice(width, width); Span b = this.rgbBuffer.Slice(width * 2, width); From b860f550aea5627e6f5f637678f579c51554a33f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 18 May 2025 18:07:29 +1000 Subject: [PATCH 6/8] Add YCCK test --- .../Formats/Jpg/JpegDecoderTests.cs | 15 +++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png | 3 +++ .../Images/Input/Jpg/icc-profiles/issue_2723.jpg | 3 +++ 4 files changed, 22 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png create mode 100644 tests/Images/Input/Jpg/icc-profiles/issue_2723.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 4b4682920..6df354276 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -380,6 +380,21 @@ public partial class JpegDecoderTests 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)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 373b38eee..a5336e52f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -212,6 +212,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_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 000000000..bf9556683 --- /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 000000000..4a342e07e --- /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 From 04053c54949a5c5e186cf2891075a2ceed80ae77 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Jun 2025 14:39:35 +1000 Subject: [PATCH 7/8] Update IccProfileHeader.cs --- src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs index 6f9b8167e..b50885d02 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs @@ -13,7 +13,7 @@ public sealed class IccProfileHeader { private static readonly Vector3 TruncatedD50 = new(0.9642029F, 1F, 0.8249054F); - // sRGB v4 Preference + // sRGB v2 Preference private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE); // sRGB v4 Preference From 65bb20bbb4c894b13cf5b6ec60760e0f44ba0c5a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Jun 2025 16:14:46 +1000 Subject: [PATCH 8/8] Update comments --- .../ColorConverters/JpegColorConverter.YCbCrScalar.cs | 9 ++++----- .../ColorConverters/JpegColorConverter.YccKScalar.cs | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs index 3fca87052..4fa18b88a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs @@ -73,11 +73,10 @@ internal abstract partial class JpegColorConverterBase // do not implement transforms from it. // Therefore, we first convert JPEG YCbCr to RGB manually, then perform // color-managed conversion to the target profile. - - // TODO: The initial YCbCr => RGB conversion is assumed to be in the sRGB working space. - // To perform accurate colorimetric conversion via XYZ, we should derive the working space - // from the source ICC profile (e.g., via header/tags). - // This is a placeholder until that logic is implemented. + // + // 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); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs index 14a365678..136a9bf90 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs @@ -104,10 +104,9 @@ internal abstract partial class JpegColorConverterBase // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. // - // TODO: The intermediate YccK => RGB step assumes a working space with sRGB primaries and D65 white point. - // To perform accurate colorimetric conversion via XYZ, we should derive the working space - // from the source ICC profile (e.g., via header/tags). - // This is a placeholder until that logic is implemented. + // 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];