Browse Source

Merge branch 'main' into patch

pull/2995/head
James Jackson-South 4 months ago
committed by GitHub
parent
commit
e7b1d7928a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 41
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  2. 72
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  3. 40
      src/ImageSharp/Formats/ImageDecoderCore.cs
  4. 7
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  5. 5
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  6. 14
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
  7. 6
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  8. 95
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
  9. 10
      tests/ImageSharp.Benchmarks/Config.cs
  10. 5
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  11. 2
      tests/ImageSharp.Benchmarks/Processing/Resize.cs
  12. 16
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  13. 26
      tests/ImageSharp.Tests/Formats/WebP/WebpVp8XTests.cs
  14. 159
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  15. 29
      tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
  16. 9
      tests/ImageSharp.Tests/TestImages.cs
  17. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png
  18. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png
  19. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png
  20. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png
  21. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png
  22. 3
      tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png
  23. 3
      tests/Images/Input/Png/icc-profiles/Perceptual.png
  24. 3
      tests/Images/Input/Png/icc-profiles/sRGB_Gray.png
  25. 3
      tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png
  26. 3
      tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png

41
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];
/// <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>
@ -81,6 +99,29 @@ internal static class ColorProfileConverterExtensionsIcc
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>

72
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
{
/// <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
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
/// </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
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -212,6 +212,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
break;
case PngChunkType.FrameData:
{
if (frameCount >= this.maxFrames)
{
goto EOF;
@ -246,7 +247,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
}
case PngChunkType.Data:
{
pngMetadata.AnimateRootFrame = currentFrameControl != null;
currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
@ -276,6 +280,8 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
}
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
@ -323,6 +329,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngThrowHelper.ThrowNoData();
}
_ = this.TryConvertIccProfile(image);
return image;
}
catch

5
src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs

@ -123,7 +123,10 @@ internal readonly struct WebpVp8X : IEquatable<WebpVp8X>
long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X);
stream.WriteByte(flags);
stream.Position += 3; // Reserved bytes
Span<byte> reserved = stackalloc byte[3];
stream.Write(reserved);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);

14
src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs

@ -39,13 +39,13 @@ internal struct UnmanagedMemoryHandle : IEquatable<UnmanagedMemoryHandle>
Interlocked.Increment(ref totalOutstandingHandles);
}
public IntPtr Handle => this.handle;
public readonly IntPtr Handle => this.handle;
public bool IsInvalid => this.Handle == IntPtr.Zero;
public readonly bool IsInvalid => this.Handle == IntPtr.Zero;
public bool IsValid => this.Handle != IntPtr.Zero;
public readonly bool IsValid => this.Handle != IntPtr.Zero;
public unsafe void* Pointer => (void*)this.Handle;
public readonly unsafe void* Pointer => (void*)this.Handle;
/// <summary>
/// Gets the total outstanding handle allocations for testing purposes.
@ -121,9 +121,9 @@ internal struct UnmanagedMemoryHandle : IEquatable<UnmanagedMemoryHandle>
this.lengthInBytes = 0;
}
public bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
public readonly bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
public override bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
public override readonly bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
public override int GetHashCode() => this.handle.GetHashCode();
public override readonly int GetHashCode() => this.handle.GetHashCode();
}

6
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -102,6 +102,12 @@ internal partial class ResizeKernelMap : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
internal ref ResizeKernel GetKernel(nuint destIdx) => ref this.kernels[(int)destIdx];
/// <summary>
/// Returns a read-only span of <see cref="ResizeKernel"/> over the underlying kernel data.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
internal ReadOnlySpan<ResizeKernel> GetKernelSpan() => this.kernels;
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>

95
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs

