Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ internal static class ColorProfileConverterExtensionsIcc
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F];

/// <summary>
/// Converts a color value from one ICC color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">The color value to convert, defined in the source color profile.</param>
/// <returns>
/// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if either the source or target ICC profile is missing from the converter options.
/// </exception>
internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
Expand Down Expand Up @@ -81,6 +99,29 @@ internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverte
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
}

/// <summary>
/// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter that provides conversion options and ICC profiles.</param>
/// <param name="source">
/// A read-only span containing the source color values to convert. The values must conform to the source color
/// profile.
/// </param>
/// <param name="destination">
/// A span to receive the converted color values in the destination color profile. Must be at least as large as the
/// source span.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the source or target ICC profile is missing from the converter options.
/// </exception>
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Converts the pixel data of the specified image from the source color profile to the target color profile using
/// the provided color profile converter.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">
/// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
/// image.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the converter's source or target ICC profile is not specified.
/// </exception>
public static void Convert<TPixel>(this ColorProfileConverter converter, Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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 chunks, the converter itself is thread safe.
source.Mutate(o => o.ProcessPixelRowsAsVector4(
row =>
{
// Gather and convert the pixels in the row to Rgb.
using IMemoryOwner<Rgb> rgbBuffer = converter.Options.MemoryAllocator.Allocate<Rgb>(row.Length);
Span<Rgb> rgbSpan = rgbBuffer.Memory.Span;
Rgb.FromScaledVector4(row, rgbSpan);

// Perform the actual color conversion.
converter.ConvertUsingIccProfile<Rgb, Rgb>(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<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb;
}
},
PixelConversionModifiers.Scale));
}
}
40 changes: 40 additions & 0 deletions src/ImageSharp/Formats/ImageDecoderCore.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -124,4 +127,41 @@ public Image<TPixel> Decode<TPixel>(
/// </remarks>
protected abstract Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;

/// <summary>
/// Converts the ICC color profile of the specified image to the compact sRGB v4 profile if a source profile is
/// available.
/// </summary>
/// <remarks>
/// 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).
/// <br/>
/// 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.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image whose ICC profile will be converted to the compact sRGB v4 profile.</param>
/// <returns>
/// <see langword="true"/> if the conversion was performed; otherwise, <see langword="false"/>.
/// </returns>
protected bool TryConvertIccProfile<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
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;
}
}
7 changes: 7 additions & 0 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
break;
case PngChunkType.FrameData:
{
if (frameCount >= this.maxFrames)
{
goto EOF;
Expand Down Expand Up @@ -246,7 +247,10 @@ protected override Image<TPixel> Decode<TPixel>(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)
Expand Down Expand Up @@ -276,6 +280,8 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
}

break;
}

case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
Expand Down Expand Up @@ -323,6 +329,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
PngThrowHelper.ThrowNoData();
}

_ = this.TryConvertIccProfile(image);
return image;
}
catch
Expand Down
16 changes: 16 additions & 0 deletions tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,22 @@ public void Decode_WithAverageFilter<TPixel>(TestImageProvider<TPixel> 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)]
[WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba32, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba64, PixelTypes.Rgba32)]
public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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)]
Expand Down
29 changes: 17 additions & 12 deletions tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,13 @@ static MemoryAllocatorValidator()
private static void MemoryDiagnostics_MemoryReleased()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
if (backing != null)
{
backing.TotalRemainingAllocated--;
}
backing?.OnReleased();
}

private static void MemoryDiagnostics_MemoryAllocated()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
if (backing != null)
{
backing.TotalAllocated++;
backing.TotalRemainingAllocated++;
}
backing?.OnAllocated();
}

public static TestMemoryDiagnostics MonitorAllocations()
Expand All @@ -48,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)
{
Expand Down
9 changes: 9 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ 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 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";
}

public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/icc-profiles/Perceptual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/icc-profiles/sRGB_Gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading