From 7e22a01469a53ea93c8dc2fff1829650700e55c5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Jan 2026 13:42:19 +1000 Subject: [PATCH] Add RGB support. --- src/ImageSharp/Formats/DecoderOptions.cs | 8 +-- src/ImageSharp/Formats/ImageDecoder.cs | 16 ++++++ src/ImageSharp/Formats/ImageDecoderCore.cs | 53 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 1 - .../Formats/Tiff/TiffDecoderCore.cs | 15 +++--- .../Formats/Tiff/TiffDecoderTests.cs | 4 +- tests/ImageSharp.Tests/TestImages.cs | 2 + ...pplyIccProfile_Rgba32_Perceptual_RGB16.png | 3 ++ ...ApplyIccProfile_Rgba32_Perceptual_RGB8.png | 3 ++ .../Tiff/icc-profiles/Perceptual_RGB16.tiff | 3 ++ .../Tiff/icc-profiles/Perceptual_RGB8.tiff | 3 ++ 11 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png create mode 100644 tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff create mode 100644 tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index bb6c2a282..d4d80fc12 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -78,12 +78,12 @@ public sealed class DecoderOptions return false; } - if (profile.IsCanonicalSrgbMatrixTrc()) + if (this.ColorProfileHandling == ColorProfileHandling.Preserve) { return false; } - if (this.ColorProfileHandling == ColorProfileHandling.Preserve) + if (profile.IsCanonicalSrgbMatrixTrc()) { return false; } @@ -99,11 +99,11 @@ public sealed class DecoderOptions return false; } - if (this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc()) + if (this.ColorProfileHandling == ColorProfileHandling.Convert) { return true; } - return this.ColorProfileHandling == ColorProfileHandling.Convert; + return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc(); } } diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index c18fc663b..2a5d44a35 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -328,6 +328,14 @@ public abstract class ImageDecoder : IImageDecoder { image.Metadata.IccProfile = null; } + + foreach (ImageFrame frame in image.Frames) + { + if (options.CanRemoveIccProfile(frame.Metadata.IccProfile)) + { + frame.Metadata.IccProfile = null; + } + } } private static void HandleIccProfile(DecoderOptions options, ImageInfo image) @@ -336,5 +344,13 @@ public abstract class ImageDecoder : IImageDecoder { image.Metadata.IccProfile = null; } + + foreach (ImageFrameMetadata frame in image.FrameMetadataCollection) + { + if (options.CanRemoveIccProfile(frame.IccProfile)) + { + frame.IccProfile = null; + } + } } } diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index da50a1abe..f6b1a92a0 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; @@ -164,4 +165,56 @@ internal abstract class ImageDecoderCore converter.Convert(image); return true; } + + /// + /// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is + /// available. + /// + /// + /// This method should only be used by decoders that gurantee that the encoded image data is in a color space + /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB). + ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made. + /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color + /// space. + ///
+ /// The pixel format. + /// The image frame whose ICC profile will be converted to the compact sRGB v4 profile. + /// + /// if the conversion was performed; otherwise, . + /// + protected bool TryConvertIccProfile(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile)) + { + return false; + } + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = frame.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + + ImageMetadata metadata = new() + { + IccProfile = frame.Metadata.IccProfile + }; + + IMemoryGroup m = frame.PixelBuffer.MemoryGroup; + + // Safe: ToArray only materializes the Memory segment list, not the underlying pixel buffers, + // and Wrap(Memory[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just + // invalidates the view). This means no pixel data is cloned and disposing the temporary image will + // not dispose or leak the frame's pixel buffer. + MemoryGroup memorySource = MemoryGroup.Wrap(m.ToArray()); + + using Image image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata); + converter.Convert(image); + return true; + } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 363c16445..270c7f356 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -429,7 +429,6 @@ internal static class TiffColorDecoderFactory } } -#pragma warning disable IDE0060 // Remove unused parameter public static TiffBasePlanarColorDecoder CreatePlanar( ImageFrameMetadata metadata, DecoderOptions options, diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 30fa277fc..e3a51aa8d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -52,11 +52,6 @@ internal class TiffDecoderCore : ImageDecoderCore /// private ByteOrder byteOrder; - /// - /// Indicating whether is BigTiff format. - /// - private bool isBigTiff; - /// /// Initializes a new instance of the class. /// @@ -167,7 +162,6 @@ internal class TiffDecoderCore : ImageDecoderCore IList directories = reader.Read(); this.byteOrder = reader.ByteOrder; - this.isBigTiff = reader.IsBigTiff; Size? size = null; uint frameCount = 0; @@ -273,6 +267,15 @@ internal class TiffDecoderCore : ImageDecoderCore this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); } + // Only RGB-compatible color types can be converted here because the TPixel-based ICC profile conversion + // expects RGB-like pixel data; other photometric interpretations (YCbCr, CMYK, Lab, etc.) would require + // dedicated transforms. We do this once at the frame level to avoid duplicating conversion logic + // across all color decoders and to keep their decode paths focused on raw pixel unpacking. + if (this.ColorType is >= TiffColorType.PaletteColor and <= TiffColorType.Rgba32323232Planar) + { + _ = this.TryConvertIccProfile(frame); + } + return frame; } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index c186d4f01..e432a7251 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -23,7 +23,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; [Theory] - // [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] + [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder.Instance)); @@ -355,6 +355,8 @@ public class TiffDecoderTests : TiffDecoderBaseTester [Theory] [WithFile(Icc.PerceptualCmyk, PixelTypes.Rgba32)] [WithFile(Icc.PerceptualCieLab, PixelTypes.Rgba32)] + [WithFile(Icc.PerceptualRgb8, PixelTypes.Rgba32)] + [WithFile(Icc.PerceptualRgb16, PixelTypes.Rgba32)] public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ee0766107..764954cae 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1199,6 +1199,8 @@ public static class TestImages { public const string PerceptualCmyk = "Tiff/icc-profiles/Perceptual_CMYK.tiff"; public const string PerceptualCieLab = "Tiff/icc-profiles/Perceptual_CIELAB.tiff"; + public const string PerceptualRgb8 = "Tiff/icc-profiles/Perceptual_RGB8.tiff"; + public const string PerceptualRgb16 = "Tiff/icc-profiles/Perceptual_RGB16.tiff"; } } diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png new file mode 100644 index 000000000..3fcea773a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de9cfa53a82b169c533999908c7ace43aa35a8b456d5f0378b539669c7857d1c +size 386 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png new file mode 100644 index 000000000..3fcea773a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de9cfa53a82b169c533999908c7ace43aa35a8b456d5f0378b539669c7857d1c +size 386 diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff new file mode 100644 index 000000000..369284819 --- /dev/null +++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b0e89ac971c23c12c9edff6f9e9dd066fe99f36e28e89972343d50562dd7dbd +size 65260 diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff new file mode 100644 index 000000000..e2c1ff54c --- /dev/null +++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b0e5ee58c9f53bf5414fc8244e98d65aa0bc6c3397e201399baff0cd378b23 +size 35236