@ -110,34 +110,62 @@ internal sealed class ResizeWorker<TPixel> : IDisposable
{
Span<Vector4> tempColSpan = this.tempColumnBuffer.GetSpan();
// When creating transposedFirstPassBuffer, we made sure it's contiguous:
// When creating transposedFirstPassBuffer, we made sure it's contiguous.
Span<Vector4> transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.DangerousGetSingleSpan();
int left = this.targetWorkingRect.Left;
int right = this.targetWorkingRect.Right;
int width = this.targetWorkingRect.Width;
nuint widthCount = (uint)width;
// Normalize destination-space Y to kernel indices using uint arithmetic.
// This relies on the contract that processing addresses are normalized (cropping/padding handled by targetOrigin).
int targetOriginY = this.targetOrigin.Y;
// Hoist invariant calculations outside the loop.
int currentWindowMax = this.currentWindow.Max;
int currentWindowMin = this.currentWindow.Min;
nuint workerHeight = (uint)this.workerHeight;
nuint workerHeight2 = workerHeight * 2;
// Ref-walk the kernel table to avoid bounds checks in the tight loop.
ReadOnlySpan<ResizeKernel> vKernels = this.verticalKernelMap.GetKernelSpan();
ref ResizeKernel vKernelBase = ref MemoryMarshal.GetReference(vKernels);
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan);
for (int y = rowInterval.Min; y < rowInterval.Max; y++)
{
// Ensure offsets are normalized for cropping and padding.
ResizeKernel kernel = this.verticalKernelMap.GetKernel((uint)(y - this.targetOrigin.Y));
// Normalize destination-space Y to an unsigned kernel index.
uint vIdx = (uint)(y - targetOriginY);
ref ResizeKernel kernel = ref Unsafe.Add(ref vKernelBase, (nint)vIdx);
while (kernel.StartIndex + kernel.Length > this.currentWindow.Max)
// Slide the working window when the kernel would read beyond the current cached region.
int kernelEnd = kernel.StartIndex + kernel.Length;
while (kernelEnd > currentWindowMax)
{
this.Slide();
currentWindowMax = this.currentWindow.Max;
currentWindowMin = this.currentWindow.Min;
}
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan);
int top = kernel.StartIndex - currentWindowMin;
ref Vector4 colRef0 = ref transposedFirstPassBufferSpan[top];
int top = kernel.StartIndex - this.currentWindow.Min;
// Unroll by 2 and advance column refs via arithmetic to reduce inner-loop overhead.
nuint i = 0;
for (; i + 1 < widthCount; i += 2)
{
ref Vector4 colRef1 = ref Unsafe.Add(ref colRef0, workerHeight);
ref Vector4 fpBase = ref transposedFirstPassBufferSpan[top];
Unsafe.Add(ref tempRowBase, i) = kernel.ConvolveCore(ref colRef0);
Unsafe.Add(ref tempRowBase, i + 1) = kernel.ConvolveCore(ref colRef1);
for (nuint x = 0; x < (uint)(right - left); x++)
{
ref Vector4 firstPassColumnBase = ref Unsafe.Add(ref fpBase, x * (uint)this.workerHeight);
colRef0 = ref Unsafe.Add(ref colRef0, workerHeight2);
}
// Destination color components
Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase);
if (i < widthCount)
{
Unsafe.Add(ref tempRowBase, i) = kernel.ConvolveCore(ref colRef0);
}
Span<TPixel> targetRowSpan = destination.DangerousGetRowSpan(y).Slice(left, width);
@ -171,7 +199,19 @@ internal sealed class ResizeWorker<TPixel> : IDisposable
nuint left = (uint)this.targetWorkingRect.Left;
nuint right = (uint)this.targetWorkingRect.Right;
nuint widthCount = right - left;
// Normalize destination-space X to kernel indices using uint arithmetic.
// This relies on the contract that processing addresses are normalized (cropping/padding handled by targetOrigin).
nuint targetOriginX = (uint)this.targetOrigin.X;
nuint workerHeight = (uint)this.workerHeight;
int currentWindowMin = this.currentWindow.Min;
// Ref-walk the kernel table to avoid bounds checks in the tight loop.
ReadOnlySpan<ResizeKernel> hKernels = this.horizontalKernelMap.GetKernelSpan();
ref ResizeKernel hKernelBase = ref MemoryMarshal.GetReference(hKernels);
for (int y = calculationInterval.Min; y < calculationInterval.Max; y++)
{
Span<TPixel> sourceRow = this.source.DangerousGetRowSpan(y);
@ -182,17 +222,30 @@ internal sealed class ResizeWorker<TPixel> : IDisposable
tempRowSpan,
this.conversionModifiers);
// optimization for:
// Span<Vector4> firstPassSpan = transposedFirstPassBufferSpan.Slice(y - this.currentWindow.Min);
ref Vector4 firstPassBaseRef = ref transposedFirstPassBufferSpan[y - this.currentWindow.Min];
ref Vector4 firstPassBaseRef = ref transposedFirstPassBufferSpan[y - currentWindowMin];
// Unroll by 2 to reduce loop and kernel lookup overhead.
nuint x = left;
nuint z = 0;
for (; z + 1 < widthCount; x += 2, z += 2)
{
nuint hIdx0 = (uint)(x - targetOriginX);
nuint hIdx1 = (uint)((x + 1) - targetOriginX);
ref ResizeKernel kernel0 = ref Unsafe.Add(ref hKernelBase, (nint)hIdx0);
ref ResizeKernel kernel1 = ref Unsafe.Add(ref hKernelBase, (nint)hIdx1);
Unsafe.Add(ref firstPassBaseRef, z * workerHeight) = kernel0.Convolve(tempRowSpan);
Unsafe.Add(ref firstPassBaseRef, (z + 1) * workerHeight) = kernel1.Convolve(tempRowSpan);
}
for (nuint x = left, z = 0; x < right; x++, z++)
if (z < widthCount)
{
ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - targetOriginX);
nuint hIdx = (uint)(x - targetOriginX);
ref ResizeKernel kernel = ref Unsafe.Add(ref hKernelBase, (nint)hIdx);
// optimization for:
// firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan);
Unsafe.Add(ref firstPassBaseRef, z * (uint)this.workerHeight) = kernel.Convolve(tempRowSpan);
Unsafe.Add(ref firstPassBaseRef, z * workerHeight) = kernel.Convolve(tempRowSpan);
}
}
}

