From e965d3fac69ff87505a292dda60fc1f72b4db701 Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 5 Dec 2025 00:27:18 -0500 Subject: [PATCH 01/12] Apply color conversion when decoding PNG --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 38f964d37b..f7eb0de1f2 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -7,9 +7,12 @@ using System.Globalization; using System.IO.Compression; using System.IO.Hashing; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -323,6 +326,11 @@ protected override Image Decode(BufferedReadStream stream, Cance PngThrowHelper.ThrowNoData(); } + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + ApplyIccProfile(image, iccProfile, CompactSrgbV4Profile.Profile); + } + return image; } catch @@ -2153,4 +2161,58 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); + + // FIXME: Maybe this could be a .Mutate(x => x.ApplyIccProfile(destinationProfile)) ? Nothing related to png here + private static void ApplyIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) + where TPixel : unmanaged, IPixel + { + ColorConversionOptions options = new() + { + SourceIccProfile = sourceProfile, + TargetIccProfile = destinationProfile, + }; + + ColorProfileConverter converter = new(options); + + image.ProcessPixelRows(pixelAccessor => + { + using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelAccessor.Width * 3); + using IMemoryOwner alphaBuffer = image.Configuration.MemoryAllocator.Allocate(pixelAccessor.Width); + Span rgbPacked = rgbBuffer.Memory.Span; + ref float rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); + Span alphaPacked = alphaBuffer.Memory.Span; + ref float alphaPackedRef = ref MemoryMarshal.GetReference(alphaPacked); + + for (int y = 0; y < pixelAccessor.Height; y++) + { + Span pixelsRow = pixelAccessor.GetRowSpan(y); + int rgbIdx = 0; + for (int x = 0; x < pixelsRow.Length; x++, rgbIdx += 3) + { + Vector4 rgba = pixelsRow[x].ToScaledVector4(); + Unsafe.Add(ref rgbPackedRef, rgbIdx) = rgba.X; + Unsafe.Add(ref rgbPackedRef, rgbIdx + 1) = rgba.Y; + Unsafe.Add(ref rgbPackedRef, rgbIdx + 2) = rgba.Z; + Unsafe.Add(ref alphaPackedRef, x) = rgba.W; + } + + Span source = MemoryMarshal.Cast(rgbPacked); + Span destination = MemoryMarshal.Cast(rgbPacked); + converter.Convert(source, destination); + + rgbIdx = 0; + for (int x = 0; x < pixelsRow.Length; x++, rgbIdx += 3) + { + float r = Unsafe.Add(ref rgbPackedRef, rgbIdx); + float g = Unsafe.Add(ref rgbPackedRef, rgbIdx + 1); + float b = Unsafe.Add(ref rgbPackedRef, rgbIdx + 2); + float a = Unsafe.Add(ref alphaPackedRef, x); + + pixelsRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + } + } + } + ); + } + } From bd532d66961bc56ac47852ce19dacb33233eaf1c Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 00:07:00 -0500 Subject: [PATCH 02/12] Add tests to check if PngDecoder apply ICC profile --- .../Formats/Png/PngDecoderTests.cs | 14 ++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 7 +++++++ ...ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png | 3 +++ ...IsConvert_ApplyIccProfile_Rgba32_Perceptual.png | 3 +++ ...gIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png | 3 +++ .../Png/icc-profiles/Perceptual-cLUT-only.png | 3 +++ tests/Images/Input/Png/icc-profiles/Perceptual.png | 3 +++ tests/Images/Input/Png/icc-profiles/sRGB_Gray.png | 3 +++ 8 files changed, 39 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png create mode 100644 tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png create mode 100644 tests/Images/Input/Png/icc-profiles/Perceptual.png create mode 100644 tests/Images/Input/Png/icc-profiles/sRGB_Gray.png diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 3589a25a2d..98cfd06b63 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -206,6 +206,20 @@ public void Decode_WithAverageFilter(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.Icc.Perceptual, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.PerceptualcLUTOnly, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGray, PixelTypes.Rgba32)] + public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert }); + + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + Assert.Null(image.Metadata.IccProfile); + } + [Theory] [WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)] [WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bc699da88e..18ad623729 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -166,6 +166,13 @@ public static class Png // Issue 3000: https://github.com/SixLabors/ImageSharp/issues/3000 public const string Issue3000 = "Png/issues/issue_3000.png"; + public static class Icc + { + public const string SRgbGray = "Png/icc-profiles/sRGB_Gray.png"; + public const string Perceptual = "Png/icc-profiles/Perceptual.png"; + public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png"; + } + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png new file mode 100644 index 0000000000..ffc9839019 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b72c885278a066e63c013885c42b772275f25a5f0b2290aa38c87f3dbeac984b +size 81432 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png new file mode 100644 index 0000000000..25a97ca48d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:936261278b1a9f5bf9a2bb4f8da09f2a82e1b5c693790e137c5f98fa4d885735 +size 81785 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png new file mode 100644 index 0000000000..5a35cf5796 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf856e49e4ece7e59eea684f6fa533ba313a36955be4703894f16b100283cb4a +size 2687 diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png new file mode 100644 index 0000000000..8ac1afde9f --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c734cacc2c6e761bab088cac80ef09da7b56a545ce71c6cced4cac31e661795 +size 119811 diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual.png b/tests/Images/Input/Png/icc-profiles/Perceptual.png new file mode 100644 index 0000000000..cac9bcb1e5 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/Perceptual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:208a325dedea4453b7accce1ec540452af2e9be0f8c1f636f1d61a463eb3a9ae +size 123151 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png new file mode 100644 index 0000000000..3326936ceb --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c64e0f6cc38750c83e6ff0cf1911e210c342900bb2cd6c88d3daed30c854e863 +size 4531 From 15d11515b77c925b884397217d56ae34cd7dd44a Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 00:07:37 -0500 Subject: [PATCH 03/12] Fix concurrency problem with MemoryAllocatorValidator --- tests/ImageSharp.Tests/MemoryAllocatorValidator.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs index 395dfd455f..2afa5fdc9d 100644 --- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -22,7 +22,10 @@ private static void MemoryDiagnostics_MemoryReleased() TestMemoryDiagnostics backing = LocalInstance.Value; if (backing != null) { - backing.TotalRemainingAllocated--; + lock (backing) + { + backing.TotalRemainingAllocated--; + } } } @@ -31,8 +34,11 @@ private static void MemoryDiagnostics_MemoryAllocated() TestMemoryDiagnostics backing = LocalInstance.Value; if (backing != null) { - backing.TotalAllocated++; - backing.TotalRemainingAllocated++; + lock (backing) + { + backing.TotalAllocated++; + backing.TotalRemainingAllocated++; + } } } From f8c8174b575fedf89e1e2924a8d187794137125b Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 00:09:45 -0500 Subject: [PATCH 04/12] Rework ApplyIccProfile to use ProcessPixelRowsAsVector4 --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 54 +++++++------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f7eb0de1f2..2a2b07389d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -26,6 +26,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Png; @@ -328,7 +329,7 @@ protected override Image Decode(BufferedReadStream stream, Cance if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) { - ApplyIccProfile(image, iccProfile, CompactSrgbV4Profile.Profile); + ApplyRgbaCompatibleIccProfile(image, iccProfile, CompactSrgbV4Profile.Profile); } return image; @@ -2162,8 +2163,7 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); - // FIXME: Maybe this could be a .Mutate(x => x.ApplyIccProfile(destinationProfile)) ? Nothing related to png here - private static void ApplyIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) + private static void ApplyRgbaCompatibleIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) where TPixel : unmanaged, IPixel { ColorConversionOptions options = new() @@ -2174,45 +2174,27 @@ private static void ApplyIccProfile(Image image, IccProfile sour ColorProfileConverter converter = new(options); - image.ProcessPixelRows(pixelAccessor => - { - using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelAccessor.Width * 3); - using IMemoryOwner alphaBuffer = image.Configuration.MemoryAllocator.Allocate(pixelAccessor.Width); - Span rgbPacked = rgbBuffer.Memory.Span; - ref float rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); - Span alphaPacked = alphaBuffer.Memory.Span; - ref float alphaPackedRef = ref MemoryMarshal.GetReference(alphaPacked); - - for (int y = 0; y < pixelAccessor.Height; y++) + image.Mutate(o => o.ProcessPixelRowsAsVector4((pixelsRow, _) => { - Span pixelsRow = pixelAccessor.GetRowSpan(y); - int rgbIdx = 0; - for (int x = 0; x < pixelsRow.Length; x++, rgbIdx += 3) + using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); + Span rgbPacked = rgbBuffer.Memory.Span; + ref Rgb rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); + + for (int x = 0; x < pixelsRow.Length; x++) { - Vector4 rgba = pixelsRow[x].ToScaledVector4(); - Unsafe.Add(ref rgbPackedRef, rgbIdx) = rgba.X; - Unsafe.Add(ref rgbPackedRef, rgbIdx + 1) = rgba.Y; - Unsafe.Add(ref rgbPackedRef, rgbIdx + 2) = rgba.Z; - Unsafe.Add(ref alphaPackedRef, x) = rgba.W; + Unsafe.Add(ref rgbPackedRef, x) = Rgb.FromScaledVector4(pixelsRow[x]); } - Span source = MemoryMarshal.Cast(rgbPacked); - Span destination = MemoryMarshal.Cast(rgbPacked); - converter.Convert(source, destination); + converter.Convert(rgbPacked, rgbPacked); - rgbIdx = 0; - for (int x = 0; x < pixelsRow.Length; x++, rgbIdx += 3) - { - float r = Unsafe.Add(ref rgbPackedRef, rgbIdx); - float g = Unsafe.Add(ref rgbPackedRef, rgbIdx + 1); - float b = Unsafe.Add(ref rgbPackedRef, rgbIdx + 2); - float a = Unsafe.Add(ref alphaPackedRef, x); + Span pixelsRowAsFloats = MemoryMarshal.Cast(pixelsRow); + ref float pixelsRowAsFloatsRef = ref MemoryMarshal.GetReference(pixelsRowAsFloats); - pixelsRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + int cIdx = 0; + for (int x = 0; x < pixelsRow.Length; x++, cIdx += 4) + { + Unsafe.As(ref Unsafe.Add(ref pixelsRowAsFloatsRef, cIdx)) = rgbPacked[x]; } - } - } - ); + })); } - } From 472089abfbe9eaf9771c226471752a3a7b5558fd Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 02:26:03 -0500 Subject: [PATCH 05/12] Use bulk overload to convert the pixel row from Vector4 to Rgb in ApplyRgbaCompatibleIccProfile --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 2a2b07389d..5ad2682335 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -2178,13 +2178,8 @@ private static void ApplyRgbaCompatibleIccProfile(Image image, I { using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); Span rgbPacked = rgbBuffer.Memory.Span; - ref Rgb rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); - - for (int x = 0; x < pixelsRow.Length; x++) - { - Unsafe.Add(ref rgbPackedRef, x) = Rgb.FromScaledVector4(pixelsRow[x]); - } + Rgb.FromScaledVector4(pixelsRow, rgbPacked); converter.Convert(rgbPacked, rgbPacked); Span pixelsRowAsFloats = MemoryMarshal.Cast(pixelsRow); From e6d62d019a75b8d14974446ee06697dd02426a6b Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 14:02:47 -0500 Subject: [PATCH 06/12] Make sure pixel values are float between 0 and 1 during processing of ApplyRgbaCompatibleIccProfile --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5ad2682335..9da5330778 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -2174,7 +2174,8 @@ private static void ApplyRgbaCompatibleIccProfile(Image image, I ColorProfileConverter converter = new(options); - image.Mutate(o => o.ProcessPixelRowsAsVector4((pixelsRow, _) => + image.Mutate(o => o.ProcessPixelRowsAsVector4( + (pixelsRow, _) => { using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); Span rgbPacked = rgbBuffer.Memory.Span; @@ -2190,6 +2191,7 @@ private static void ApplyRgbaCompatibleIccProfile(Image image, I { Unsafe.As(ref Unsafe.Add(ref pixelsRowAsFloatsRef, cIdx)) = rgbPacked[x]; } - })); + }, + PixelConversionModifiers.Scale)); } } From 7d06e6df3d09d50306cb4e5213e41de2d9771b8f Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 19 Dec 2025 14:10:56 -0500 Subject: [PATCH 07/12] Use the same MemoryAllocator as the one of the image when decoding icc profile of png with ApplyRgbaCompatibleIccProfile --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 9da5330778..0bea161dc9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -2170,6 +2170,7 @@ private static void ApplyRgbaCompatibleIccProfile(Image image, I { SourceIccProfile = sourceProfile, TargetIccProfile = destinationProfile, + MemoryAllocator = image.Configuration.MemoryAllocator, }; ColorProfileConverter converter = new(options); From 33aa0c84b78d4d525f989ac5fbddaef02676fa7e Mon Sep 17 00:00:00 2001 From: Socolin Date: Sat, 20 Dec 2025 01:01:10 -0500 Subject: [PATCH 08/12] Apply ICC profile when decoding InterlacedRgba PNG --- src/ImageSharp/ColorProfiles/Rgb.cs | 11 ++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 71 ++++++-- .../Formats/Png/PngScanlineProcessor.cs | 162 +++++++++++++----- .../Formats/Png/PngDecoderTests.cs | 2 + tests/ImageSharp.Tests/TestImages.cs | 2 + ...ile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png | 3 + ...ile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png | 3 + .../sRGB_Gray_Interlaced_Rgba32.png | 3 + .../sRGB_Gray_Interlaced_Rgba64.png | 3 + 9 files changed, 205 insertions(+), 55 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png create mode 100644 tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png create mode 100644 tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 42e502592c..73c7611985 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -100,6 +100,17 @@ public static Rgb FromScaledVector4(Vector4 source) public Vector4 ToScaledVector4() => new(this.AsVector3Unsafe(), 1F); + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and usually clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The alpha component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4(float alpha) + => new(this.AsVector3Unsafe(), 1F); + /// public static void ToScaledVector4(ReadOnlySpan source, Span destination) { diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 0bea161dc9..b0a84341fd 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -216,6 +216,7 @@ protected override Image Decode(BufferedReadStream stream, Cance currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: + { if (frameCount >= this.maxFrames) { goto EOF; @@ -233,6 +234,11 @@ protected override Image Decode(BufferedReadStream stream, Cance this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + metadata.IccProfile = null; + } + this.currentStream.Position += 4; this.ReadScanlines( chunk.Length - 4, @@ -240,6 +246,7 @@ protected override Image Decode(BufferedReadStream stream, Cance pngMetadata, this.ReadNextFrameDataChunk, currentFrameControl.Value, + iccProfile, cancellationToken); // if current frame dispose is restore to previous, then from future frame's perspective, it never happened @@ -250,7 +257,10 @@ protected override Image Decode(BufferedReadStream stream, Cance } break; + } + case PngChunkType.Data: + { pngMetadata.AnimateRootFrame = currentFrameControl != null; currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height); if (image is null) @@ -261,12 +271,18 @@ protected override Image Decode(BufferedReadStream stream, Cance AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + metadata.IccProfile = null; + } + this.ReadScanlines( chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, currentFrameControl.Value, + iccProfile, cancellationToken); if (pngMetadata.AnimateRootFrame) { @@ -280,6 +296,8 @@ protected override Image Decode(BufferedReadStream stream, Cance } break; + } + case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); break; @@ -327,9 +345,9 @@ protected override Image Decode(BufferedReadStream stream, Cance PngThrowHelper.ThrowNoData(); } - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfileToApply)) { - ApplyRgbaCompatibleIccProfile(image, iccProfile, CompactSrgbV4Profile.Profile); + ApplyRgbaCompatibleIccProfile(image, iccProfileToApply, CompactSrgbV4Profile.Profile); } return image; @@ -752,6 +770,7 @@ private int CalculateScanlineLength(int width) /// The png metadata /// A delegate to get more data from the inner stream for . /// The frame control + /// Optional ICC profile for color conversion. /// The cancellation token. private void ReadScanlines( int chunkLength, @@ -759,6 +778,7 @@ private void ReadScanlines( PngMetadata pngMetadata, Func getData, in FrameControl frameControl, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -772,11 +792,11 @@ private void ReadScanlines( if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { - this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); } else { - this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + this.DecodePixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); } } @@ -788,12 +808,14 @@ private void ReadScanlines( /// The compressed pixel data stream. /// The image frame to decode to. /// The png metadata + /// Optional ICC profile for color conversion. /// The CancellationToken private void DecodePixelData( FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -860,7 +882,7 @@ private void DecodePixelData( break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer); + this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer, iccProfile); this.SwapScanlineBuffers(); currentRow++; } @@ -878,12 +900,14 @@ private void DecodePixelData( /// The compressed pixel data stream. /// The current image frame. /// The png metadata. + /// Optional ICC profile for color conversion. /// The cancellation token. private void DecodeInterlacedPixelData( in FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -974,6 +998,7 @@ private void DecodeInterlacedPixelData( rowSpan, pngMetadata, blendRowBuffer, + iccProfile, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); @@ -1012,13 +1037,15 @@ private void DecodeInterlacedPixelData( /// The image /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. + /// Optional ICC profile for color conversion. private void ProcessDefilteredScanline( in FrameControl frameControl, int currentRow, ReadOnlySpan scanline, ImageFrame pixels, PngMetadata pngMetadata, - Span blendRowBuffer) + Span blendRowBuffer, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -1052,7 +1079,8 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1063,7 +1091,8 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample); + (uint)this.bytesPerSample, + iccProfile); break; @@ -1072,7 +1101,8 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.ColorTable); + pngMetadata.ColorTable, + iccProfile); break; @@ -1085,7 +1115,8 @@ private void ProcessDefilteredScanline( rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1097,7 +1128,8 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, this.bytesPerPixel, - this.bytesPerSample); + this.bytesPerSample, + iccProfile); break; } @@ -1124,6 +1156,7 @@ private void ProcessDefilteredScanline( /// The current image row. /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. + /// Optional ICC profile for color conversion. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. private void ProcessInterlacedDefilteredScanline( @@ -1132,6 +1165,7 @@ private void ProcessInterlacedDefilteredScanline( Span destination, PngMetadata pngMetadata, Span blendRowBuffer, + IccProfile? iccProfile, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel @@ -1166,7 +1200,8 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1179,7 +1214,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample); + (uint)this.bytesPerSample, + iccProfile); break; @@ -1190,7 +1226,8 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.ColorTable); + pngMetadata.ColorTable, + iccProfile); break; @@ -1205,7 +1242,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1219,7 +1257,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, this.bytesPerPixel, - this.bytesPerSample); + this.bytesPerSample, + iccProfile); break; } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 33ba58f545..ca4eaa58d2 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -1,10 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -20,7 +25,8 @@ public static void ProcessGrayscaleScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( bitDepth, @@ -29,7 +35,8 @@ public static void ProcessGrayscaleScanline( rowSpan, 0, 1, - transparentColor); + transparentColor, + iccProfile); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -38,9 +45,11 @@ public static void ProcessInterlacedGrayscaleScanline( Span rowSpan, uint pixelOffset, uint increment, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -98,7 +107,8 @@ public static void ProcessGrayscaleWithAlphaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, - uint bytesPerSample) + uint bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleWithAlphaScanline( bitDepth, @@ -108,7 +118,8 @@ public static void ProcessGrayscaleWithAlphaScanline( 0, 1, bytesPerPixel, - bytesPerSample); + bytesPerSample, + iccProfile); public static void ProcessInterlacedGrayscaleWithAlphaScanline( int bitDepth, @@ -118,9 +129,11 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( uint pixelOffset, uint increment, uint bytesPerPixel, - uint bytesPerSample) + uint bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -153,7 +166,8 @@ public static void ProcessPaletteScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlyMemory? palette) + ReadOnlyMemory? palette, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -161,7 +175,8 @@ public static void ProcessPaletteScanline( rowSpan, 0, 1, - palette); + palette, + iccProfile); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -169,9 +184,11 @@ public static void ProcessInterlacedPaletteScanline( Span rowSpan, uint pixelOffset, uint increment, - ReadOnlyMemory? palette) + ReadOnlyMemory? palette, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { +// FIXME-icc if (palette is null) { PngThrowHelper.ThrowMissingPalette(); @@ -198,7 +215,8 @@ public static void ProcessRgbScanline( Span rowSpan, int bytesPerPixel, int bytesPerSample, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbScanline( configuration, @@ -210,7 +228,8 @@ public static void ProcessRgbScanline( 1, bytesPerPixel, bytesPerSample, - transparentColor); + transparentColor, + iccProfile); public static void ProcessInterlacedRgbScanline( Configuration configuration, @@ -222,9 +241,11 @@ public static void ProcessInterlacedRgbScanline( uint increment, int bytesPerPixel, int bytesPerSample, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -302,7 +323,8 @@ public static void ProcessRgbaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, - int bytesPerSample) + int bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( configuration, @@ -313,7 +335,8 @@ public static void ProcessRgbaScanline( 0, 1, bytesPerPixel, - bytesPerSample); + bytesPerSample, + iccProfile); public static void ProcessInterlacedRgbaScanline( Configuration configuration, @@ -324,43 +347,104 @@ public static void ProcessInterlacedRgbaScanline( uint pixelOffset, uint increment, int bytesPerPixel, - int bytesPerSample) + int bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { uint offset = pixelOffset + frameControl.XOffset; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (bitDepth == 16) + if (iccProfile != null) { - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + ColorConversionOptions options = new() + { + SourceIccProfile = iccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + using IMemoryOwner rgbBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); + Span rgbPacked = rgbBuffer.Memory.Span; + ref Rgb rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); + using IMemoryOwner alphaBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); + Span alphaPacked = alphaBuffer.Memory.Span; + ref float alphaPackedRef = ref MemoryMarshal.GetReference(alphaPacked); + + if (bitDepth == 16) + { + int o = 0; + for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + + Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)ushort.MaxValue, g / (float)ushort.MaxValue, b / (float)ushort.MaxValue); + Unsafe.Add(ref alphaPackedRef, i) = a / (float)ushort.MaxValue; + } + } + else { - ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + + Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)byte.MaxValue, g / (float)byte.MaxValue, b / (float)byte.MaxValue); + Unsafe.Add(ref alphaPackedRef, i) = a / (float)byte.MaxValue; + } + } + + converter.Convert(rgbPacked, rgbPacked); + + int idx = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, idx++) + { + Rgb rgb = Unsafe.Add(ref rgbPackedRef, idx); + Vector4 rgba = rgb.ToScaledVector4(Unsafe.Add(ref alphaPackedRef, idx)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromScaledVector4(rgba); } - } - else if (pixelOffset == 0 && increment == 1) - { - PixelOperations.Instance.FromRgba32Bytes( - configuration, - scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], - rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), - (int)frameControl.Width); } else { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } + else { - byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); - byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 98cfd06b63..a58101a6bd 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -210,6 +210,8 @@ public void Decode_WithAverageFilter(TestImageProvider provider) [WithFile(TestImages.Png.Icc.Perceptual, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Icc.PerceptualcLUTOnly, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Icc.SRgbGray, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba32, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba64, 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 18ad623729..25bec12664 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -169,6 +169,8 @@ public static class Png public static class Icc { public const string SRgbGray = "Png/icc-profiles/sRGB_Gray.png"; + public const string SRgbGrayInterlacedRgba32 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png"; + public const string SRgbGrayInterlacedRgba64 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png"; public const string Perceptual = "Png/icc-profiles/Perceptual.png"; public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png"; } diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png new file mode 100644 index 0000000000..270555a555 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337e84b78fb07359a42e7eee0eed32e6728497c64aa30c6bd5ea8a3a5ec67ebc +size 5151 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png new file mode 100644 index 0000000000..dc5f4a559c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:456ae30184b13aa2dc3d922db433017e076ff969862fe506436ed96c2d9be0a1 +size 6143 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png new file mode 100644 index 0000000000..7afc51cfe3 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fc63cea5de188e76503bde2fce3ff84518af5064bb46d506420cd6d7e58285b +size 7237 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png new file mode 100644 index 0000000000..822aca4f53 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64343871be4ad61451ef968fa9f07c6a11dee65d0f8fd718ae8c4941586aa60c +size 8227 From 82eb56b1180ec1e509219b2e3732841db7d3dff9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Jan 2026 15:26:26 +1000 Subject: [PATCH 09/12] Basic fallback functionality complete --- .../ColorProfileConverterExtensionsIcc.cs | 41 +++++++++++ ...ofileConverterExtensionsPixelCompatible.cs | 72 +++++++++++++++++++ src/ImageSharp/Formats/ImageDecoderCore.cs | 40 +++++++++++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 53 ++------------ .../MemoryAllocatorValidator.cs | 35 +++++---- 5 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index 3ddbf93b58..fd99fb4467 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -39,6 +39,24 @@ internal static class ColorProfileConverterExtensionsIcc 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F]; + /// + /// Converts a color value from one ICC color profile to another using the specified color profile converter. + /// + /// + /// This method performs color conversion using ICC profiles, ensuring accurate color mapping + /// between different color spaces. Both the source and target ICC profiles must be provided in the converter's + /// options. The method supports perceptual adjustments when required by the profiles. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter configured with source and target ICC profiles. + /// The color value to convert, defined in the source color profile. + /// + /// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value. + /// + /// + /// Thrown if either the source or target ICC profile is missing from the converter options. + /// internal static TTo ConvertUsingIccProfile(this ColorProfileConverter converter, in TFrom source) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile @@ -81,6 +99,29 @@ internal static TTo ConvertUsingIccProfile(this ColorProfileConverte return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); } + /// + /// Converts a span of color values from a source color profile to a destination color profile using ICC profiles. + /// + /// + /// This method performs color conversion by transforming the input values through the Profile + /// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required + /// by the profiles. The method does not support absolute colorimetric intent and will not perform such + /// conversions. + /// + /// The type representing the source color profile. Must implement . + /// The type representing the destination color profile. Must implement . + /// The color profile converter that provides conversion options and ICC profiles. + /// + /// A read-only span containing the source color values to convert. The values must conform to the source color + /// profile. + /// + /// + /// A span to receive the converted color values in the destination color profile. Must be at least as large as the + /// source span. + /// + /// + /// Thrown if the source or target ICC profile is missing from the converter options. + /// internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination) where TFrom : struct, IColorProfile where TTo : struct, IColorProfile diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs new file mode 100644 index 0000000000..5f3d42afb2 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs @@ -0,0 +1,72 @@ +// 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.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal static class ColorProfileConverterExtensionsPixelCompatible +{ + /// + /// Converts the pixel data of the specified image from the source color profile to the target color profile using + /// the provided color profile converter. + /// + /// + /// This method modifies the source image in place by converting its pixel data according to the + /// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible; + /// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC + /// profiles are set on the converter before calling this method. + /// + /// The pixel format. + /// The color profile converter configured with source and target ICC profiles. + /// + /// The image whose pixel data will be converted. The conversion is performed in place, modifying the original + /// image. + /// + /// + /// Thrown if the converter's source or target ICC profile is not specified. + /// + public static void Convert(this ColorProfileConverter converter, Image source) + where TPixel : unmanaged, IPixel + { + // These checks actually take place within the converter, but we want to fail fast here. + // Note. we do not check to see whether the profiles themselves are RGB compatible, + // if they are not, then the converter will simply produce incorrect results. + if (converter.Options.SourceIccProfile is null) + { + throw new InvalidOperationException("Source ICC profile is missing."); + } + + if (converter.Options.TargetIccProfile is null) + { + throw new InvalidOperationException("Target ICC profile is missing."); + } + + // Process the rows in parallel chnks, the converter itself is thread safe. + source.Mutate(o => o.ProcessPixelRowsAsVector4( + row => + { + // Gather and convert the pixels in the row to Rgb. + using IMemoryOwner rgbBuffer = converter.Options.MemoryAllocator.Allocate(row.Length); + Span rgbSpan = rgbBuffer.Memory.Span; + Rgb.FromScaledVector4(row, rgbSpan); + + // Perform the actual color conversion. + converter.ConvertUsingIccProfile(rgbSpan, rgbSpan); + + // Copy the converted Rgb pixels back to the row as TPixel. + ref Vector4 rowRef = ref MemoryMarshal.GetReference(row); + for (int i = 0; i < rgbSpan.Length; i++) + { + Vector3 rgb = rgbSpan[i].AsVector3Unsafe(); + Unsafe.As(ref Unsafe.Add(ref rowRef, i)) = rgb; + } + }, + PixelConversionModifiers.Scale)); + } +} diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index adf0107da0..da50a1abec 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -124,4 +127,41 @@ public Image Decode( /// protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; + + /// + /// Converts the ICC color profile of the specified image 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 whose ICC profile will be converted to the compact sRGB v4 profile. + /// + /// if the conversion was performed; otherwise, . + /// + protected bool TryConvertIccProfile(Image image) + where TPixel : unmanaged, IPixel + { + if (!this.Options.TryGetIccProfileForColorConversion(image.Metadata.IccProfile, out IccProfile? profile)) + { + return false; + } + + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = image.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + converter.Convert(image); + return true; + } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index b0a84341fd..4c9bd6f326 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -7,12 +7,9 @@ using System.Globalization; using System.IO.Compression; using System.IO.Hashing; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -26,7 +23,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Png; @@ -234,9 +230,10 @@ protected override Image Decode(BufferedReadStream stream, Cance this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) { - metadata.IccProfile = null; + // TODO: Rework this. We need to preserve metadata + // metadata.IccProfile = null; } this.currentStream.Position += 4; @@ -271,9 +268,10 @@ protected override Image Decode(BufferedReadStream stream, Cance AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) { - metadata.IccProfile = null; + // TODO: Rework this. We need to preserve metadata + // metadata.IccProfile = null; } this.ReadScanlines( @@ -345,11 +343,7 @@ protected override Image Decode(BufferedReadStream stream, Cance PngThrowHelper.ThrowNoData(); } - if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfileToApply)) - { - ApplyRgbaCompatibleIccProfile(image, iccProfileToApply, CompactSrgbV4Profile.Profile); - } - + _ = this.TryConvertIccProfile(image); return image; } catch @@ -2201,37 +2195,4 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); - - private static void ApplyRgbaCompatibleIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) - where TPixel : unmanaged, IPixel - { - ColorConversionOptions options = new() - { - SourceIccProfile = sourceProfile, - TargetIccProfile = destinationProfile, - MemoryAllocator = image.Configuration.MemoryAllocator, - }; - - ColorProfileConverter converter = new(options); - - image.Mutate(o => o.ProcessPixelRowsAsVector4( - (pixelsRow, _) => - { - using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); - Span rgbPacked = rgbBuffer.Memory.Span; - - Rgb.FromScaledVector4(pixelsRow, rgbPacked); - converter.Convert(rgbPacked, rgbPacked); - - Span pixelsRowAsFloats = MemoryMarshal.Cast(pixelsRow); - ref float pixelsRowAsFloatsRef = ref MemoryMarshal.GetReference(pixelsRowAsFloats); - - int cIdx = 0; - for (int x = 0; x < pixelsRow.Length; x++, cIdx += 4) - { - Unsafe.As(ref Unsafe.Add(ref pixelsRowAsFloatsRef, cIdx)) = rgbPacked[x]; - } - }, - PixelConversionModifiers.Scale)); - } } diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs index 2afa5fdc9d..6c8dd2618c 100644 --- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -20,26 +20,13 @@ static MemoryAllocatorValidator() private static void MemoryDiagnostics_MemoryReleased() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - lock (backing) - { - backing.TotalRemainingAllocated--; - } - } + backing?.OnReleased(); } private static void MemoryDiagnostics_MemoryAllocated() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - lock (backing) - { - backing.TotalAllocated++; - backing.TotalRemainingAllocated++; - } - } + backing?.OnAllocated(); } public static TestMemoryDiagnostics MonitorAllocations() @@ -54,11 +41,23 @@ public static TestMemoryDiagnostics MonitorAllocations() public static void ValidateAllocations(int expectedAllocationCount = 0) => LocalInstance.Value?.Validate(expectedAllocationCount); - public class TestMemoryDiagnostics : IDisposable + public sealed class TestMemoryDiagnostics : IDisposable { - public int TotalAllocated { get; set; } + private int totalAllocated; + private int totalRemainingAllocated; + + public int TotalAllocated => Volatile.Read(ref this.totalAllocated); + + public int TotalRemainingAllocated => Volatile.Read(ref this.totalRemainingAllocated); + + internal void OnAllocated() + { + Interlocked.Increment(ref this.totalAllocated); + Interlocked.Increment(ref this.totalRemainingAllocated); + } - public int TotalRemainingAllocated { get; set; } + internal void OnReleased() + => Interlocked.Decrement(ref this.totalRemainingAllocated); public void Validate(int expectedAllocationCount) { From d9183d54e73e2d1d28548477cc1d731f2b72fca2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Jan 2026 15:35:50 +1000 Subject: [PATCH 10/12] Add dev note [skip ci] --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 4c9bd6f326..f0d3f33397 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1134,6 +1134,8 @@ private void ProcessDefilteredScanline( PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); blender.Blend(this.configuration, destination, destination, rowSpan, 1F); } + + // TODO: Here is where we would perform ICC color conversion if needed over the 'destination' span. } finally { From bb93350bb562dfb27ef16d21a8eef2186c50d054 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Jan 2026 21:00:17 +1000 Subject: [PATCH 11/12] Remove per-pixel transform --- ...ofileConverterExtensionsPixelCompatible.cs | 4 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 65 ++----- .../Formats/Png/PngScanlineProcessor.cs | 162 +++++------------- 3 files changed, 55 insertions(+), 176 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs index 5f3d42afb2..2780f04bae 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs @@ -47,7 +47,7 @@ public static void Convert(this ColorProfileConverter converter, Image o.ProcessPixelRowsAsVector4( row => { @@ -64,7 +64,7 @@ public static void Convert(this ColorProfileConverter converter, Image(ref Unsafe.Add(ref rowRef, i)) = rgb; + Unsafe.As(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb; } }, PixelConversionModifiers.Scale)); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f0d3f33397..bff4d30ee5 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -230,12 +230,6 @@ protected override Image Decode(BufferedReadStream stream, Cance this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); - if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) - { - // TODO: Rework this. We need to preserve metadata - // metadata.IccProfile = null; - } - this.currentStream.Position += 4; this.ReadScanlines( chunk.Length - 4, @@ -243,7 +237,6 @@ protected override Image Decode(BufferedReadStream stream, Cance pngMetadata, this.ReadNextFrameDataChunk, currentFrameControl.Value, - iccProfile, cancellationToken); // if current frame dispose is restore to previous, then from future frame's perspective, it never happened @@ -268,19 +261,12 @@ protected override Image Decode(BufferedReadStream stream, Cance AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - if (!this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) - { - // TODO: Rework this. We need to preserve metadata - // metadata.IccProfile = null; - } - this.ReadScanlines( chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, currentFrameControl.Value, - iccProfile, cancellationToken); if (pngMetadata.AnimateRootFrame) { @@ -764,7 +750,6 @@ private int CalculateScanlineLength(int width) /// The png metadata /// A delegate to get more data from the inner stream for . /// The frame control - /// Optional ICC profile for color conversion. /// The cancellation token. private void ReadScanlines( int chunkLength, @@ -772,7 +757,6 @@ private void ReadScanlines( PngMetadata pngMetadata, Func getData, in FrameControl frameControl, - IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -786,11 +770,11 @@ private void ReadScanlines( if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { - this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); + this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); } else { - this.DecodePixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); + this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); } } @@ -802,14 +786,12 @@ private void ReadScanlines( /// The compressed pixel data stream. /// The image frame to decode to. /// The png metadata - /// Optional ICC profile for color conversion. /// The CancellationToken private void DecodePixelData( FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, - IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -876,7 +858,7 @@ private void DecodePixelData( break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer, iccProfile); + this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer); this.SwapScanlineBuffers(); currentRow++; } @@ -894,14 +876,12 @@ private void DecodePixelData( /// The compressed pixel data stream. /// The current image frame. /// The png metadata. - /// Optional ICC profile for color conversion. /// The cancellation token. private void DecodeInterlacedPixelData( in FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, - IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -992,7 +972,6 @@ private void DecodeInterlacedPixelData( rowSpan, pngMetadata, blendRowBuffer, - iccProfile, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); @@ -1031,15 +1010,13 @@ private void DecodeInterlacedPixelData( /// The image /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. - /// Optional ICC profile for color conversion. private void ProcessDefilteredScanline( in FrameControl frameControl, int currentRow, ReadOnlySpan scanline, ImageFrame pixels, PngMetadata pngMetadata, - Span blendRowBuffer, - IccProfile? iccProfile) + Span blendRowBuffer) where TPixel : unmanaged, IPixel { Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -1073,8 +1050,7 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.TransparentColor, - iccProfile); + pngMetadata.TransparentColor); break; @@ -1085,8 +1061,7 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample, - iccProfile); + (uint)this.bytesPerSample); break; @@ -1095,8 +1070,7 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.ColorTable, - iccProfile); + pngMetadata.ColorTable); break; @@ -1109,8 +1083,7 @@ private void ProcessDefilteredScanline( rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor, - iccProfile); + pngMetadata.TransparentColor); break; @@ -1122,8 +1095,7 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, this.bytesPerPixel, - this.bytesPerSample, - iccProfile); + this.bytesPerSample); break; } @@ -1134,8 +1106,6 @@ private void ProcessDefilteredScanline( PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); blender.Blend(this.configuration, destination, destination, rowSpan, 1F); } - - // TODO: Here is where we would perform ICC color conversion if needed over the 'destination' span. } finally { @@ -1152,7 +1122,6 @@ private void ProcessDefilteredScanline( /// The current image row. /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. - /// Optional ICC profile for color conversion. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. private void ProcessInterlacedDefilteredScanline( @@ -1161,7 +1130,6 @@ private void ProcessInterlacedDefilteredScanline( Span destination, PngMetadata pngMetadata, Span blendRowBuffer, - IccProfile? iccProfile, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel @@ -1196,8 +1164,7 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.TransparentColor, - iccProfile); + pngMetadata.TransparentColor); break; @@ -1210,8 +1177,7 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample, - iccProfile); + (uint)this.bytesPerSample); break; @@ -1222,8 +1188,7 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.ColorTable, - iccProfile); + pngMetadata.ColorTable); break; @@ -1238,8 +1203,7 @@ private void ProcessInterlacedDefilteredScanline( (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor, - iccProfile); + pngMetadata.TransparentColor); break; @@ -1253,8 +1217,7 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, this.bytesPerPixel, - this.bytesPerSample, - iccProfile); + this.bytesPerSample); break; } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index ca4eaa58d2..33ba58f545 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -1,15 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Buffers.Binary; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats.Png.Chunks; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -25,8 +20,7 @@ public static void ProcessGrayscaleScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - Color? transparentColor, - IccProfile? iccProfile) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( bitDepth, @@ -35,8 +29,7 @@ public static void ProcessGrayscaleScanline( rowSpan, 0, 1, - transparentColor, - iccProfile); + transparentColor); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -45,11 +38,9 @@ public static void ProcessInterlacedGrayscaleScanline( Span rowSpan, uint pixelOffset, uint increment, - Color? transparentColor, - IccProfile? iccProfile) + Color? transparentColor) where TPixel : unmanaged, IPixel { - // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -107,8 +98,7 @@ public static void ProcessGrayscaleWithAlphaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, - uint bytesPerSample, - IccProfile? iccProfile) + uint bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleWithAlphaScanline( bitDepth, @@ -118,8 +108,7 @@ public static void ProcessGrayscaleWithAlphaScanline( 0, 1, bytesPerPixel, - bytesPerSample, - iccProfile); + bytesPerSample); public static void ProcessInterlacedGrayscaleWithAlphaScanline( int bitDepth, @@ -129,11 +118,9 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( uint pixelOffset, uint increment, uint bytesPerPixel, - uint bytesPerSample, - IccProfile? iccProfile) + uint bytesPerSample) where TPixel : unmanaged, IPixel { - // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -166,8 +153,7 @@ public static void ProcessPaletteScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlyMemory? palette, - IccProfile? iccProfile) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -175,8 +161,7 @@ public static void ProcessPaletteScanline( rowSpan, 0, 1, - palette, - iccProfile); + palette); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -184,11 +169,9 @@ public static void ProcessInterlacedPaletteScanline( Span rowSpan, uint pixelOffset, uint increment, - ReadOnlyMemory? palette, - IccProfile? iccProfile) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { -// FIXME-icc if (palette is null) { PngThrowHelper.ThrowMissingPalette(); @@ -215,8 +198,7 @@ public static void ProcessRgbScanline( Span rowSpan, int bytesPerPixel, int bytesPerSample, - Color? transparentColor, - IccProfile? iccProfile) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbScanline( configuration, @@ -228,8 +210,7 @@ public static void ProcessRgbScanline( 1, bytesPerPixel, bytesPerSample, - transparentColor, - iccProfile); + transparentColor); public static void ProcessInterlacedRgbScanline( Configuration configuration, @@ -241,11 +222,9 @@ public static void ProcessInterlacedRgbScanline( uint increment, int bytesPerPixel, int bytesPerSample, - Color? transparentColor, - IccProfile? iccProfile) + Color? transparentColor) where TPixel : unmanaged, IPixel { - // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -323,8 +302,7 @@ public static void ProcessRgbaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, - int bytesPerSample, - IccProfile? iccProfile) + int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( configuration, @@ -335,8 +313,7 @@ public static void ProcessRgbaScanline( 0, 1, bytesPerPixel, - bytesPerSample, - iccProfile); + bytesPerSample); public static void ProcessInterlacedRgbaScanline( Configuration configuration, @@ -347,104 +324,43 @@ public static void ProcessInterlacedRgbaScanline( uint pixelOffset, uint increment, int bytesPerPixel, - int bytesPerSample, - IccProfile? iccProfile) + int bytesPerSample) where TPixel : unmanaged, IPixel { uint offset = pixelOffset + frameControl.XOffset; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (iccProfile != null) + if (bitDepth == 16) { - ColorConversionOptions options = new() - { - SourceIccProfile = iccProfile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - MemoryAllocator = configuration.MemoryAllocator, - }; - - ColorProfileConverter converter = new(options); - using IMemoryOwner rgbBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); - Span rgbPacked = rgbBuffer.Memory.Span; - ref Rgb rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); - using IMemoryOwner alphaBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); - Span alphaPacked = alphaBuffer.Memory.Span; - ref float alphaPackedRef = ref MemoryMarshal.GetReference(alphaPacked); - - if (bitDepth == 16) - { - int o = 0; - for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) - { - ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); - - Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)ushort.MaxValue, g / (float)ushort.MaxValue, b / (float)ushort.MaxValue); - Unsafe.Add(ref alphaPackedRef, i) = a / (float)ushort.MaxValue; - } - } - else - { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - int o = 0; - for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) - { - byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); - byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - - Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)byte.MaxValue, g / (float)byte.MaxValue, b / (float)byte.MaxValue); - Unsafe.Add(ref alphaPackedRef, i) = a / (float)byte.MaxValue; - } - } - - converter.Convert(rgbPacked, rgbPacked); - - int idx = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, idx++) + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { - Rgb rgb = Unsafe.Add(ref rgbPackedRef, idx); - Vector4 rgba = rgb.ToScaledVector4(Unsafe.Add(ref alphaPackedRef, idx)); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromScaledVector4(rgba); + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); } } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } else { - if (bitDepth == 16) - { - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) - { - ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); - } - } - else if (pixelOffset == 0 && increment == 1) - { - PixelOperations.Instance.FromRgba32Bytes( - configuration, - scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], - rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), - (int)frameControl.Width); - } - else + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) - { - byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); - byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); - } + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); } } } From ce7835694f5e6af7c4dc7b2e34e99de471c245b7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Jan 2026 21:03:28 +1000 Subject: [PATCH 12/12] Update Rgb.cs --- src/ImageSharp/ColorProfiles/Rgb.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 73c7611985..42e502592c 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -100,17 +100,6 @@ public static Rgb FromScaledVector4(Vector4 source) public Vector4 ToScaledVector4() => new(this.AsVector3Unsafe(), 1F); - /// - /// Expands the color into a generic ("scaled") representation - /// with values scaled and usually clamped between 0 and 1. - /// The vector components are typically expanded in least to greatest significance order. - /// - /// The alpha component. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToScaledVector4(float alpha) - => new(this.AsVector3Unsafe(), 1F); - /// public static void ToScaledVector4(ReadOnlySpan source, Span destination) {