diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
index 3ddbf93b5..fd99fb446 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 class ColorProfileConverterExtensionsIcc
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 000000000..5f3d42afb
--- /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 adf0107da..da50a1abe 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 @@ internal abstract class ImageDecoderCore
///
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 b0a84341f..4c9bd6f32 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -7,12 +7,9 @@ using System.Diagnostics.CodeAnalysis;
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.Exif;
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 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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 2afa5fdc9..6c8dd2618 100644
--- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
+++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
@@ -20,26 +20,13 @@ public static class 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 class MemoryAllocatorValidator
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)
{