10
tests/ImageSharp.Benchmarks/Config.cs

@ -10,6 +10,7 @@ using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
namespace SixLabors.ImageSharp.Benchmarks;
@ -45,6 +46,15 @@ public partial class Config : ManualConfig
.WithArguments([new MsBuildArgument("/p:DebugType=portable")]));
}
public class StandardInProcess : Config
{
public StandardInProcess() => this.AddJob(
Job.Default
.WithRuntime(CoreRuntime.Core80)
.WithToolchain(InProcessEmitToolchain.Instance)
.WithArguments([new MsBuildArgument("/p:DebugType=portable")]));
}
#if OS_WINDOWS
private bool IsElevated => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
#endif

5
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -57,8 +57,9 @@
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" />
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.3.36812.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" Condition="'$(IsWindows)'=='true'" />
<PackageReference Include="Colourful" />
<PackageReference Include="NetVips" />
<PackageReference Include="NetVips.Native" />

2
tests/ImageSharp.Benchmarks/Processing/Resize.cs

@ -12,7 +12,7 @@ using SDImage = System.Drawing.Image;
namespace SixLabors.ImageSharp.Benchmarks;
[Config(typeof(Config.Standard))]
[Config(typeof(Config.StandardInProcess))]
public abstract class Resize<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{

16
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -206,6 +206,22 @@ public partial class PngDecoderTests
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)]

26
tests/ImageSharp.Tests/Formats/WebP/WebpVp8XTests.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Webp.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.WebP;
[Trait("Format", "Webp")]
public class WebpVp8XTests
{
[Fact]
public void WebpVp8X_WriteTo_Writes_Reserved_Bytes()
{
// arrange
WebpVp8X header = new(false, false, false, false, false, 10, 40);
MemoryStream ms = new();
byte[] expected = [86, 80, 56, 88, 10, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 39, 0, 0];
// act
header.WriteTo(ms);
// assert
byte[] actual = ms.ToArray();
Assert.Equal(expected, actual);
}
}

159
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor;
@ -273,67 +274,75 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
[InlineData(1200)] // Group of two UniformUnmanagedMemoryPool buffers
public void AllocateMemoryGroup_Finalization_ReturnsToPool(int length)
{
if (TestEnvironment.IsMacOS)
{
// Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
return;
}
if (TestEnvironment.OSArchitecture == Architecture.Arm64)
{
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
return;
}
if (!TestEnvironment.RunsOnCI)
{
// This may fail in local runs resulting in high memory load.
// Remove the condition for local debugging!
return;
}
// RunTest(length.ToString());
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose();
RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose();
static void RunTest(string lengthStr)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024);
int lengthInner = int.Parse(lengthStr);
int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture);
// We want to verify that a leaked (not disposed) `MemoryGroup<byte>` still returns its
// unmanaged handles into the pool when it is finalized.
//
// We intentionally do NOT validate this by checking the contents of the re-rented memory
// (contents are not guaranteed to be preserved) nor by comparing pointer values
// (the pool may return a different handle while still correctly pooling).
//
// Instead, we validate that after a forced GC+finalization cycle, a subsequent allocation
// of the same size does not cause the number of outstanding unmanaged handles to *increase*
// compared to a known baseline.
// Establish a baseline: create one allocation and dispose it so the pool is initialized.
// (This ensures subsequent observations are not biased by first-time pool growth.)
allocator.AllocateGroup<byte>(lengthInner, 100).Dispose();
int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles;
// Leak one allocation and force finalization.
AllocateGroupAndForget(allocator, lengthInner);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
AllocateGroupAndForget(allocator, lengthInner, true);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
using MemoryGroup<byte> g = allocator.AllocateGroup<byte>(lengthInner, 100);
Assert.Equal(42, g.First().Span[0]);
// Allocate again. If the leaked group was finalized correctly and returned to the pool,
// this should not require additional unmanaged allocations (ie, the handle count must not grow).
allocator.AllocateGroup<byte>(lengthInner, 100).Dispose();
// Note: we use "<=" instead of "==" here.
//
// After we record the baseline, the pool is allowed to legitimately *decrease*
// `UnmanagedMemoryHandle.TotalOutstandingHandles` by trimming retained buffers
// (eg. via the pool's trim timer/GC callbacks/high-pressure logic).
//
// What must not happen is the opposite: the leaked (non-disposed) group should be finalized
// and its handles returned to the pool such that allocating again does NOT require creating
// additional unmanaged handles. Therefore the only invariant we can reliably assert here is
// "no growth" relative to the baseline.
Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles);
}
}
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
{
// Allocate a group and drop the reference without disposing.
// The test relies on the group's finalizer to return the rented memory to the pool.
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(length, 100);
if (check)
{
Assert.Equal(42, g.First().Span[0]);
}
g.First().Span[0] = 42;
// Touch the memory to ensure the buffer is actually materialized/usable.
g[0].Span[0] = 42;
if (length < 512)
{
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
// repeat rental to make sure per-core buckets are also utilized.
// For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
// Repeat rental to increase the chance that per-core buckets are involved when length
// is small and allocations go through ArrayPool.
MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(length, 100);
g1.First().Span[0] = 42;
g1[0].Span[0] = 42;
g1 = null;
}
g = null;
}
[Theory]
@ -341,69 +350,63 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
[InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer
public void AllocateSingleMemoryOwner_Finalization_ReturnsToPool(int length)
{
if (TestEnvironment.IsMacOS)
{
// Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
return;
}
if (TestEnvironment.OSArchitecture == Architecture.Arm64)
{
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
return;
}
if (!TestEnvironment.RunsOnCI)
{
// This may fail in local runs resulting in high memory load.
// Remove the condition for local debugging!
return;
}
// RunTest(length.ToString());
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose();
RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose();
static void RunTest(string lengthStr)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024);
int lengthInner = int.Parse(lengthStr);
int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture);
// This test verifies pooling behavior when an `IMemoryOwner<byte>` is leaked (not disposed)
// and must be returned to the pool by finalization.
//
// We do NOT use a sentinel byte value to prove reuse because the contents of pooled buffers
// are not required to be preserved across rentals.
//
// Instead, we assert that after forcing GC+finalization, renting the same size again does not
// increase `UnmanagedMemoryHandle.TotalOutstandingHandles` above a baseline.
// Establish a baseline: allocate+dispose once so the pool has a chance to materialize/retain buffers.
allocator.Allocate<byte>(lengthInner).Dispose();
int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles;
// Leak one allocation and force finalization.
AllocateSingleAndForget(allocator, lengthInner);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
AllocateSingleAndForget(allocator, lengthInner, true);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
// Allocate again. If the leaked owner was finalized correctly and returned to the pool,
// this should not require additional unmanaged allocations (ie, the handle count must not grow).
allocator.Allocate<byte>(lengthInner).Dispose();
using IMemoryOwner<byte> g = allocator.Allocate<byte>(lengthInner);
Assert.Equal(42, g.GetSpan()[0]);
GC.KeepAlive(allocator);
// Note: we use "<=" rather than "==". The pool may legitimately trim and free retained buffers,
// reducing the handle count between baseline and check. The invariant is "no growth".
Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
{
// Allocate and intentionally do not dispose.
IMemoryOwner<byte> g = allocator.Allocate<byte>(length);
if (check)
{
Assert.Equal(42, g.GetSpan()[0]);
}
// Touch the memory to ensure the buffer is actually materialized/usable.
g.GetSpan()[0] = 42;
if (length < 512)
{
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
// repeat rental to make sure per-core buckets are also utilized.
// For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
// Repeat rental to increase the chance that per-core buckets are involved when length
// is small and allocations go through ArrayPool.
IMemoryOwner<byte> g1 = allocator.Allocate<byte>(length);
g1.GetSpan()[0] = 42;
g1 = null;
}
g = null;
}
[Fact]

29
tests/ImageSharp.Tests/MemoryAllocatorValidator.cs

@ -20,20 +20,13 @@ public static class 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()
@ -48,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)
{

9
tests/ImageSharp.Tests/TestImages.cs

@ -166,6 +166,15 @@ public static class TestImages
// 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";

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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
Loading…
Cancel
Save