From b69405f534e129159f8a69f5ecb57906904900de Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 22 Aug 2024 22:18:58 +1000 Subject: [PATCH 01/24] Add convolution API --- .../Convolution/BoxBlurExtensions.cs | 12 +-- .../Convolution/ConvolutionExtensions.cs | 89 +++++++++++++++++++ .../Convolution/GaussianBlurExtensions.cs | 2 +- .../Convolution2DProcessor{TPixel}.cs | 6 +- .../Convolution2PassProcessor{TPixel}.cs | 54 ++++++++--- .../Convolution/ConvolutionProcessor.cs | 79 ++++++++++++++++ .../ConvolutionProcessor{TPixel}.cs | 44 +++++++-- .../EdgeDetectorCompassProcessor{TPixel}.cs | 8 +- .../EdgeDetectorProcessor{TPixel}.cs | 2 +- .../GaussianBlurProcessor{TPixel}.cs | 20 +---- .../GaussianSharpenProcessor{TPixel}.cs | 18 +--- .../Convolution/ConvolutionTests.cs | 49 ++++++++++ 12 files changed, 316 insertions(+), 67 deletions(-) create mode 100644 src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs create mode 100644 src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs diff --git a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs index 6611af742b..11a7fc6a7a 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs @@ -55,9 +55,11 @@ public static class BoxBlurExtensions /// The to use when mapping the pixels outside of the border, in Y direction. /// /// The . - public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) - { - var processor = new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY); - return source.ApplyProcessor(processor, rectangle); - } + public static IImageProcessingContext BoxBlur( + this IImageProcessingContext source, + int radius, + Rectangle rectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY), rectangle); } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs new file mode 100644 index 0000000000..2980ff44f0 --- /dev/null +++ b/src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing.Extensions.Convolution; + +/// +/// Defines general convolution extensions to apply on an +/// using Mutate/Clone. +/// +public static class ConvolutionExtensions +{ + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix kernelXY) + => Convolve(source, kernelXY, false); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix kernelXY, bool preserveAlpha) + => Convolve(source, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + /// The . + public static IImageProcessingContext Convolve( + this IImageProcessingContext source, + DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY)); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix kernelXY) + => Convolve(source, rectangle, kernelXY, false); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The . + public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix kernelXY, bool preserveAlpha) + => Convolve(source, rectangle, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Applies a convolution filter to the image. + /// + /// The current image processing context. + /// The rectangle structure that specifies the portion of the image object to alter. + /// The convolution kernel to apply. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + /// The . + public static IImageProcessingContext Convolve( + this IImageProcessingContext source, + Rectangle rectangle, + DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY), rectangle); +} diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs index b851482008..64e0be3eac 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs @@ -57,7 +57,7 @@ public static class GaussianBlurExtensions /// The . public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { - var processor = new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY); + GaussianBlurProcessor processor = new(sigma, borderWrapModeX, borderWrapModeY); return source.ApplyProcessor(processor, rectangle); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs index 8a7c424815..8f5ddd1690 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -62,14 +62,14 @@ internal class Convolution2DProcessor : ImageProcessor source.CopyTo(targetPixels); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - using (var map = new KernelSamplingMap(allocator)) + using (KernelSamplingMap map = new(allocator)) { // Since the kernel sizes are identical we can use a single map. map.BuildSamplingOffsetMap(this.KernelY, interest); - var operation = new Convolution2DRowOperation( + Convolution2DRowOperation operation = new( interest, targetPixels, source.PixelBuffer, diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index 10780a21e2..cdd8ff8ae6 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -35,18 +35,48 @@ internal class Convolution2PassProcessor : ImageProcessor Rectangle sourceRectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(configuration, kernel, kernel, preserveAlpha, source, sourceRectangle, borderWrapModeX, borderWrapModeY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 1D convolution kernel. X Direction + /// The 1D convolution kernel. Y Direction + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public Convolution2PassProcessor( + Configuration configuration, + float[] kernelX, + float[] kernelY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { - this.Kernel = kernel; + this.KernelX = kernelX; + this.KernelY = kernelY; this.PreserveAlpha = preserveAlpha; this.BorderWrapModeX = borderWrapModeX; this.BorderWrapModeY = borderWrapModeY; } /// - /// Gets the convolution kernel. + /// Gets the convolution kernel. X direction. + /// + public float[] KernelX { get; } + + /// + /// Gets the convolution kernel. Y direction. /// - public float[] Kernel { get; } + public float[] KernelY { get; } /// /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. @@ -68,21 +98,21 @@ internal class Convolution2PassProcessor : ImageProcessor { using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We can create a single sampling map with the size as if we were using the non separated 2D kernel // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. - using var mapXY = new KernelSamplingMap(this.Configuration.MemoryAllocator); + using KernelSamplingMap mapXY = new(this.Configuration.MemoryAllocator); - mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY); + mapXY.BuildSamplingOffsetMap(this.KernelX.Length, this.KernelX.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY); // Horizontal convolution - var horizontalOperation = new HorizontalConvolutionRowOperation( + HorizontalConvolutionRowOperation horizontalOperation = new( interest, firstPassPixels, source.PixelBuffer, mapXY, - this.Kernel, + this.KernelX, this.Configuration, this.PreserveAlpha); @@ -92,12 +122,12 @@ internal class Convolution2PassProcessor : ImageProcessor in horizontalOperation); // Vertical convolution - var verticalOperation = new VerticalConvolutionRowOperation( + VerticalConvolutionRowOperation verticalOperation = new( interest, source.PixelBuffer, firstPassPixels, mapXY, - this.Kernel, + this.KernelY, this.Configuration, this.PreserveAlpha); @@ -140,7 +170,7 @@ internal class Convolution2PassProcessor : ImageProcessor } /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetRequiredBufferLength(Rectangle bounds) => 2 * bounds.Width; @@ -306,7 +336,7 @@ internal class Convolution2PassProcessor : ImageProcessor } /// - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetRequiredBufferLength(Rectangle bounds) => 2 * bounds.Width; diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs new file mode 100644 index 0000000000..995a5164d9 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution; + +/// +/// Defines a processor that uses a 2 dimensional matrix to perform convolution against an image. +/// +public class ConvolutionProcessor : IImageProcessor +{ + /// + /// Initializes a new instance of the class. + /// + /// The 2d gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public ConvolutionProcessor( + in DenseMatrix kernelXY, + bool preserveAlpha, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + { + this.KernelXY = kernelXY; + this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Gets the 2d convolution kernel. + /// + public DenseMatrix KernelXY { get; } + + /// + /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, + IPixel + { + if (this.KernelXY.TryGetLinearlySeparableComponents(out float[]? kernelX, out float[]? kernelY)) + { + return new Convolution2PassProcessor( + configuration, + kernelX, + kernelY, + this.PreserveAlpha, + source, + sourceRectangle, + this.BorderWrapModeX, + this.BorderWrapModeY); + } + + return new ConvolutionProcessor( + configuration, + this.KernelXY, + this.PreserveAlpha, + source, + sourceRectangle, + this.BorderWrapModeX, + this.BorderWrapModeY); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index ae79f2c31d..9b4659929b 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -31,10 +31,34 @@ internal class ConvolutionProcessor : ImageProcessor bool preserveAlpha, Image source, Rectangle sourceRectangle) + : this(configuration, kernelXY, preserveAlpha, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The 2d gradient operator. + /// Whether the convolution filter is applied to alpha as well as the color channels. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public ConvolutionProcessor( + Configuration configuration, + in DenseMatrix kernelXY, + bool preserveAlpha, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { this.KernelXY = kernelXY; this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -47,6 +71,16 @@ internal class ConvolutionProcessor : ImageProcessor /// public bool PreserveAlpha { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { @@ -55,13 +89,13 @@ internal class ConvolutionProcessor : ImageProcessor source.CopyTo(targetPixels); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - using (var map = new KernelSamplingMap(allocator)) + using (KernelSamplingMap map = new(allocator)) { - map.BuildSamplingOffsetMap(this.KernelXY, interest); + map.BuildSamplingOffsetMap(this.KernelXY.Rows, this.KernelXY.Columns, interest, this.BorderWrapModeX, this.BorderWrapModeY); - var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha); + RowOperation operation = new(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -121,7 +155,7 @@ internal class ConvolutionProcessor : ImageProcessor ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span); Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - var state = new ConvolutionState(in this.kernel, this.map); + ConvolutionState state = new(in this.kernel, this.map); int row = y - this.bounds.Y; ref int sampleRowBase = ref state.GetSampleRow((uint)row); diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index ae891f3507..a1fa4db973 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -58,12 +58,12 @@ internal class EdgeDetectorCompassProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We need a clean copy for each pass to start from using ImageFrame cleanCopy = source.Clone(); - using (var processor = new ConvolutionProcessor(this.Configuration, in this.kernels[0], true, this.Source, interest)) + using (ConvolutionProcessor processor = new(this.Configuration, in this.kernels[0], true, this.Source, interest)) { processor.Apply(source); } @@ -78,12 +78,12 @@ internal class EdgeDetectorCompassProcessor : ImageProcessor { using ImageFrame pass = cleanCopy.Clone(); - using (var processor = new ConvolutionProcessor(this.Configuration, in this.kernels[i], true, this.Source, interest)) + using (ConvolutionProcessor processor = new(this.Configuration, in this.kernels[i], true, this.Source, interest)) { processor.Apply(pass); } - var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest); + RowOperation operation = new(source.PixelBuffer, pass.PixelBuffer, interest); ParallelRowIterator.IterateRows( this.Configuration, interest, diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs index c353f46b5f..3139d24bb4 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs @@ -53,7 +53,7 @@ internal class EdgeDetectorProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new ConvolutionProcessor(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle); + using ConvolutionProcessor processor = new(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle); processor.Apply(source); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs index 6518375b9e..d762dd336b 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs @@ -12,24 +12,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution; internal class GaussianBlurProcessor : ImageProcessor where TPixel : unmanaged, IPixel { - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public GaussianBlurProcessor( - Configuration configuration, - GaussianBlurProcessor definition, - Image source, - Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - int kernelSize = (definition.Radius * 2) + 1; - this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); - } - /// /// Initializes a new instance of the class. /// @@ -72,7 +54,7 @@ internal class GaussianBlurProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + using Convolution2PassProcessor processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs index a286201dff..bdb3a4b380 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs @@ -12,22 +12,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution; internal class GaussianSharpenProcessor : ImageProcessor where TPixel : unmanaged, IPixel { - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public GaussianSharpenProcessor( - Configuration configuration, - GaussianSharpenProcessor definition, - Image source, - Rectangle sourceRectangle) - : this(configuration, definition, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) - { - } - /// /// Initializes a new instance of the class. /// @@ -70,7 +54,7 @@ internal class GaussianSharpenProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); + using Convolution2PassProcessor processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs new file mode 100644 index 0000000000..46e9ede2ad --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Extensions.Convolution; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution; + +public class ConvolutionTests +{ + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); + + public static readonly TheoryData> Values = new TheoryData> + { + // Sharpening kernel. + new float[,] + { + { -1, -1, -1 }, + { -1, 16, -1 }, + { -1, -1, -1 } + } + }; + + public static readonly string[] InputImages = + [ + TestImages.Bmp.Car, + TestImages.Png.CalliphoraPartial, + TestImages.Png.Blur + ]; + + [Theory] + [WithFileCollection(nameof(InputImages), nameof(Values), PixelTypes.Rgba32)] + public void OnFullImage(TestImageProvider provider, DenseMatrix value) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + x => x.Convolve(value), + string.Join('_', value.Data), + ValidatorComparer); + + [Theory] + [WithFileCollection(nameof(InputImages), nameof(Values), PixelTypes.Rgba32)] + public void InBox(TestImageProvider provider, DenseMatrix value) + where TPixel : unmanaged, IPixel + => provider.RunRectangleConstrainedValidatingProcessorTest( + (x, rect) => x.Convolve(rect, value), + string.Join('_', value.Data), + ValidatorComparer); +} From 065c1f93cc577540d9162f654dc5470ccf1cb6fd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 23 Aug 2024 22:11:28 +1000 Subject: [PATCH 02/24] Add test output --- .../Processing/Processors/Convolution/ConvolutionTests.cs | 1 + ...Box_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ .../InBox_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ .../InBox_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ ...age_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ .../OnFullImage_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ .../OnFullImage_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png | 3 +++ 7 files changed, 19 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs index 46e9ede2ad..0cb56b732d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution; +[GroupOutput("Convolution")] public class ConvolutionTests { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..ffa8e7cd8a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b311f117167e17b4611d525aef57146a6757db08f2a36fa093f45f237df9e1a2 +size 326809 diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..682fd8b49b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0eabd5f7dd2f258d04d3f1db8ee4d958754541e0009ae183d6e512f408e3201 +size 249497 diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..55d592a60f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/InBox_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:048b4a1679852d34d6f74f01b750461ec9db2d3e7e3aa3d2d89d48e5725aa560 +size 142367 diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..f01496ad76 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_CalliphoraPartial_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6f63d117433a93e30b9b41f68f676bb53eb1761f3c665f178ef07ec2c9626c3 +size 338527 diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..99e9737b51 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_Car_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d72a3994fc6dcc461da3af5176d5bf62c95b7abeffcbf5547172377db3b0d9ac +size 287158 diff --git a/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png new file mode 100644 index 0000000000..ecc09e70ff --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/ConvolutionTests/OnFullImage_Rgba32_blur_-1_-1_-1_-1_16_-1_-1_-1_-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3fd7906f11e87a2b0043cde179317f53e9a2bf02d9e64763be8f9923d60f057 +size 257492 From 418ce0e2472e618213e3f88bac26efee9f5b939a Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Sun, 22 Sep 2024 11:46:48 +0300 Subject: [PATCH 03/24] cleanup projects and environment variables --- src/ImageSharp/ImageSharp.csproj | 17 +--- .../Bulk/Pad3Shuffle4Channel.cs | 64 +++++++-------- .../Bulk/Shuffle3Channel.cs | 32 ++++---- .../Bulk/Shuffle4Slice3Channel.cs | 80 +++++++++---------- .../Bulk/ShuffleByte4Channel.cs | 40 +++++----- .../Bulk/ShuffleFloat4Channel.cs | 40 +++++----- .../Config.HwIntrinsics.cs | 36 ++++----- .../ImageSharp.Benchmarks.csproj | 14 +--- .../ImageSharp.Tests.ProfilingSandbox.csproj | 14 +--- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 14 +--- .../FeatureTesting/FeatureTestRunner.cs | 14 ++-- 11 files changed, 158 insertions(+), 207 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index d3c4034717..d94d02aa29 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -19,6 +19,8 @@ enable Nullable + net8.0 + true @@ -26,21 +28,6 @@ 4.0 - - - - net8.0 - true - - - - - net8.0 - true - - - - diff --git a/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs index 1b6663e70e..8728dd6715 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs @@ -47,37 +47,37 @@ public class Pad3Shuffle4Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | // |------------------------- |------------------- |-------------------------------------------------- |------ |------------:|----------:|----------:|------------:|------:|--------:|------:|------:|------:|----------:| -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 120.64 ns | 7.190 ns | 21.200 ns | 114.26 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 120.64 ns | 7.190 ns | 21.200 ns | 114.26 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4 | 2. AVX | Empty | 96 | 23.63 ns | 0.175 ns | 0.155 ns | 23.65 ns | 0.15 | 0.01 | - | - | - | - | -// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 96 | 25.25 ns | 0.356 ns | 0.298 ns | 25.27 ns | 0.17 | 0.01 | - | - | - | - | +// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 96 | 25.25 ns | 0.356 ns | 0.298 ns | 25.27 ns | 0.17 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 14.80 ns | 0.358 ns | 1.032 ns | 14.64 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 14.80 ns | 0.358 ns | 1.032 ns | 14.64 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4FastFallback | 2. AVX | Empty | 96 | 24.84 ns | 0.376 ns | 0.333 ns | 24.74 ns | 1.57 | 0.06 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 96 | 24.58 ns | 0.471 ns | 0.704 ns | 24.38 ns | 1.60 | 0.09 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 96 | 24.58 ns | 0.471 ns | 0.704 ns | 24.38 ns | 1.60 | 0.09 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 258.92 ns | 4.873 ns | 4.069 ns | 257.95 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 258.92 ns | 4.873 ns | 4.069 ns | 257.95 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4 | 2. AVX | Empty | 384 | 41.41 ns | 0.859 ns | 1.204 ns | 41.33 ns | 0.16 | 0.00 | - | - | - | - | -// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 384 | 40.74 ns | 0.848 ns | 0.793 ns | 40.48 ns | 0.16 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 384 | 40.74 ns | 0.848 ns | 0.793 ns | 40.48 ns | 0.16 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 74.50 ns | 0.490 ns | 0.383 ns | 74.49 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 74.50 ns | 0.490 ns | 0.383 ns | 74.49 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4FastFallback | 2. AVX | Empty | 384 | 40.74 ns | 0.624 ns | 0.584 ns | 40.72 ns | 0.55 | 0.01 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 384 | 38.28 ns | 0.534 ns | 0.417 ns | 38.22 ns | 0.51 | 0.01 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 384 | 38.28 ns | 0.534 ns | 0.417 ns | 38.22 ns | 0.51 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 503.91 ns | 6.466 ns | 6.048 ns | 501.58 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 503.91 ns | 6.466 ns | 6.048 ns | 501.58 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4 | 2. AVX | Empty | 768 | 62.86 ns | 0.332 ns | 0.277 ns | 62.80 ns | 0.12 | 0.00 | - | - | - | - | -// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 768 | 64.59 ns | 0.469 ns | 0.415 ns | 64.62 ns | 0.13 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 768 | 64.59 ns | 0.469 ns | 0.415 ns | 64.62 ns | 0.13 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 110.51 ns | 0.592 ns | 0.554 ns | 110.33 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 110.51 ns | 0.592 ns | 0.554 ns | 110.33 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4FastFallback | 2. AVX | Empty | 768 | 64.72 ns | 1.306 ns | 1.090 ns | 64.51 ns | 0.59 | 0.01 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 768 | 62.11 ns | 0.816 ns | 0.682 ns | 61.98 ns | 0.56 | 0.01 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 768 | 62.11 ns | 0.816 ns | 0.682 ns | 61.98 ns | 0.56 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 1,005.84 ns | 13.176 ns | 12.325 ns | 1,004.70 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 1,005.84 ns | 13.176 ns | 12.325 ns | 1,004.70 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4 | 2. AVX | Empty | 1536 | 110.05 ns | 0.256 ns | 0.214 ns | 110.04 ns | 0.11 | 0.00 | - | - | - | - | -// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 1536 | 110.23 ns | 0.545 ns | 0.483 ns | 110.09 ns | 0.11 | 0.00 | - | - | - | - | +// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 1536 | 110.23 ns | 0.545 ns | 0.483 ns | 110.09 ns | 0.11 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 220.37 ns | 1.601 ns | 1.419 ns | 220.13 ns | 1.00 | 0.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 220.37 ns | 1.601 ns | 1.419 ns | 220.13 ns | 1.00 | 0.00 | - | - | - | - | // | Pad3Shuffle4FastFallback | 2. AVX | Empty | 1536 | 111.54 ns | 2.173 ns | 2.901 ns | 111.27 ns | 0.51 | 0.01 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 1536 | 110.23 ns | 0.456 ns | 0.427 ns | 110.25 ns | 0.50 | 0.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 1536 | 110.23 ns | 0.456 ns | 0.427 ns | 110.25 ns | 0.50 | 0.00 | - | - | - | - | // 2023-02-21 // ########## @@ -94,34 +94,34 @@ public class Pad3Shuffle4Channel // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | // |------------------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|------:|------:|------:|----------:| -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 57.45 ns | 0.126 ns | 0.118 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 96 | 14.70 ns | 0.105 ns | 0.098 ns | 0.26 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 57.45 ns | 0.126 ns | 0.118 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 96 | 14.70 ns | 0.105 ns | 0.098 ns | 0.26 | - | - | - | - | // | Pad3Shuffle4 | 3. AVX | Empty | 96 | 14.63 ns | 0.070 ns | 0.062 ns | 0.25 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 12.08 ns | 0.028 ns | 0.025 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 96 | 14.04 ns | 0.050 ns | 0.044 ns | 1.16 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 12.08 ns | 0.028 ns | 0.025 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 96 | 14.04 ns | 0.050 ns | 0.044 ns | 1.16 | - | - | - | - | // | Pad3Shuffle4FastFallback | 3. AVX | Empty | 96 | 13.90 ns | 0.086 ns | 0.080 ns | 1.15 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 202.67 ns | 2.010 ns | 1.678 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 384 | 25.54 ns | 0.060 ns | 0.053 ns | 0.13 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 202.67 ns | 2.010 ns | 1.678 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 384 | 25.54 ns | 0.060 ns | 0.053 ns | 0.13 | - | - | - | - | // | Pad3Shuffle4 | 3. AVX | Empty | 384 | 25.72 ns | 0.139 ns | 0.130 ns | 0.13 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 60.35 ns | 0.080 ns | 0.071 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 384 | 25.18 ns | 0.388 ns | 0.324 ns | 0.42 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 60.35 ns | 0.080 ns | 0.071 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 384 | 25.18 ns | 0.388 ns | 0.324 ns | 0.42 | - | - | - | - | // | Pad3Shuffle4FastFallback | 3. AVX | Empty | 384 | 26.21 ns | 0.067 ns | 0.059 ns | 0.43 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 393.88 ns | 1.353 ns | 1.199 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 768 | 39.44 ns | 0.230 ns | 0.204 ns | 0.10 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 393.88 ns | 1.353 ns | 1.199 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 768 | 39.44 ns | 0.230 ns | 0.204 ns | 0.10 | - | - | - | - | // | Pad3Shuffle4 | 3. AVX | Empty | 768 | 39.51 ns | 0.108 ns | 0.101 ns | 0.10 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 112.02 ns | 0.140 ns | 0.131 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 768 | 38.60 ns | 0.091 ns | 0.080 ns | 0.34 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 112.02 ns | 0.140 ns | 0.131 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 768 | 38.60 ns | 0.091 ns | 0.080 ns | 0.34 | - | - | - | - | // | Pad3Shuffle4FastFallback | 3. AVX | Empty | 768 | 38.18 ns | 0.100 ns | 0.084 ns | 0.34 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 777.95 ns | 1.719 ns | 1.342 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 1536 | 73.11 ns | 0.090 ns | 0.075 ns | 0.09 | - | - | - | - | +// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 777.95 ns | 1.719 ns | 1.342 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 1536 | 73.11 ns | 0.090 ns | 0.075 ns | 0.09 | - | - | - | - | // | Pad3Shuffle4 | 3. AVX | Empty | 1536 | 73.41 ns | 0.125 ns | 0.117 ns | 0.09 | - | - | - | - | // | | | | | | | | | | | | | -// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 218.14 ns | 0.377 ns | 0.334 ns | 1.00 | - | - | - | - | -// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 1536 | 72.55 ns | 1.418 ns | 1.184 ns | 0.33 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 218.14 ns | 0.377 ns | 0.334 ns | 1.00 | - | - | - | - | +// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 1536 | 72.55 ns | 1.418 ns | 1.184 ns | 0.33 | - | - | - | - | // | Pad3Shuffle4FastFallback | 3. AVX | Empty | 1536 | 73.15 ns | 0.330 ns | 0.292 ns | 0.34 | - | - | - | - | diff --git a/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs index 8b7b89eb36..e4c12900f5 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs @@ -43,21 +43,21 @@ public class Shuffle3Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | // |--------------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|----------:|------:|--------:|------:|------:|------:|----------:| -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 48.46 ns | 1.034 ns | 2.438 ns | 47.46 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 48.46 ns | 1.034 ns | 2.438 ns | 47.46 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle3 | 2. AVX | Empty | 96 | 32.42 ns | 0.537 ns | 0.476 ns | 32.34 ns | 0.66 | 0.04 | - | - | - | - | -// | Shuffle3 | 3. SSE | COMPlus_EnableAVX=0 | 96 | 32.51 ns | 0.373 ns | 0.349 ns | 32.56 ns | 0.66 | 0.03 | - | - | - | - | +// | Shuffle3 | 3. SSE | DOTNET_EnableAVX=0 | 96 | 32.51 ns | 0.373 ns | 0.349 ns | 32.56 ns | 0.66 | 0.03 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 199.04 ns | 1.512 ns | 1.180 ns | 199.17 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 199.04 ns | 1.512 ns | 1.180 ns | 199.17 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle3 | 2. AVX | Empty | 384 | 71.20 ns | 2.654 ns | 7.784 ns | 69.60 ns | 0.41 | 0.02 | - | - | - | - | -// | Shuffle3 | 3. SSE | COMPlus_EnableAVX=0 | 384 | 63.23 ns | 0.569 ns | 0.505 ns | 63.21 ns | 0.32 | 0.00 | - | - | - | - | +// | Shuffle3 | 3. SSE | DOTNET_EnableAVX=0 | 384 | 63.23 ns | 0.569 ns | 0.505 ns | 63.21 ns | 0.32 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 391.28 ns | 5.087 ns | 3.972 ns | 391.22 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 391.28 ns | 5.087 ns | 3.972 ns | 391.22 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle3 | 2. AVX | Empty | 768 | 109.12 ns | 2.149 ns | 2.010 ns | 108.66 ns | 0.28 | 0.01 | - | - | - | - | -// | Shuffle3 | 3. SSE | COMPlus_EnableAVX=0 | 768 | 106.51 ns | 0.734 ns | 0.613 ns | 106.56 ns | 0.27 | 0.00 | - | - | - | - | +// | Shuffle3 | 3. SSE | DOTNET_EnableAVX=0 | 768 | 106.51 ns | 0.734 ns | 0.613 ns | 106.56 ns | 0.27 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 773.70 ns | 5.516 ns | 4.890 ns | 772.96 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 773.70 ns | 5.516 ns | 4.890 ns | 772.96 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle3 | 2. AVX | Empty | 1536 | 190.41 ns | 1.090 ns | 0.851 ns | 190.38 ns | 0.25 | 0.00 | - | - | - | - | -// | Shuffle3 | 3. SSE | COMPlus_EnableAVX=0 | 1536 | 190.94 ns | 0.985 ns | 0.769 ns | 190.85 ns | 0.25 | 0.00 | - | - | - | - | +// | Shuffle3 | 3. SSE | DOTNET_EnableAVX=0 | 1536 | 190.94 ns | 0.985 ns | 0.769 ns | 190.85 ns | 0.25 | 0.00 | - | - | - | - | // 2023-02-21 // ########## @@ -74,18 +74,18 @@ public class Shuffle3Channel // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | // |--------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|------:|------:|------:|----------:| -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 44.55 ns | 0.564 ns | 0.528 ns | 1.00 | - | - | - | - | -// | Shuffle3 | 2. SSE | COMPlus_EnableAVX=0 | 96 | 15.46 ns | 0.064 ns | 0.060 ns | 0.35 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 44.55 ns | 0.564 ns | 0.528 ns | 1.00 | - | - | - | - | +// | Shuffle3 | 2. SSE | DOTNET_EnableAVX=0 | 96 | 15.46 ns | 0.064 ns | 0.060 ns | 0.35 | - | - | - | - | // | Shuffle3 | 3. AVX | Empty | 96 | 15.18 ns | 0.056 ns | 0.053 ns | 0.34 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 155.68 ns | 0.539 ns | 0.504 ns | 1.00 | - | - | - | - | -// | Shuffle3 | 2. SSE | COMPlus_EnableAVX=0 | 384 | 30.04 ns | 0.100 ns | 0.089 ns | 0.19 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 155.68 ns | 0.539 ns | 0.504 ns | 1.00 | - | - | - | - | +// | Shuffle3 | 2. SSE | DOTNET_EnableAVX=0 | 384 | 30.04 ns | 0.100 ns | 0.089 ns | 0.19 | - | - | - | - | // | Shuffle3 | 3. AVX | Empty | 384 | 29.70 ns | 0.061 ns | 0.054 ns | 0.19 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 302.76 ns | 1.023 ns | 0.957 ns | 1.00 | - | - | - | - | -// | Shuffle3 | 2. SSE | COMPlus_EnableAVX=0 | 768 | 50.24 ns | 0.098 ns | 0.092 ns | 0.17 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 302.76 ns | 1.023 ns | 0.957 ns | 1.00 | - | - | - | - | +// | Shuffle3 | 2. SSE | DOTNET_EnableAVX=0 | 768 | 50.24 ns | 0.098 ns | 0.092 ns | 0.17 | - | - | - | - | // | Shuffle3 | 3. AVX | Empty | 768 | 49.28 ns | 0.156 ns | 0.131 ns | 0.16 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 596.53 ns | 2.675 ns | 2.503 ns | 1.00 | - | - | - | - | -// | Shuffle3 | 2. SSE | COMPlus_EnableAVX=0 | 1536 | 94.09 ns | 0.312 ns | 0.260 ns | 0.16 | - | - | - | - | +// | Shuffle3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 596.53 ns | 2.675 ns | 2.503 ns | 1.00 | - | - | - | - | +// | Shuffle3 | 2. SSE | DOTNET_EnableAVX=0 | 1536 | 94.09 ns | 0.312 ns | 0.260 ns | 0.16 | - | - | - | - | // | Shuffle3 | 3. AVX | Empty | 1536 | 93.57 ns | 0.196 ns | 0.183 ns | 0.16 | - | - | - | - | diff --git a/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs index 5ade55c73f..579e2c54db 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs @@ -47,45 +47,45 @@ public class Shuffle4Slice3Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | // |--------------------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|----------:|------:|--------:|------:|------:|------:|----------:| -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 56.44 ns | 2.843 ns | 8.382 ns | 56.70 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 56.44 ns | 2.843 ns | 8.382 ns | 56.70 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3 | 2. AVX | Empty | 128 | 27.15 ns | 0.556 ns | 0.762 ns | 27.34 ns | 0.41 | 0.03 | - | - | - | - | -// | Shuffle4Slice3 | 3. SSE | COMPlus_EnableAVX=0 | 128 | 26.36 ns | 0.321 ns | 0.268 ns | 26.26 ns | 0.38 | 0.02 | - | - | - | - | +// | Shuffle4Slice3 | 3. SSE | DOTNET_EnableAVX=0 | 128 | 26.36 ns | 0.321 ns | 0.268 ns | 26.26 ns | 0.38 | 0.02 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 25.85 ns | 0.494 ns | 0.462 ns | 25.84 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 25.85 ns | 0.494 ns | 0.462 ns | 25.84 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3FastFallback | 2. AVX | Empty | 128 | 26.15 ns | 0.113 ns | 0.106 ns | 26.16 ns | 1.01 | 0.02 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 128 | 25.57 ns | 0.078 ns | 0.061 ns | 25.56 ns | 0.99 | 0.02 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 128 | 25.57 ns | 0.078 ns | 0.061 ns | 25.56 ns | 0.99 | 0.02 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 97.47 ns | 0.327 ns | 0.289 ns | 97.35 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 97.47 ns | 0.327 ns | 0.289 ns | 97.35 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3 | 2. AVX | Empty | 256 | 32.61 ns | 0.107 ns | 0.095 ns | 32.62 ns | 0.33 | 0.00 | - | - | - | - | -// | Shuffle4Slice3 | 3. SSE | COMPlus_EnableAVX=0 | 256 | 33.21 ns | 0.169 ns | 0.150 ns | 33.15 ns | 0.34 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 3. SSE | DOTNET_EnableAVX=0 | 256 | 33.21 ns | 0.169 ns | 0.150 ns | 33.15 ns | 0.34 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 52.34 ns | 0.779 ns | 0.729 ns | 51.94 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 52.34 ns | 0.779 ns | 0.729 ns | 51.94 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3FastFallback | 2. AVX | Empty | 256 | 32.16 ns | 0.111 ns | 0.104 ns | 32.16 ns | 0.61 | 0.01 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 256 | 33.61 ns | 0.342 ns | 0.319 ns | 33.62 ns | 0.64 | 0.01 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 256 | 33.61 ns | 0.342 ns | 0.319 ns | 33.62 ns | 0.64 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 210.74 ns | 3.825 ns | 5.956 ns | 207.70 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 210.74 ns | 3.825 ns | 5.956 ns | 207.70 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3 | 2. AVX | Empty | 512 | 51.03 ns | 0.535 ns | 0.501 ns | 51.18 ns | 0.24 | 0.01 | - | - | - | - | -// | Shuffle4Slice3 | 3. SSE | COMPlus_EnableAVX=0 | 512 | 66.60 ns | 1.313 ns | 1.613 ns | 65.93 ns | 0.31 | 0.01 | - | - | - | - | +// | Shuffle4Slice3 | 3. SSE | DOTNET_EnableAVX=0 | 512 | 66.60 ns | 1.313 ns | 1.613 ns | 65.93 ns | 0.31 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 119.12 ns | 1.905 ns | 1.689 ns | 118.52 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 119.12 ns | 1.905 ns | 1.689 ns | 118.52 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3FastFallback | 2. AVX | Empty | 512 | 50.33 ns | 0.382 ns | 0.339 ns | 50.41 ns | 0.42 | 0.01 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 512 | 49.25 ns | 0.555 ns | 0.492 ns | 49.26 ns | 0.41 | 0.01 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 512 | 49.25 ns | 0.555 ns | 0.492 ns | 49.26 ns | 0.41 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 423.55 ns | 4.891 ns | 4.336 ns | 423.27 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 423.55 ns | 4.891 ns | 4.336 ns | 423.27 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3 | 2. AVX | Empty | 1024 | 77.13 ns | 1.355 ns | 2.264 ns | 76.19 ns | 0.19 | 0.01 | - | - | - | - | -// | Shuffle4Slice3 | 3. SSE | COMPlus_EnableAVX=0 | 1024 | 79.39 ns | 0.103 ns | 0.086 ns | 79.37 ns | 0.19 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 3. SSE | DOTNET_EnableAVX=0 | 1024 | 79.39 ns | 0.103 ns | 0.086 ns | 79.37 ns | 0.19 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 226.57 ns | 2.930 ns | 2.598 ns | 226.10 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 226.57 ns | 2.930 ns | 2.598 ns | 226.10 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3FastFallback | 2. AVX | Empty | 1024 | 80.25 ns | 1.647 ns | 2.082 ns | 80.98 ns | 0.35 | 0.01 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 1024 | 84.99 ns | 1.234 ns | 1.155 ns | 85.60 ns | 0.38 | 0.01 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 1024 | 84.99 ns | 1.234 ns | 1.155 ns | 85.60 ns | 0.38 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 794.96 ns | 1.735 ns | 1.538 ns | 795.15 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 794.96 ns | 1.735 ns | 1.538 ns | 795.15 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3 | 2. AVX | Empty | 2048 | 128.41 ns | 0.417 ns | 0.390 ns | 128.24 ns | 0.16 | 0.00 | - | - | - | - | -// | Shuffle4Slice3 | 3. SSE | COMPlus_EnableAVX=0 | 2048 | 127.24 ns | 0.294 ns | 0.229 ns | 127.23 ns | 0.16 | 0.00 | - | - | - | - | +// | Shuffle4Slice3 | 3. SSE | DOTNET_EnableAVX=0 | 2048 | 127.24 ns | 0.294 ns | 0.229 ns | 127.23 ns | 0.16 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 382.97 ns | 1.064 ns | 0.831 ns | 382.87 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 382.97 ns | 1.064 ns | 0.831 ns | 382.87 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Slice3FastFallback | 2. AVX | Empty | 2048 | 126.93 ns | 0.382 ns | 0.339 ns | 126.94 ns | 0.33 | 0.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 2048 | 149.36 ns | 1.875 ns | 1.754 ns | 149.33 ns | 0.39 | 0.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 2048 | 149.36 ns | 1.875 ns | 1.754 ns | 149.33 ns | 0.39 | 0.00 | - | - | - | - | // 2023-02-21 // ########## @@ -102,42 +102,42 @@ public class Shuffle4Slice3Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | // |--------------------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|------:|------:|------:|----------:| -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 45.59 ns | 0.166 ns | 0.147 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3 | 2. SSE | COMPlus_EnableAVX=0 | 128 | 15.62 ns | 0.056 ns | 0.052 ns | 0.34 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 45.59 ns | 0.166 ns | 0.147 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3 | 2. SSE | DOTNET_EnableAVX=0 | 128 | 15.62 ns | 0.056 ns | 0.052 ns | 0.34 | - | - | - | - | // | Shuffle4Slice3 | 3. AVX | Empty | 128 | 16.37 ns | 0.047 ns | 0.040 ns | 0.36 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 13.23 ns | 0.028 ns | 0.026 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 128 | 14.41 ns | 0.013 ns | 0.012 ns | 1.09 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 13.23 ns | 0.028 ns | 0.026 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 128 | 14.41 ns | 0.013 ns | 0.012 ns | 1.09 | - | - | - | - | // | Shuffle4Slice3FastFallback | 3. AVX | Empty | 128 | 14.70 ns | 0.050 ns | 0.047 ns | 1.11 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 85.48 ns | 0.192 ns | 0.179 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3 | 2. SSE | COMPlus_EnableAVX=0 | 256 | 19.18 ns | 0.230 ns | 0.204 ns | 0.22 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 85.48 ns | 0.192 ns | 0.179 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3 | 2. SSE | DOTNET_EnableAVX=0 | 256 | 19.18 ns | 0.230 ns | 0.204 ns | 0.22 | - | - | - | - | // | Shuffle4Slice3 | 3. AVX | Empty | 256 | 18.66 ns | 0.017 ns | 0.015 ns | 0.22 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 24.34 ns | 0.078 ns | 0.073 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 256 | 18.58 ns | 0.061 ns | 0.057 ns | 0.76 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 24.34 ns | 0.078 ns | 0.073 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 256 | 18.58 ns | 0.061 ns | 0.057 ns | 0.76 | - | - | - | - | // | Shuffle4Slice3FastFallback | 3. AVX | Empty | 256 | 19.23 ns | 0.018 ns | 0.016 ns | 0.79 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 165.31 ns | 0.742 ns | 0.694 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3 | 2. SSE | COMPlus_EnableAVX=0 | 512 | 28.10 ns | 0.077 ns | 0.068 ns | 0.17 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 165.31 ns | 0.742 ns | 0.694 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3 | 2. SSE | DOTNET_EnableAVX=0 | 512 | 28.10 ns | 0.077 ns | 0.068 ns | 0.17 | - | - | - | - | // | Shuffle4Slice3 | 3. AVX | Empty | 512 | 28.99 ns | 0.018 ns | 0.014 ns | 0.18 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 53.45 ns | 0.270 ns | 0.226 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 512 | 27.50 ns | 0.034 ns | 0.028 ns | 0.51 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 53.45 ns | 0.270 ns | 0.226 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 512 | 27.50 ns | 0.034 ns | 0.028 ns | 0.51 | - | - | - | - | // | Shuffle4Slice3FastFallback | 3. AVX | Empty | 512 | 28.76 ns | 0.017 ns | 0.015 ns | 0.54 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 323.87 ns | 0.549 ns | 0.487 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3 | 2. SSE | COMPlus_EnableAVX=0 | 1024 | 40.81 ns | 0.056 ns | 0.050 ns | 0.13 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 323.87 ns | 0.549 ns | 0.487 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3 | 2. SSE | DOTNET_EnableAVX=0 | 1024 | 40.81 ns | 0.056 ns | 0.050 ns | 0.13 | - | - | - | - | // | Shuffle4Slice3 | 3. AVX | Empty | 1024 | 39.95 ns | 0.075 ns | 0.067 ns | 0.12 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 101.37 ns | 0.080 ns | 0.067 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 1024 | 40.72 ns | 0.049 ns | 0.041 ns | 0.40 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 101.37 ns | 0.080 ns | 0.067 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 1024 | 40.72 ns | 0.049 ns | 0.041 ns | 0.40 | - | - | - | - | // | Shuffle4Slice3FastFallback | 3. AVX | Empty | 1024 | 39.78 ns | 0.029 ns | 0.027 ns | 0.39 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 642.95 ns | 2.067 ns | 1.933 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3 | 2. SSE | COMPlus_EnableAVX=0 | 2048 | 73.19 ns | 0.082 ns | 0.077 ns | 0.11 | - | - | - | - | +// | Shuffle4Slice3 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 642.95 ns | 2.067 ns | 1.933 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3 | 2. SSE | DOTNET_EnableAVX=0 | 2048 | 73.19 ns | 0.082 ns | 0.077 ns | 0.11 | - | - | - | - | // | Shuffle4Slice3 | 3. AVX | Empty | 2048 | 69.83 ns | 0.319 ns | 0.267 ns | 0.11 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 196.85 ns | 0.238 ns | 0.211 ns | 1.00 | - | - | - | - | -// | Shuffle4Slice3FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 2048 | 72.89 ns | 0.117 ns | 0.098 ns | 0.37 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 196.85 ns | 0.238 ns | 0.211 ns | 1.00 | - | - | - | - | +// | Shuffle4Slice3FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 2048 | 72.89 ns | 0.117 ns | 0.098 ns | 0.37 | - | - | - | - | // | Shuffle4Slice3FastFallback | 3. AVX | Empty | 2048 | 69.59 ns | 0.073 ns | 0.061 ns | 0.35 | - | - | - | - | diff --git a/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs index 911c4e0a58..6a16bb5710 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs @@ -42,25 +42,25 @@ public class ShuffleByte4Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | // |---------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|--------:|------:|------:|------:|----------:| -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 17.39 ns | 0.187 ns | 0.175 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 17.39 ns | 0.187 ns | 0.175 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 128 | 21.72 ns | 0.299 ns | 0.279 ns | 1.25 | 0.02 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 128 | 18.10 ns | 0.346 ns | 0.289 ns | 1.04 | 0.02 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 128 | 18.10 ns | 0.346 ns | 0.289 ns | 1.04 | 0.02 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 35.51 ns | 0.711 ns | 0.790 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 35.51 ns | 0.711 ns | 0.790 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 256 | 23.90 ns | 0.508 ns | 0.820 ns | 0.69 | 0.02 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 256 | 20.40 ns | 0.133 ns | 0.111 ns | 0.57 | 0.01 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 256 | 20.40 ns | 0.133 ns | 0.111 ns | 0.57 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 73.39 ns | 0.310 ns | 0.259 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 73.39 ns | 0.310 ns | 0.259 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 512 | 26.10 ns | 0.418 ns | 0.391 ns | 0.36 | 0.01 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 512 | 27.59 ns | 0.556 ns | 0.571 ns | 0.38 | 0.01 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 512 | 27.59 ns | 0.556 ns | 0.571 ns | 0.38 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 150.64 ns | 2.903 ns | 2.716 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 150.64 ns | 2.903 ns | 2.716 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 1024 | 38.67 ns | 0.801 ns | 1.889 ns | 0.24 | 0.02 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 1024 | 47.13 ns | 0.948 ns | 1.054 ns | 0.31 | 0.01 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 1024 | 47.13 ns | 0.948 ns | 1.054 ns | 0.31 | 0.01 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 315.29 ns | 5.206 ns | 6.583 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 315.29 ns | 5.206 ns | 6.583 ns | 1.00 | 0.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 2048 | 57.37 ns | 1.152 ns | 1.078 ns | 0.18 | 0.01 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 2048 | 65.75 ns | 1.198 ns | 1.600 ns | 0.21 | 0.01 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 2048 | 65.75 ns | 1.198 ns | 1.600 ns | 0.21 | 0.01 | - | - | - | - | // 2023-02-21 // ########## @@ -77,22 +77,22 @@ public class ShuffleByte4Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | // |---------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|--------:|------:|------:|------:|----------:| -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 10.76 ns | 0.033 ns | 0.029 ns | 1.00 | 0.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 128 | 11.39 ns | 0.045 ns | 0.040 ns | 1.06 | 0.01 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 10.76 ns | 0.033 ns | 0.029 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 128 | 11.39 ns | 0.045 ns | 0.040 ns | 1.06 | 0.01 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 128 | 14.05 ns | 0.029 ns | 0.024 ns | 1.31 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 32.09 ns | 0.655 ns | 1.000 ns | 1.00 | 0.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 256 | 14.03 ns | 0.047 ns | 0.041 ns | 0.44 | 0.02 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 32.09 ns | 0.655 ns | 1.000 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 256 | 14.03 ns | 0.047 ns | 0.041 ns | 0.44 | 0.02 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 256 | 15.18 ns | 0.052 ns | 0.043 ns | 0.48 | 0.03 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 59.26 ns | 0.084 ns | 0.070 ns | 1.00 | 0.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 512 | 18.80 ns | 0.036 ns | 0.034 ns | 0.32 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 59.26 ns | 0.084 ns | 0.070 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 512 | 18.80 ns | 0.036 ns | 0.034 ns | 0.32 | 0.00 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 512 | 17.69 ns | 0.038 ns | 0.034 ns | 0.30 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 112.48 ns | 0.285 ns | 0.253 ns | 1.00 | 0.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 1024 | 31.57 ns | 0.041 ns | 0.036 ns | 0.28 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 112.48 ns | 0.285 ns | 0.253 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 1024 | 31.57 ns | 0.041 ns | 0.036 ns | 0.28 | 0.00 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 1024 | 28.41 ns | 0.068 ns | 0.064 ns | 0.25 | 0.00 | - | - | - | - | // | | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 218.59 ns | 0.303 ns | 0.283 ns | 1.00 | 0.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 2048 | 53.04 ns | 0.106 ns | 0.099 ns | 0.24 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 218.59 ns | 0.303 ns | 0.283 ns | 1.00 | 0.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 2048 | 53.04 ns | 0.106 ns | 0.099 ns | 0.24 | 0.00 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 2048 | 34.74 ns | 0.061 ns | 0.054 ns | 0.16 | 0.00 | - | - | - | - | diff --git a/tests/ImageSharp.Benchmarks/Bulk/ShuffleFloat4Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/ShuffleFloat4Channel.cs index 5bb3cf9165..7cc894486f 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/ShuffleFloat4Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/ShuffleFloat4Channel.cs @@ -42,25 +42,25 @@ public class ShuffleFloat4Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | // |---------------- |------------------- |-------------------------------------------------- |------ |-----------:|----------:|----------:|------:|------:|------:|------:|----------:| -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 63.647 ns | 0.5475 ns | 0.4853 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 63.647 ns | 0.5475 ns | 0.4853 ns | 1.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 128 | 9.818 ns | 0.1457 ns | 0.1292 ns | 0.15 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 128 | 15.267 ns | 0.1005 ns | 0.0940 ns | 0.24 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 128 | 15.267 ns | 0.1005 ns | 0.0940 ns | 0.24 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 125.586 ns | 1.9312 ns | 1.8064 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 125.586 ns | 1.9312 ns | 1.8064 ns | 1.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 256 | 15.878 ns | 0.1983 ns | 0.1758 ns | 0.13 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 256 | 29.170 ns | 0.2925 ns | 0.2442 ns | 0.23 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 256 | 29.170 ns | 0.2925 ns | 0.2442 ns | 0.23 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 263.859 ns | 2.6660 ns | 2.3634 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 263.859 ns | 2.6660 ns | 2.3634 ns | 1.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 512 | 29.452 ns | 0.3334 ns | 0.3118 ns | 0.11 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 512 | 52.912 ns | 0.1932 ns | 0.1713 ns | 0.20 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 512 | 52.912 ns | 0.1932 ns | 0.1713 ns | 0.20 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 495.717 ns | 1.9850 ns | 1.8567 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 495.717 ns | 1.9850 ns | 1.8567 ns | 1.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 1024 | 53.757 ns | 0.3212 ns | 0.2847 ns | 0.11 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 1024 | 107.815 ns | 1.6201 ns | 1.3528 ns | 0.22 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 1024 | 107.815 ns | 1.6201 ns | 1.3528 ns | 0.22 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 980.134 ns | 3.7407 ns | 3.1237 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 980.134 ns | 3.7407 ns | 3.1237 ns | 1.00 | - | - | - | - | // | Shuffle4Channel | 2. AVX | Empty | 2048 | 105.120 ns | 0.6140 ns | 0.5443 ns | 0.11 | - | - | - | - | -// | Shuffle4Channel | 3. SSE | COMPlus_EnableAVX=0 | 2048 | 216.473 ns | 2.3268 ns | 2.0627 ns | 0.22 | - | - | - | - | +// | Shuffle4Channel | 3. SSE | DOTNET_EnableAVX=0 | 2048 | 216.473 ns | 2.3268 ns | 2.0627 ns | 0.22 | - | - | - | - | // 2023-02-21 // ########## @@ -77,22 +77,22 @@ public class ShuffleFloat4Channel // // | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | // |---------------- |------------------- |-------------------------------------------------- |------ |-----------:|----------:|----------:|------:|------:|------:|------:|----------:| -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 128 | 57.819 ns | 0.2360 ns | 0.1970 ns | 1.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 128 | 11.564 ns | 0.0234 ns | 0.0195 ns | 0.20 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 128 | 57.819 ns | 0.2360 ns | 0.1970 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 128 | 11.564 ns | 0.0234 ns | 0.0195 ns | 0.20 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 128 | 7.770 ns | 0.0696 ns | 0.0617 ns | 0.13 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 256 | 105.282 ns | 0.2713 ns | 0.2405 ns | 1.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 256 | 19.867 ns | 0.0393 ns | 0.0348 ns | 0.19 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 256 | 105.282 ns | 0.2713 ns | 0.2405 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 256 | 19.867 ns | 0.0393 ns | 0.0348 ns | 0.19 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 256 | 17.586 ns | 0.0582 ns | 0.0544 ns | 0.17 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 512 | 200.799 ns | 0.5678 ns | 0.5033 ns | 1.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 512 | 41.137 ns | 0.1524 ns | 0.1351 ns | 0.20 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 512 | 200.799 ns | 0.5678 ns | 0.5033 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 512 | 41.137 ns | 0.1524 ns | 0.1351 ns | 0.20 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 512 | 24.040 ns | 0.0445 ns | 0.0395 ns | 0.12 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1024 | 401.046 ns | 0.5865 ns | 0.5199 ns | 1.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 1024 | 94.904 ns | 0.4633 ns | 0.4334 ns | 0.24 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1024 | 401.046 ns | 0.5865 ns | 0.5199 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 1024 | 94.904 ns | 0.4633 ns | 0.4334 ns | 0.24 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 1024 | 68.456 ns | 0.1192 ns | 0.0996 ns | 0.17 | - | - | - | - | // | | | | | | | | | | | | | -// | Shuffle4Channel | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 2048 | 772.297 ns | 0.6270 ns | 0.5558 ns | 1.00 | - | - | - | - | -// | Shuffle4Channel | 2. SSE | COMPlus_EnableAVX=0 | 2048 | 184.561 ns | 0.4319 ns | 0.4040 ns | 0.24 | - | - | - | - | +// | Shuffle4Channel | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 2048 | 772.297 ns | 0.6270 ns | 0.5558 ns | 1.00 | - | - | - | - | +// | Shuffle4Channel | 2. SSE | DOTNET_EnableAVX=0 | 2048 | 184.561 ns | 0.4319 ns | 0.4040 ns | 0.24 | - | - | - | - | // | Shuffle4Channel | 3. AVX | Empty | 2048 | 133.634 ns | 1.7864 ns | 1.8345 ns | 0.17 | - | - | - | - | diff --git a/tests/ImageSharp.Benchmarks/Config.HwIntrinsics.cs b/tests/ImageSharp.Benchmarks/Config.HwIntrinsics.cs index 92f8917cf8..e21d0c76d8 100644 --- a/tests/ImageSharp.Benchmarks/Config.HwIntrinsics.cs +++ b/tests/ImageSharp.Benchmarks/Config.HwIntrinsics.cs @@ -33,24 +33,24 @@ public partial class Config // `FeatureSIMD` ends up impacting all SIMD support(including `System.Numerics`) but not things // like `LZCNT`, `BMI1`, or `BMI2` // `EnableSSE3_4` is a legacy switch that exists for compat and is basically the same as `EnableSSE3` - private const string EnableAES = "COMPlus_EnableAES"; - private const string EnableAVX = "COMPlus_EnableAVX"; - private const string EnableAVX2 = "COMPlus_EnableAVX2"; - private const string EnableBMI1 = "COMPlus_EnableBMI1"; - private const string EnableBMI2 = "COMPlus_EnableBMI2"; - private const string EnableFMA = "COMPlus_EnableFMA"; - private const string EnableHWIntrinsic = "COMPlus_EnableHWIntrinsic"; - private const string EnableLZCNT = "COMPlus_EnableLZCNT"; - private const string EnablePCLMULQDQ = "COMPlus_EnablePCLMULQDQ"; - private const string EnablePOPCNT = "COMPlus_EnablePOPCNT"; - private const string EnableSSE = "COMPlus_EnableSSE"; - private const string EnableSSE2 = "COMPlus_EnableSSE2"; - private const string EnableSSE3 = "COMPlus_EnableSSE3"; - private const string EnableSSE3_4 = "COMPlus_EnableSSE3_4"; - private const string EnableSSE41 = "COMPlus_EnableSSE41"; - private const string EnableSSE42 = "COMPlus_EnableSSE42"; - private const string EnableSSSE3 = "COMPlus_EnableSSSE3"; - private const string FeatureSIMD = "COMPlus_FeatureSIMD"; + private const string EnableAES = "DOTNET_EnableAES"; + private const string EnableAVX = "DOTNET_EnableAVX"; + private const string EnableAVX2 = "DOTNET_EnableAVX2"; + private const string EnableBMI1 = "DOTNET_EnableBMI1"; + private const string EnableBMI2 = "DOTNET_EnableBMI2"; + private const string EnableFMA = "DOTNET_EnableFMA"; + private const string EnableHWIntrinsic = "DOTNET_EnableHWIntrinsic"; + private const string EnableLZCNT = "DOTNET_EnableLZCNT"; + private const string EnablePCLMULQDQ = "DOTNET_EnablePCLMULQDQ"; + private const string EnablePOPCNT = "DOTNET_EnablePOPCNT"; + private const string EnableSSE = "DOTNET_EnableSSE"; + private const string EnableSSE2 = "DOTNET_EnableSSE2"; + private const string EnableSSE3 = "DOTNET_EnableSSE3"; + private const string EnableSSE3_4 = "DOTNET_EnableSSE3_4"; + private const string EnableSSE41 = "DOTNET_EnableSSE41"; + private const string EnableSSE42 = "DOTNET_EnableSSE42"; + private const string EnableSSSE3 = "DOTNET_EnableSSSE3"; + private const string FeatureSIMD = "DOTNET_FeatureSIMD"; public class HwIntrinsics_SSE_AVX : Config { diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 4408159ef4..a91c78576b 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -2,6 +2,7 @@ + net8.0 ImageSharp.Benchmarks Exe SixLabors.ImageSharp.Benchmarks @@ -20,19 +21,6 @@ CA1822 - - - - net8.0 - - - - - net8.0 - - - - diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index b93d011910..cf028ad756 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -2,6 +2,7 @@ + net8.0 ImageSharp.Tests.ProfilingSandbox A cross-platform library for processing of image files written in C# Exe @@ -16,19 +17,6 @@ false - - - - net8.0 - - - - - net8.0 - - - - diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 41e6e525f8..08fa822111 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -2,6 +2,7 @@ + net8.0 True SixLabors.ImageSharp.Tests AnyCPU;x64;x86;ARM64 @@ -9,19 +10,6 @@ Debug;Release - - - - net8.0 - - - - - net8.0 - - - - diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index 07ad5e8f03..63126dcbca 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -104,7 +104,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -148,7 +148,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -192,7 +192,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -241,7 +241,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -288,7 +288,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -333,7 +333,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, @@ -379,7 +379,7 @@ public static class FeatureTestRunner ProcessStartInfo processStartInfo = new(); if (intrinsic.Key != HwIntrinsics.AllowAll) { - processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; + processStartInfo.Environment[$"DOTNET_{intrinsic.Value}"] = "0"; RemoteExecutor.Invoke( action, From 9cc5ff2ffd7b95712f941a2f1a925e2813223e2a Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:57:08 +0300 Subject: [PATCH 04/24] add net9.0 --- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 08fa822111..1a89545a41 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -2,7 +2,6 @@ - net8.0 True SixLabors.ImageSharp.Tests AnyCPU;x64;x86;ARM64 @@ -10,7 +9,20 @@ Debug;Release - + + + + net8.0;net9.0 + + + + + net8.0 + + + + + From 56b28fcdd0053eaa61e107e42c7543dbb3008fc6 Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:03:26 +0300 Subject: [PATCH 05/24] net9.0 --- src/ImageSharp/ImageSharp.csproj | 17 +++++++++++++++-- .../ImageSharp.Benchmarks.csproj | 14 +++++++++++++- .../ImageSharp.Tests.ProfilingSandbox.csproj | 14 +++++++++++++- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index d94d02aa29..ec05e662d8 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -19,8 +19,6 @@ enable Nullable - net8.0 - true @@ -28,6 +26,21 @@ 4.0 + + + + net8.0;net9.0 + true + + + + + net8.0 + true + + + + diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index a91c78576b..a705b24b28 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -2,7 +2,6 @@ - net8.0 ImageSharp.Benchmarks Exe SixLabors.ImageSharp.Benchmarks @@ -21,6 +20,19 @@ CA1822 + + + + net8.0;net9.0 + + + + + net8.0 + + + + diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index cf028ad756..832f3d171f 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -2,7 +2,6 @@ - net8.0 ImageSharp.Tests.ProfilingSandbox A cross-platform library for processing of image files written in C# Exe @@ -17,6 +16,19 @@ false + + + + net8.0;net9.0 + + + + + net8.0 + + + + diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 1a89545a41..9af4f41a81 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -22,7 +22,7 @@ - + From e6cf1e5413b2e18ed9634ed7f95fb9c30846c32e Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:34:53 +0000 Subject: [PATCH 06/24] Revert .net9 --- src/ImageSharp/ImageSharp.csproj | 2 +- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 2 +- .../ImageSharp.Tests.ProfilingSandbox.csproj | 2 +- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index ec05e662d8..d3c4034717 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -29,7 +29,7 @@ - net8.0;net9.0 + net8.0 true diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index a705b24b28..4408159ef4 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -23,7 +23,7 @@ - net8.0;net9.0 + net8.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 832f3d171f..b93d011910 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -19,7 +19,7 @@ - net8.0;net9.0 + net8.0 diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 9af4f41a81..41e6e525f8 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -12,7 +12,7 @@ - net8.0;net9.0 + net8.0 From 911480d6ebc43e006aa1a0a5038afcb9c9642e04 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 16 Oct 2024 19:10:37 +1000 Subject: [PATCH 07/24] Fix #2779 buffer overrun --- src/ImageSharp/Image.WrapMemory.cs | 9 +++--- .../Image/ImageTests.WrapMemory.cs | 30 +++++++++++++------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index d8cea246fe..03bec8bc6a 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -50,7 +50,7 @@ public abstract partial class Image { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); - Guard.IsTrue(pixelMemory.Length >= width * height, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(pixelMemory.Length >= (long)width * height, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemory); return new Image(configuration, memorySource, width, height, metadata); @@ -145,7 +145,7 @@ public abstract partial class Image { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); - Guard.IsTrue(pixelMemoryOwner.Memory.Length >= width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= (long)width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); return new Image(configuration, memorySource, width, height, metadata); @@ -232,7 +232,7 @@ public abstract partial class Image ByteMemoryManager memoryManager = new(byteMemory); - Guard.IsTrue(memoryManager.Memory.Length >= width * height, nameof(byteMemory), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(memoryManager.Memory.Length >= (long)width * height, nameof(byteMemory), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); return new Image(configuration, memorySource, width, height, metadata); @@ -422,10 +422,11 @@ public abstract partial class Image Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must be not null"); Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); + Guard.MustBeLessThanOrEqualTo(height * (long)width, int.MaxValue, "Total amount of pixels exceeds int.MaxValue"); UnmanagedMemoryManager memoryManager = new(pointer, width * height); - Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes, memoryManager.Memory.Span.Length, nameof(bufferSizeInBytes)); + Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / sizeof(TPixel), memoryManager.Memory.Span.Length, nameof(bufferSizeInBytes)); MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); return new Image(configuration, memorySource, width, height, metadata); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 9aaefa41ef..46ae146942 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -294,8 +294,11 @@ public partial class ImageTests } } - [Fact] - public unsafe void WrapMemory_Throws_OnTooLessWrongSize() + [Theory] + [InlineData(20, 5, 5)] + [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] + public unsafe void WrapMemory_Throws_OnTooLessWrongSize(int size, int width, int height) { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); @@ -306,7 +309,7 @@ public partial class ImageTests { try { - using var image = Image.WrapMemory(cfg, ptr, 24, 5, 5, metaData); + using var image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData); } catch (Exception e) { @@ -317,24 +320,30 @@ public partial class ImageTests Assert.IsType(thrownException); } - [Fact] - public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect() + [Theory] + [InlineData(25, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [InlineData(1024, 32, 32)] + [InlineData(2048, 32, 32)] + public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect(int size, int width, int height) { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); - var array = new Rgba32[25]; + var array = new Rgba32[size]; fixed (void* ptr = array) { - using (var image = Image.WrapMemory(cfg, ptr, 25, 5, 5, metaData)) + using (var image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData)) { Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); Span imageSpan = imageMem.Span; + Span sourceSpan = array.AsSpan(0, width * height); ref Rgba32 pixel0 = ref imageSpan[0]; - Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); + Assert.True(Unsafe.AreSame(ref sourceSpan[0], ref pixel0)); ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; - Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1)); + Assert.True(Unsafe.AreSame(ref sourceSpan[sourceSpan.Length - 1], ref pixel_1)); Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); @@ -395,6 +404,7 @@ public partial class ImageTests [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_MemoryOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; @@ -430,6 +440,7 @@ public partial class ImageTests [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_IMemoryOwnerOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; @@ -476,6 +487,7 @@ public partial class ImageTests [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_IMemoryOwnerOfByte_InvalidSize(int size, int height, int width) { var array = new byte[size * Unsafe.SizeOf()]; From b8fcdda44a3ca0ab32b70cc59fc1d72c2038ff20 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 21 Oct 2024 20:19:10 +1000 Subject: [PATCH 08/24] Backport - WEBP : Use Correct Width With AlphaDecoder --- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 7 ++++--- .../Formats/Webp/Lossless/LosslessUtils.cs | 11 +++++++---- .../Formats/Webp/Lossless/WebpLosslessDecoder.cs | 7 ++++++- .../Formats/WebP/WebpEncoderTests.cs | 15 +++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/issues/Issue2801.webp | 3 +++ 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 tests/Images/Input/Webp/issues/Issue2801.webp diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index 63571617fb..ca419e619e 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -181,7 +181,7 @@ internal class AlphaDecoder : IDisposable else { this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); - this.ExtractAlphaRows(this.Vp8LDec); + this.ExtractAlphaRows(this.Vp8LDec, this.Width); } } @@ -255,14 +255,15 @@ internal class AlphaDecoder : IDisposable /// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet. /// /// The VP8L decoder. - private void ExtractAlphaRows(Vp8LDecoder dec) + /// The image width. + private void ExtractAlphaRows(Vp8LDecoder dec, int width) { int numRowsToProcess = dec.Height; - int width = dec.Width; Span input = dec.Pixels.Memory.Span; Span output = this.Alpha.Memory.Span; // Extract alpha (which is stored in the green plane). + // the final width (!= dec->width_) int pixelCount = width * numRowsToProcess; WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); ExtractGreen(input, output, pixelCount); diff --git a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs index 024adb7c23..5287f0b753 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -269,7 +269,11 @@ internal static unsafe class LosslessUtils /// /// The transform data contains color table size and the entries in the color table. /// The pixel data to apply the reverse transform on. - public static void ColorIndexInverseTransform(Vp8LTransform transform, Span pixelData) + /// The resulting pixel data with the reversed transformation data. + public static void ColorIndexInverseTransform( + Vp8LTransform transform, + Span pixelData, + Span outputSpan) { int bitsPerPixel = 8 >> transform.Bits; int width = transform.XSize; @@ -282,7 +286,6 @@ internal static unsafe class LosslessUtils int countMask = pixelsPerByte - 1; int bitMask = (1 << bitsPerPixel) - 1; - uint[] decodedPixelData = new uint[width * height]; int pixelDataPos = 0; for (int y = 0; y < height; y++) { @@ -298,12 +301,12 @@ internal static unsafe class LosslessUtils packedPixels = GetArgbIndex(pixelData[pixelDataPos++]); } - decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; + outputSpan[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; packedPixels >>= bitsPerPixel; } } - decodedPixelData.AsSpan().CopyTo(pixelData); + outputSpan.CopyTo(pixelData); } else { diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index e4c2a7ddf6..0f366cb1e7 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -684,6 +684,7 @@ internal sealed class WebpLosslessDecoder List transforms = decoder.Transforms; for (int i = transforms.Count - 1; i >= 0; i--) { + // TODO: Review these 1D allocations. They could conceivably exceed limits. Vp8LTransform transform = transforms[i]; switch (transform.TransformType) { @@ -701,7 +702,11 @@ internal sealed class WebpLosslessDecoder LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); break; case Vp8LTransformType.ColorIndexingTransform: - LosslessUtils.ColorIndexInverseTransform(transform, pixelData); + using (IMemoryOwner output = memoryAllocator.Allocate(pixelData.Length, AllocationOptions.Clean)) + { + LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan()); + } + break; } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d1d83ffb9a..031a9ba059 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -514,6 +514,21 @@ public class WebpEncoderTests image.VerifyEncoder(provider, "webp", string.Empty, encoder); } + // https://github.com/SixLabors/ImageSharp/issues/2801 + [Theory] + [WithFile(Lossy.Issue2801, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Issue2801(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + WebpEncoder encoder = new() + { + Quality = 100 + }; + + using Image image = provider.GetImage(); + image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.TolerantPercentage(0.0994F)); + } + public static void RunEncodeLossy_WithPeakImage() { TestImageProvider provider = TestImageProvider.File(TestImageLossyFullPath); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 847aac3478..a0e951e70d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -824,6 +824,7 @@ public static class TestImages public const string Issue2257 = "Webp/issues/Issue2257.webp"; public const string Issue2670 = "Webp/issues/Issue2670.webp"; public const string Issue2763 = "Webp/issues/Issue2763.png"; + public const string Issue2801 = "Webp/issues/Issue2801.webp"; } } diff --git a/tests/Images/Input/Webp/issues/Issue2801.webp b/tests/Images/Input/Webp/issues/Issue2801.webp new file mode 100644 index 0000000000..a3b5fee6e0 --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2801.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e90a0d853ddf70d823d8da44eb6c57081e955b1fb7f436a1fd88ca5e5c75a003 +size 261212 From 1e58db2205cf5b05b40ad988e5449c4e0da9d605 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 22 Oct 2024 17:25:35 +1000 Subject: [PATCH 09/24] Remove ChunkedMemoryStream --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- .../Sections/GifXmpApplicationExtension.cs | 10 +- src/ImageSharp/Formats/ImageDecoder.cs | 12 +- src/ImageSharp/Formats/ImageEncoder.cs | 7 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 585 ------------------ src/ImageSharp/Image.FromStream.cs | 5 +- .../Formats/WebP/WebpEncoderTests.cs | 19 + .../IO/ChunkedMemoryStreamTests.cs | 373 ----------- .../Image/NonSeekableStream.cs | 6 +- 9 files changed, 36 insertions(+), 983 deletions(-) delete mode 100644 src/ImageSharp/IO/ChunkedMemoryStream.cs delete mode 100644 tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 68f4e5fa2d..c45450a47b 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -317,7 +317,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp && !this.skipMetadata) { - GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream); if (extension.Data.Length > 0) { this.metadata!.XmpProfile = new XmpProfile(extension.Data); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 1c1127c3be..8bd8497eea 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif; @@ -26,11 +25,10 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension /// Reads the XMP metadata from the specified stream. /// /// The stream to read from. - /// The memory allocator. /// The XMP metadata - public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) + public static GifXmpApplicationExtension Read(Stream stream) { - byte[] xmpBytes = ReadXmpData(stream, allocator); + byte[] xmpBytes = ReadXmpData(stream); // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 @@ -71,9 +69,9 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) + private static byte[] ReadXmpData(Stream stream) { - using ChunkedMemoryStream bytes = new(allocator); + using MemoryStream bytes = new(); // XMP data doesn't have a fixed length nor is there an indicator of the length. // So we simply read one byte at a time until we hit the 0x0 value at the end diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 549a28d409..03cfa27cfb 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -210,7 +209,7 @@ public abstract class ImageDecoder : IImageDecoder } Configuration configuration = options.Configuration; - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + using MemoryStream memoryStream = new(); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -266,11 +265,6 @@ public abstract class ImageDecoder : IImageDecoder return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } - if (stream is ChunkedMemoryStream cms) - { - return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); - } - return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } @@ -282,9 +276,11 @@ public abstract class ImageDecoder : IImageDecoder { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + + await using MemoryStream memoryStream = new(); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; + return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); } diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index deb527f698..34d34c3637 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -48,8 +47,8 @@ public abstract class ImageEncoder : IImageEncoder } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); - this.Encode(image, stream, cancellationToken); + using MemoryStream ms = new(); + this.Encode(image, ms, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -65,7 +64,7 @@ public abstract class ImageEncoder : IImageEncoder } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await using MemoryStream ms = new(); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs deleted file mode 100644 index 2534548141..0000000000 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ /dev/null @@ -1,585 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.IO; - -/// -/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized. -/// Chunks are allocated by the assigned via the constructor -/// and is designed to take advantage of buffer pooling when available. -/// -internal sealed class ChunkedMemoryStream : Stream -{ - // The memory allocator. - private readonly MemoryAllocator allocator; - - // Data - private MemoryChunk? memoryChunk; - - // The total number of allocated chunks - private int chunkCount; - - // The length of the largest contiguous buffer that can be handled by the allocator. - private readonly int allocatorCapacity; - - // Has the stream been disposed. - private bool isDisposed; - - // Current chunk to write to - private MemoryChunk? writeChunk; - - // Offset into chunk to write to - private int writeOffset; - - // Current chunk to read from - private MemoryChunk? readChunk; - - // Offset into chunk to read from - private int readOffset; - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - public ChunkedMemoryStream(MemoryAllocator allocator) - { - this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); - this.allocator = allocator; - } - - /// - public override bool CanRead => !this.isDisposed; - - /// - public override bool CanSeek => !this.isDisposed; - - /// - public override bool CanWrite => !this.isDisposed; - - /// - public override long Length - { - get - { - this.EnsureNotDisposed(); - - int length = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - MemoryChunk? next = chunk.Next; - if (next != null) - { - length += chunk.Length; - } - else - { - length += this.writeOffset; - } - - chunk = next; - } - - return length; - } - } - - /// - public override long Position - { - get - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - return 0; - } - - int pos = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != this.readChunk && chunk is not null) - { - pos += chunk.Length; - chunk = chunk.Next; - } - - pos += this.readOffset; - - return pos; - } - - set - { - this.EnsureNotDisposed(); - - if (value < 0) - { - ThrowArgumentOutOfRange(nameof(value)); - } - - // Back up current position in case new position is out of range - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = null; - this.readOffset = 0; - - int leftUntilAtPos = (int)value; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - if ((leftUntilAtPos < chunk.Length) - || ((leftUntilAtPos == chunk.Length) - && (chunk.Next is null))) - { - // The desired position is in this chunk - this.readChunk = chunk; - this.readOffset = leftUntilAtPos; - break; - } - - leftUntilAtPos -= chunk.Length; - chunk = chunk.Next; - } - - if (this.readChunk is null) - { - // Position is out of range - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - } - } - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override long Seek(long offset, SeekOrigin origin) - { - this.EnsureNotDisposed(); - - switch (origin) - { - case SeekOrigin.Begin: - this.Position = offset; - break; - - case SeekOrigin.Current: - this.Position += offset; - break; - - case SeekOrigin.End: - this.Position = this.Length + offset; - break; - default: - ThrowInvalidSeek(); - break; - } - - return this.Position; - } - - /// - public override void SetLength(long value) - => throw new NotSupportedException(); - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - try - { - this.isDisposed = true; - if (disposing) - { - ReleaseMemoryChunks(this.memoryChunk); - } - - this.memoryChunk = null; - this.writeChunk = null; - this.readChunk = null; - this.chunkCount = 0; - } - finally - { - base.Dispose(disposing); - } - } - - /// - public override void Flush() - { - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(byte[] buffer, int offset, int count) - { - Guard.NotNull(buffer, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); - Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); - - const string bufferMessage = "Offset subtracted from the buffer length is less than count."; - Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - - return this.ReadImpl(buffer.AsSpan(offset, count)); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(Span buffer) => this.ReadImpl(buffer); - - private int ReadImpl(Span buffer) - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - int bytesRead = 0; - int offset = 0; - int count = buffer.Length; - while (count > 0) - { - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - } - - int readCount = Math.Min(count, chunkSize - this.readOffset); - chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); - offset += readCount; - count -= readCount; - this.readOffset += readCount; - bytesRead += readCount; - } - - return bytesRead; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int ReadByte() - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - return -1; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - } - - return chunkBuffer.GetSpan()[this.readOffset++]; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(byte[] buffer, int offset, int count) - { - Guard.NotNull(buffer, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); - Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); - - const string bufferMessage = "Offset subtracted from the buffer length is less than count."; - Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - - this.WriteImpl(buffer.AsSpan(offset, count)); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); - - private void WriteImpl(ReadOnlySpan buffer) - { - this.EnsureNotDisposed(); - - if (this.memoryChunk is null) - { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; - } - - Guard.NotNull(this.writeChunk); - - Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); - int chunkSize = this.writeChunk.Length; - int count = buffer.Length; - int offset = 0; - while (count > 0) - { - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer.GetSpan(); - chunkSize = this.writeChunk.Length; - } - - int copyCount = Math.Min(count, chunkSize - this.writeOffset); - buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); - - offset += copyCount; - count -= copyCount; - this.writeOffset += copyCount; - } - } - - /// - public override void WriteByte(byte value) - { - this.EnsureNotDisposed(); - - if (this.memoryChunk is null) - { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; - } - - Guard.NotNull(this.writeChunk); - - IMemoryOwner chunkBuffer = this.writeChunk.Buffer; - int chunkSize = this.writeChunk.Length; - - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer; - } - - chunkBuffer.GetSpan()[this.writeOffset++] = value; - } - - /// - /// Copy entire buffer into an array. - /// - /// The . - public byte[] ToArray() - { - int length = (int)this.Length; // This will throw if stream is closed - byte[] copy = new byte[this.Length]; - - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - this.Read(copy, 0, length); - - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - - return copy; - } - - /// - /// Write remainder of this stream to another stream. - /// - /// The stream to write to. - public void WriteTo(Stream stream) - { - this.EnsureNotDisposed(); - - Guard.NotNull(stream, nameof(stream)); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - // Following code mirrors Read() logic (readChunk/readOffset should - // point just past last byte of last chunk when done) - // loop until end of chunks is found - while (true) - { - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - } - - int writeCount = chunkSize - this.readOffset; - stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); - this.readOffset = chunkSize; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureNotDisposed() - { - if (this.isDisposed) - { - ThrowDisposed(); - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private MemoryChunk AllocateMemoryChunk() - { - // Tweak our buffer sizes to take the minimum of the provided buffer sizes - // or the allocator buffer capacity which provides us with the largest - // available contiguous buffer size. - IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); - - return new MemoryChunk(buffer) - { - Next = null, - Length = buffer.Length() - }; - } - - private static void ReleaseMemoryChunks(MemoryChunk? chunk) - { - while (chunk != null) - { - chunk.Dispose(); - chunk = chunk.Next; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetChunkSize(int i) - { - // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. - // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 -#pragma warning disable IDE1006 // Naming Styles - const int _128K = 1 << 17; - const int _4M = 1 << 22; - return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; -#pragma warning restore IDE1006 // Naming Styles - } - - private sealed class MemoryChunk : IDisposable - { - private bool isDisposed; - - public MemoryChunk(IMemoryOwner buffer) => this.Buffer = buffer; - - public IMemoryOwner Buffer { get; } - - public MemoryChunk? Next { get; set; } - - public int Length { get; init; } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - this.Buffer.Dispose(); - } - - this.isDisposed = true; - } - } - - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 63f9e64f6c..c73d2880a2 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -301,7 +300,7 @@ public abstract partial class Image return action(stream); } - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + using MemoryStream memoryStream = new(); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -343,7 +342,7 @@ public abstract partial class Image return await action(stream, cancellationToken).ConfigureAwait(false); } - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await using MemoryStream memoryStream = new(); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d1d83ffb9a..dd94606084 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -529,6 +529,25 @@ public class WebpEncoderTests [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public void CanSave_NonSeekableStream(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + using MemoryStream memoryStream = new(); + using NonSeekableStream nonSeekable = new(memoryStream); + + image.Save(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs deleted file mode 100644 index 1803cfddb9..0000000000 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - -namespace SixLabors.ImageSharp.Tests.IO; - -/// -/// Tests for the class. -/// -public class ChunkedMemoryStreamTests -{ - /// - /// The default length in bytes of each buffer chunk when allocating large buffers. - /// - private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb - - /// - /// The default length in bytes of each buffer chunk when allocating small buffers. - /// - private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb - - private readonly MemoryAllocator allocator; - - public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; - - [Fact] - public void MemoryStream_GetPositionTest_Negative() - { - using var ms = new ChunkedMemoryStream(this.allocator); - long iCurrentPos = ms.Position; - for (int i = -1; i > -6; i--) - { - Assert.Throws(() => ms.Position = i); - Assert.Equal(ms.Position, iCurrentPos); - } - } - - [Fact] - public void MemoryStream_ReadTest_Negative() - { - var ms2 = new ChunkedMemoryStream(this.allocator); - - Assert.Throws(() => ms2.Read(null, 0, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, -1)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 2, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 2)); - - ms2.Dispose(); - - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 1)); - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i], cms.ReadByte()); - } - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteBufferTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - byte[] buffer = new byte[2]; - for (int i = 0; i < expected.Length; i += 2) - { - cms.Read(buffer); - Assert.Equal(expected[i], buffer[0]); - Assert.Equal(expected[i + 1], buffer[1]); - } - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteBufferSpanTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - Span buffer = new byte[2]; - for (int i = 0; i < expected.Length; i += 2) - { - cms.Read(buffer); - Assert.Equal(expected[i], buffer[0]); - Assert.Equal(expected[i + 1], buffer[1]); - } - } - - [Fact] - public void MemoryStream_WriteToTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - // [] Write to memoryStream, check the memoryStream - ms2.Write(bytArr, 0, bytArr.Length); - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - - // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - ms2.Write(bytArr, 0, bytArr.Length); - ms2.WriteTo(ms3); - ms3.Position = 0; - bytArrRet = new byte[(int)ms3.Length]; - ms3.Read(bytArrRet, 0, (int)ms3.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteToSpanTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - // [] Write to memoryStream, check the memoryStream - ms2.Write(bytArr, 0, bytArr.Length); - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - - // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) - { - Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - ms2.Write(bytArr, 0, bytArr.Length); - ms2.WriteTo(ms3); - ms3.Position = 0; - bytArrRet = new byte[(int)ms3.Length]; - ms3.Read(bytArrRet, 0, (int)ms3.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteByteTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - for (int i = 0; i < bytArr.Length; i++) - { - ms2.WriteByte(bytArr[i]); - } - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteToTests_Negative() - { - using var ms2 = new ChunkedMemoryStream(this.allocator); - Assert.Throws(() => ms2.WriteTo(null)); - - ms2.Write(new byte[] { 1 }, 0, 1); - var readonlyStream = new MemoryStream(new byte[1028], false); - Assert.Throws(() => ms2.WriteTo(readonlyStream)); - - readonlyStream.Dispose(); - - // [] Pass in a closed stream - Assert.Throws(() => ms2.WriteTo(readonlyStream)); - } - - [Fact] - public void MemoryStream_CopyTo_Invalid() - { - ChunkedMemoryStream memoryStream; - const string bufferSize = nameof(bufferSize); - using (memoryStream = new ChunkedMemoryStream(this.allocator)) - { - const string destination = nameof(destination); - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null)); - - // Validate the destination parameter first. - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); - - // Then bufferSize. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); - } - - // After the Stream is disposed, we should fail on all CopyTos. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); - - ChunkedMemoryStream disposedStream = memoryStream; - - // We should throw first for the source being disposed... - Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); - - // Then for the destination being disposed. - memoryStream = new ChunkedMemoryStream(this.allocator); - Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); - memoryStream.Dispose(); - } - - [Theory] - [MemberData(nameof(CopyToData))] - public void CopyTo(Stream source, byte[] expected) - { - using var destination = new ChunkedMemoryStream(this.allocator); - source.CopyTo(destination); - Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. - Assert.Equal(expected, destination.ToArray()); - } - - public static IEnumerable GetAllTestImages() - { - IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) - .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); - - var result = new List(); - foreach (string path in allImageFiles) - { - result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); - } - - return result; - } - - public static IEnumerable AllTestImages = GetAllTestImages(); - - [Theory] - [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] - public void DecoderIntegrationTest(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - if (!TestEnvironment.Is64BitProcess) - { - return; - } - - Image expected; - try - { - expected = provider.GetImage(); - } - catch - { - // The image is invalid - return; - } - - string fullPath = Path.Combine( - TestEnvironment.InputImagesDirectoryFullPath, - ((TestImageProvider.FileProvider)provider).FilePath); - - using FileStream fs = File.OpenRead(fullPath); - using var nonSeekableStream = new NonSeekableStream(fs); - - var actual = Image.Load(nonSeekableStream); - - ImageComparer.Exact.VerifySimilarity(expected, actual); - } - - public static IEnumerable CopyToData() - { - // Stream is positioned @ beginning of data - byte[] data1 = new byte[] { 1, 2, 3 }; - var stream1 = new MemoryStream(data1); - - yield return new object[] { stream1, data1 }; - - // Stream is positioned in the middle of data - byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; - var stream2 = new MemoryStream(data2) { Position = 1 }; - - yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; - - // Stream is positioned after end of data - byte[] data3 = data2; - var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; - - yield return new object[] { stream3, Array.Empty() }; - } - - private MemoryStream CreateTestStream(int length) - { - byte[] buffer = new byte[length]; - var random = new Random(); - random.NextBytes(buffer); - - return new MemoryStream(buffer); - } -} diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 4b1f6e1568..2941490e9a 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ internal class NonSeekableStream : Stream public override bool CanSeek => false; - public override bool CanWrite => false; + public override bool CanWrite => this.dataStream.CanWrite; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ internal class NonSeekableStream : Stream => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => throw new NotImplementedException(); + => this.dataStream.Write(buffer, offset, count); } From 1a150780fd5dc79194e02568503a12b36cbc42cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 22 Oct 2024 19:37:44 +1000 Subject: [PATCH 10/24] Update BufferedStreams.cs --- .../General/IO/BufferedStreams.cs | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs index 2a926d1cd8..a7b22e7ab8 100644 --- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs @@ -19,12 +19,8 @@ public class BufferedStreams private MemoryStream stream4; private MemoryStream stream5; private MemoryStream stream6; - private ChunkedMemoryStream chunkedMemoryStream1; - private ChunkedMemoryStream chunkedMemoryStream2; private BufferedReadStream bufferedStream1; private BufferedReadStream bufferedStream2; - private BufferedReadStream bufferedStream3; - private BufferedReadStream bufferedStream4; private BufferedReadStreamWrapper bufferedStreamWrap1; private BufferedReadStreamWrapper bufferedStreamWrap2; @@ -39,18 +35,8 @@ public class BufferedStreams this.stream6 = new MemoryStream(this.buffer); this.stream6 = new MemoryStream(this.buffer); - this.chunkedMemoryStream1 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); - this.chunkedMemoryStream1.Write(this.buffer); - this.chunkedMemoryStream1.Position = 0; - - this.chunkedMemoryStream2 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); - this.chunkedMemoryStream2.Write(this.buffer); - this.chunkedMemoryStream2.Position = 0; - this.bufferedStream1 = new BufferedReadStream(Configuration.Default, this.stream3); this.bufferedStream2 = new BufferedReadStream(Configuration.Default, this.stream4); - this.bufferedStream3 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream1); - this.bufferedStream4 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream2); this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5); this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6); } @@ -60,12 +46,8 @@ public class BufferedStreams { this.bufferedStream1?.Dispose(); this.bufferedStream2?.Dispose(); - this.bufferedStream3?.Dispose(); - this.bufferedStream4?.Dispose(); this.bufferedStreamWrap1?.Dispose(); this.bufferedStreamWrap2?.Dispose(); - this.chunkedMemoryStream1?.Dispose(); - this.chunkedMemoryStream2?.Dispose(); this.stream1?.Dispose(); this.stream2?.Dispose(); this.stream3?.Dispose(); @@ -104,21 +86,6 @@ public class BufferedStreams return r; } - [Benchmark] - public int BufferedReadStreamChunkedRead() - { - int r = 0; - BufferedReadStream reader = this.bufferedStream3; - byte[] b = this.chunk2; - - for (int i = 0; i < reader.Length / 2; i++) - { - r += reader.Read(b, 0, 2); - } - - return r; - } - [Benchmark] public int BufferedReadStreamWrapRead() { @@ -162,20 +129,6 @@ public class BufferedStreams return r; } - [Benchmark] - public int BufferedReadStreamChunkedReadByte() - { - int r = 0; - BufferedReadStream reader = this.bufferedStream4; - - for (int i = 0; i < reader.Length; i++) - { - r += reader.ReadByte(); - } - - return r; - } - [Benchmark] public int BufferedReadStreamWrapReadByte() { From c418bb0def340b1a2c25d09e27b0404842f58227 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 09:50:03 +1000 Subject: [PATCH 11/24] Revert "Remove ChunkedMemoryStream" This reverts commit 1e58db2205cf5b05b40ad988e5449c4e0da9d605. --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- .../Sections/GifXmpApplicationExtension.cs | 10 +- src/ImageSharp/Formats/ImageDecoder.cs | 12 +- src/ImageSharp/Formats/ImageEncoder.cs | 7 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 585 ++++++++++++++++++ src/ImageSharp/Image.FromStream.cs | 5 +- .../Formats/WebP/WebpEncoderTests.cs | 19 - .../IO/ChunkedMemoryStreamTests.cs | 373 +++++++++++ .../Image/NonSeekableStream.cs | 6 +- 9 files changed, 983 insertions(+), 36 deletions(-) create mode 100644 src/ImageSharp/IO/ChunkedMemoryStream.cs create mode 100644 tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index c45450a47b..68f4e5fa2d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -317,7 +317,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp && !this.skipMetadata) { - GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); if (extension.Data.Length > 0) { this.metadata!.XmpProfile = new XmpProfile(extension.Data); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 8bd8497eea..1c1127c3be 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif; @@ -25,10 +26,11 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension /// Reads the XMP metadata from the specified stream. /// /// The stream to read from. + /// The memory allocator. /// The XMP metadata - public static GifXmpApplicationExtension Read(Stream stream) + public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) { - byte[] xmpBytes = ReadXmpData(stream); + byte[] xmpBytes = ReadXmpData(stream, allocator); // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 @@ -69,9 +71,9 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream) + private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) { - using MemoryStream bytes = new(); + using ChunkedMemoryStream bytes = new(allocator); // XMP data doesn't have a fixed length nor is there an indicator of the length. // So we simply read one byte at a time until we hit the 0x0 value at the end diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 03cfa27cfb..549a28d409 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -209,7 +210,7 @@ public abstract class ImageDecoder : IImageDecoder } Configuration configuration = options.Configuration; - using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -265,6 +266,11 @@ public abstract class ImageDecoder : IImageDecoder return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } + if (stream is ChunkedMemoryStream cms) + { + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); + } + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } @@ -276,11 +282,9 @@ public abstract class ImageDecoder : IImageDecoder { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - - await using MemoryStream memoryStream = new(); + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; - return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); } diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index 34d34c3637..deb527f698 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -47,8 +48,8 @@ public abstract class ImageEncoder : IImageEncoder } else { - using MemoryStream ms = new(); - this.Encode(image, ms, cancellationToken); + using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + this.Encode(image, stream, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -64,7 +65,7 @@ public abstract class ImageEncoder : IImageEncoder } else { - await using MemoryStream ms = new(); + using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs new file mode 100644 index 0000000000..2534548141 --- /dev/null +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -0,0 +1,585 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.IO; + +/// +/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized. +/// Chunks are allocated by the assigned via the constructor +/// and is designed to take advantage of buffer pooling when available. +/// +internal sealed class ChunkedMemoryStream : Stream +{ + // The memory allocator. + private readonly MemoryAllocator allocator; + + // Data + private MemoryChunk? memoryChunk; + + // The total number of allocated chunks + private int chunkCount; + + // The length of the largest contiguous buffer that can be handled by the allocator. + private readonly int allocatorCapacity; + + // Has the stream been disposed. + private bool isDisposed; + + // Current chunk to write to + private MemoryChunk? writeChunk; + + // Offset into chunk to write to + private int writeOffset; + + // Current chunk to read from + private MemoryChunk? readChunk; + + // Offset into chunk to read from + private int readOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public ChunkedMemoryStream(MemoryAllocator allocator) + { + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); + this.allocator = allocator; + } + + /// + public override bool CanRead => !this.isDisposed; + + /// + public override bool CanSeek => !this.isDisposed; + + /// + public override bool CanWrite => !this.isDisposed; + + /// + public override long Length + { + get + { + this.EnsureNotDisposed(); + + int length = 0; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != null) + { + MemoryChunk? next = chunk.Next; + if (next != null) + { + length += chunk.Length; + } + else + { + length += this.writeOffset; + } + + chunk = next; + } + + return length; + } + } + + /// + public override long Position + { + get + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + return 0; + } + + int pos = 0; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != this.readChunk && chunk is not null) + { + pos += chunk.Length; + chunk = chunk.Next; + } + + pos += this.readOffset; + + return pos; + } + + set + { + this.EnsureNotDisposed(); + + if (value < 0) + { + ThrowArgumentOutOfRange(nameof(value)); + } + + // Back up current position in case new position is out of range + MemoryChunk? backupReadChunk = this.readChunk; + int backupReadOffset = this.readOffset; + + this.readChunk = null; + this.readOffset = 0; + + int leftUntilAtPos = (int)value; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != null) + { + if ((leftUntilAtPos < chunk.Length) + || ((leftUntilAtPos == chunk.Length) + && (chunk.Next is null))) + { + // The desired position is in this chunk + this.readChunk = chunk; + this.readOffset = leftUntilAtPos; + break; + } + + leftUntilAtPos -= chunk.Length; + chunk = chunk.Next; + } + + if (this.readChunk is null) + { + // Position is out of range + this.readChunk = backupReadChunk; + this.readOffset = backupReadOffset; + } + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override long Seek(long offset, SeekOrigin origin) + { + this.EnsureNotDisposed(); + + switch (origin) + { + case SeekOrigin.Begin: + this.Position = offset; + break; + + case SeekOrigin.Current: + this.Position += offset; + break; + + case SeekOrigin.End: + this.Position = this.Length + offset; + break; + default: + ThrowInvalidSeek(); + break; + } + + return this.Position; + } + + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + try + { + this.isDisposed = true; + if (disposing) + { + ReleaseMemoryChunks(this.memoryChunk); + } + + this.memoryChunk = null; + this.writeChunk = null; + this.readChunk = null; + this.chunkCount = 0; + } + finally + { + base.Dispose(disposing); + } + } + + /// + public override void Flush() + { + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + return this.ReadImpl(buffer.AsSpan(offset, count)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(Span buffer) => this.ReadImpl(buffer); + + private int ReadImpl(Span buffer) + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return 0; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + int bytesRead = 0; + int offset = 0; + int count = buffer.Length; + while (count > 0) + { + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + break; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + } + + int readCount = Math.Min(count, chunkSize - this.readOffset); + chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); + offset += readCount; + count -= readCount; + this.readOffset += readCount; + bytesRead += readCount; + } + + return bytesRead; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int ReadByte() + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return 0; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + return -1; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + } + + return chunkBuffer.GetSpan()[this.readOffset++]; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + this.WriteImpl(buffer.AsSpan(offset, count)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); + + private void WriteImpl(ReadOnlySpan buffer) + { + this.EnsureNotDisposed(); + + if (this.memoryChunk is null) + { + this.memoryChunk = this.AllocateMemoryChunk(); + this.writeChunk = this.memoryChunk; + this.writeOffset = 0; + } + + Guard.NotNull(this.writeChunk); + + Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); + int chunkSize = this.writeChunk.Length; + int count = buffer.Length; + int offset = 0; + while (count > 0) + { + if (this.writeOffset == chunkSize) + { + // Allocate a new chunk if the current one is full + this.writeChunk.Next = this.AllocateMemoryChunk(); + this.writeChunk = this.writeChunk.Next; + this.writeOffset = 0; + chunkBuffer = this.writeChunk.Buffer.GetSpan(); + chunkSize = this.writeChunk.Length; + } + + int copyCount = Math.Min(count, chunkSize - this.writeOffset); + buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); + + offset += copyCount; + count -= copyCount; + this.writeOffset += copyCount; + } + } + + /// + public override void WriteByte(byte value) + { + this.EnsureNotDisposed(); + + if (this.memoryChunk is null) + { + this.memoryChunk = this.AllocateMemoryChunk(); + this.writeChunk = this.memoryChunk; + this.writeOffset = 0; + } + + Guard.NotNull(this.writeChunk); + + IMemoryOwner chunkBuffer = this.writeChunk.Buffer; + int chunkSize = this.writeChunk.Length; + + if (this.writeOffset == chunkSize) + { + // Allocate a new chunk if the current one is full + this.writeChunk.Next = this.AllocateMemoryChunk(); + this.writeChunk = this.writeChunk.Next; + this.writeOffset = 0; + chunkBuffer = this.writeChunk.Buffer; + } + + chunkBuffer.GetSpan()[this.writeOffset++] = value; + } + + /// + /// Copy entire buffer into an array. + /// + /// The . + public byte[] ToArray() + { + int length = (int)this.Length; // This will throw if stream is closed + byte[] copy = new byte[this.Length]; + + MemoryChunk? backupReadChunk = this.readChunk; + int backupReadOffset = this.readOffset; + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + this.Read(copy, 0, length); + + this.readChunk = backupReadChunk; + this.readOffset = backupReadOffset; + + return copy; + } + + /// + /// Write remainder of this stream to another stream. + /// + /// The stream to write to. + public void WriteTo(Stream stream) + { + this.EnsureNotDisposed(); + + Guard.NotNull(stream, nameof(stream)); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + // Following code mirrors Read() logic (readChunk/readOffset should + // point just past last byte of last chunk when done) + // loop until end of chunks is found + while (true) + { + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + break; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + } + + int writeCount = chunkSize - this.readOffset; + stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); + this.readOffset = chunkSize; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureNotDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private MemoryChunk AllocateMemoryChunk() + { + // Tweak our buffer sizes to take the minimum of the provided buffer sizes + // or the allocator buffer capacity which provides us with the largest + // available contiguous buffer size. + IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); + + return new MemoryChunk(buffer) + { + Next = null, + Length = buffer.Length() + }; + } + + private static void ReleaseMemoryChunks(MemoryChunk? chunk) + { + while (chunk != null) + { + chunk.Dispose(); + chunk = chunk.Next; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 +#pragma warning disable IDE1006 // Naming Styles + const int _128K = 1 << 17; + const int _4M = 1 << 22; + return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; +#pragma warning restore IDE1006 // Naming Styles + } + + private sealed class MemoryChunk : IDisposable + { + private bool isDisposed; + + public MemoryChunk(IMemoryOwner buffer) => this.Buffer = buffer; + + public IMemoryOwner Buffer { get; } + + public MemoryChunk? Next { get; set; } + + public int Length { get; init; } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Buffer.Dispose(); + } + + this.isDisposed = true; + } + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index c73d2880a2..63f9e64f6c 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -300,7 +301,7 @@ public abstract partial class Image return action(stream); } - using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -342,7 +343,7 @@ public abstract partial class Image return await action(stream, cancellationToken).ConfigureAwait(false); } - await using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 96cdfe8539..031a9ba059 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -544,25 +544,6 @@ public class WebpEncoderTests [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); - [Theory] - [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] - public void CanSave_NonSeekableStream(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - WebpEncoder encoder = new(); - - using MemoryStream seekable = new(); - image.Save(seekable, encoder); - - using MemoryStream memoryStream = new(); - using NonSeekableStream nonSeekable = new(memoryStream); - - image.Save(nonSeekable, encoder); - - Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); - } - private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs new file mode 100644 index 0000000000..1803cfddb9 --- /dev/null +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -0,0 +1,373 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.IO; + +/// +/// Tests for the class. +/// +public class ChunkedMemoryStreamTests +{ + /// + /// The default length in bytes of each buffer chunk when allocating large buffers. + /// + private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb + + /// + /// The default length in bytes of each buffer chunk when allocating small buffers. + /// + private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb + + private readonly MemoryAllocator allocator; + + public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; + + [Fact] + public void MemoryStream_GetPositionTest_Negative() + { + using var ms = new ChunkedMemoryStream(this.allocator); + long iCurrentPos = ms.Position; + for (int i = -1; i > -6; i--) + { + Assert.Throws(() => ms.Position = i); + Assert.Equal(ms.Position, iCurrentPos); + } + } + + [Fact] + public void MemoryStream_ReadTest_Negative() + { + var ms2 = new ChunkedMemoryStream(this.allocator); + + Assert.Throws(() => ms2.Read(null, 0, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, -1)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 2, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 2)); + + ms2.Dispose(); + + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 1)); + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], cms.ReadByte()); + } + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteBufferTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + byte[] buffer = new byte[2]; + for (int i = 0; i < expected.Length; i += 2) + { + cms.Read(buffer); + Assert.Equal(expected[i], buffer[0]); + Assert.Equal(expected[i + 1], buffer[1]); + } + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteBufferSpanTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + Span buffer = new byte[2]; + for (int i = 0; i < expected.Length; i += 2) + { + cms.Read(buffer); + Assert.Equal(expected[i], buffer[0]); + Assert.Equal(expected[i + 1], buffer[1]); + } + } + + [Fact] + public void MemoryStream_WriteToTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + // [] Write to memoryStream, check the memoryStream + ms2.Write(bytArr, 0, bytArr.Length); + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + + // [] Write to memoryStream, check the memoryStream + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (var ms3 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); + ms3.Position = 0; + bytArrRet = new byte[(int)ms3.Length]; + ms3.Read(bytArrRet, 0, (int)ms3.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteToSpanTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + Span bytArrRet; + Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + // [] Write to memoryStream, check the memoryStream + ms2.Write(bytArr, 0, bytArr.Length); + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + + // [] Write to memoryStream, check the memoryStream + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (var ms3 = new ChunkedMemoryStream(this.allocator)) + { + Span bytArrRet; + Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); + ms3.Position = 0; + bytArrRet = new byte[(int)ms3.Length]; + ms3.Read(bytArrRet, 0, (int)ms3.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteByteTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + for (int i = 0; i < bytArr.Length; i++) + { + ms2.WriteByte(bytArr[i]); + } + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteToTests_Negative() + { + using var ms2 = new ChunkedMemoryStream(this.allocator); + Assert.Throws(() => ms2.WriteTo(null)); + + ms2.Write(new byte[] { 1 }, 0, 1); + var readonlyStream = new MemoryStream(new byte[1028], false); + Assert.Throws(() => ms2.WriteTo(readonlyStream)); + + readonlyStream.Dispose(); + + // [] Pass in a closed stream + Assert.Throws(() => ms2.WriteTo(readonlyStream)); + } + + [Fact] + public void MemoryStream_CopyTo_Invalid() + { + ChunkedMemoryStream memoryStream; + const string bufferSize = nameof(bufferSize); + using (memoryStream = new ChunkedMemoryStream(this.allocator)) + { + const string destination = nameof(destination); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null)); + + // Validate the destination parameter first. + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); + + // Then bufferSize. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + } + + // After the Stream is disposed, we should fail on all CopyTos. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + + ChunkedMemoryStream disposedStream = memoryStream; + + // We should throw first for the source being disposed... + Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); + + // Then for the destination being disposed. + memoryStream = new ChunkedMemoryStream(this.allocator); + Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); + memoryStream.Dispose(); + } + + [Theory] + [MemberData(nameof(CopyToData))] + public void CopyTo(Stream source, byte[] expected) + { + using var destination = new ChunkedMemoryStream(this.allocator); + source.CopyTo(destination); + Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. + Assert.Equal(expected, destination.ToArray()); + } + + public static IEnumerable GetAllTestImages() + { + IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) + .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); + + var result = new List(); + foreach (string path in allImageFiles) + { + result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); + } + + return result; + } + + public static IEnumerable AllTestImages = GetAllTestImages(); + + [Theory] + [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] + public void DecoderIntegrationTest(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + Image expected; + try + { + expected = provider.GetImage(); + } + catch + { + // The image is invalid + return; + } + + string fullPath = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + ((TestImageProvider.FileProvider)provider).FilePath); + + using FileStream fs = File.OpenRead(fullPath); + using var nonSeekableStream = new NonSeekableStream(fs); + + var actual = Image.Load(nonSeekableStream); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + + public static IEnumerable CopyToData() + { + // Stream is positioned @ beginning of data + byte[] data1 = new byte[] { 1, 2, 3 }; + var stream1 = new MemoryStream(data1); + + yield return new object[] { stream1, data1 }; + + // Stream is positioned in the middle of data + byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; + var stream2 = new MemoryStream(data2) { Position = 1 }; + + yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; + + // Stream is positioned after end of data + byte[] data3 = data2; + var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; + + yield return new object[] { stream3, Array.Empty() }; + } + + private MemoryStream CreateTestStream(int length) + { + byte[] buffer = new byte[length]; + var random = new Random(); + random.NextBytes(buffer); + + return new MemoryStream(buffer); + } +} diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 2941490e9a..4b1f6e1568 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ internal class NonSeekableStream : Stream public override bool CanSeek => false; - public override bool CanWrite => this.dataStream.CanWrite; + public override bool CanWrite => false; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ internal class NonSeekableStream : Stream => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => this.dataStream.Write(buffer, offset, count); + => throw new NotImplementedException(); } From 48645f8b4772a4d3dc07bcc5d96105fe979f94e7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 12:18:16 +1000 Subject: [PATCH 12/24] Rewrite ChunkedMemoryStream --- src/ImageSharp/Formats/ImageEncoder.cs | 4 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 628 ++++++++---------- .../Formats/WebP/WebpEncoderTests.cs | 38 ++ .../IO/ChunkedMemoryStreamTests.cs | 93 +-- .../Image/NonSeekableStream.cs | 6 +- 5 files changed, 361 insertions(+), 408 deletions(-) diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index deb527f698..27a4f11cdd 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -49,7 +49,7 @@ public abstract class ImageEncoder : IImageEncoder else { using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); - this.Encode(image, stream, cancellationToken); + this.Encode(image, ms, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -65,7 +65,7 @@ public abstract class ImageEncoder : IImageEncoder } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 2534548141..f178764603 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Collections; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; @@ -12,44 +13,24 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -internal sealed class ChunkedMemoryStream : Stream +/// Provides an in-memory stream composed of non-contiguous chunks. +public class ChunkedMemoryStream : Stream { - // The memory allocator. - private readonly MemoryAllocator allocator; + private readonly MemoryChunkBuffer memoryChunkBuffer; + private readonly byte[] singleReadBuffer = new byte[1]; - // Data - private MemoryChunk? memoryChunk; - - // The total number of allocated chunks - private int chunkCount; - - // The length of the largest contiguous buffer that can be handled by the allocator. - private readonly int allocatorCapacity; - - // Has the stream been disposed. + private long length; + private long position; + private int currentChunk; + private int currentChunkIndex; private bool isDisposed; - // Current chunk to write to - private MemoryChunk? writeChunk; - - // Offset into chunk to write to - private int writeOffset; - - // Current chunk to read from - private MemoryChunk? readChunk; - - // Offset into chunk to read from - private int readOffset; - /// /// Initializes a new instance of the class. /// /// The memory allocator. public ChunkedMemoryStream(MemoryAllocator allocator) - { - this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); - this.allocator = allocator; - } + => this.memoryChunkBuffer = new(allocator); /// public override bool CanRead => !this.isDisposed; @@ -66,25 +47,7 @@ internal sealed class ChunkedMemoryStream : Stream get { this.EnsureNotDisposed(); - - int length = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - MemoryChunk? next = chunk.Next; - if (next != null) - { - length += chunk.Length; - } - else - { - length += this.writeOffset; - } - - chunk = next; - } - - return length; + return this.length; } } @@ -94,93 +57,35 @@ internal sealed class ChunkedMemoryStream : Stream get { this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - return 0; - } - - int pos = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != this.readChunk && chunk is not null) - { - pos += chunk.Length; - chunk = chunk.Next; - } - - pos += this.readOffset; - - return pos; + return this.position; } set { this.EnsureNotDisposed(); - - if (value < 0) - { - ThrowArgumentOutOfRange(nameof(value)); - } - - // Back up current position in case new position is out of range - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = null; - this.readOffset = 0; - - int leftUntilAtPos = (int)value; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - if ((leftUntilAtPos < chunk.Length) - || ((leftUntilAtPos == chunk.Length) - && (chunk.Next is null))) - { - // The desired position is in this chunk - this.readChunk = chunk; - this.readOffset = leftUntilAtPos; - break; - } - - leftUntilAtPos -= chunk.Length; - chunk = chunk.Next; - } - - if (this.readChunk is null) - { - // Position is out of range - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - } + this.SetPosition(value); } } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Flush() + { + } + + /// public override long Seek(long offset, SeekOrigin origin) { this.EnsureNotDisposed(); - switch (origin) + this.Position = origin switch { - case SeekOrigin.Begin: - this.Position = offset; - break; - - case SeekOrigin.Current: - this.Position += offset; - break; - - case SeekOrigin.End: - this.Position = this.Length + offset; - break; - default: - ThrowInvalidSeek(); - break; - } + SeekOrigin.Begin => (int)offset, + SeekOrigin.Current => (int)(this.Position + offset), + SeekOrigin.End => (int)(this.Length + offset), + _ => throw new ArgumentOutOfRangeException(nameof(offset)), + }; - return this.Position; + return this.position; } /// @@ -188,41 +93,23 @@ internal sealed class ChunkedMemoryStream : Stream => throw new NotSupportedException(); /// - protected override void Dispose(bool disposing) + public override int ReadByte() { - if (this.isDisposed) - { - return; - } - - try - { - this.isDisposed = true; - if (disposing) - { - ReleaseMemoryChunks(this.memoryChunk); - } - - this.memoryChunk = null; - this.writeChunk = null; - this.readChunk = null; - this.chunkCount = 0; - } - finally + this.EnsureNotDisposed(); + if (this.position >= this.length) { - base.Dispose(disposing); + return -1; } - } - /// - public override void Flush() - { + _ = this.Read(this.singleReadBuffer, 0, 1); + return this.singleReadBuffer[^1]; } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int Read(byte[] buffer, int offset, int count) { + this.EnsureNotDisposed(); + Guard.NotNull(buffer, nameof(buffer)); Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); @@ -230,111 +117,63 @@ internal sealed class ChunkedMemoryStream : Stream const string bufferMessage = "Offset subtracted from the buffer length is less than count."; Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - return this.ReadImpl(buffer.AsSpan(offset, count)); + return this.Read(buffer.AsSpan(offset, count)); } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(Span buffer) => this.ReadImpl(buffer); - - private int ReadImpl(Span buffer) + public override int Read(Span buffer) { this.EnsureNotDisposed(); - if (this.readChunk is null) + int offset = 0; + int count = buffer.Length; + int bytesRead = 0; + long bytesToRead = this.length - this.position; + if (bytesToRead > count) { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; + bytesToRead = count; } - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) + if (bytesToRead <= 0) { - chunkSize = this.writeOffset; + // Already at the end of the stream, nothing to read + return 0; } - int bytesRead = 0; - int offset = 0; - int count = buffer.Length; - while (count > 0) + while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { - if (this.readOffset == chunkSize) + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToRead, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; } - int readCount = Math.Min(count, chunkSize - this.readOffset); - chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); - offset += readCount; - count -= readCount; - this.readOffset += readCount; - bytesRead += readCount; - } - - return bytesRead; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int ReadByte() - { - this.EnsureNotDisposed(); + // Read n bytes from the current chunk + chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n).CopyTo(buffer.Slice(offset, n)); + bytesToRead -= n; + offset += n; + bytesRead += n; - if (this.readChunk is null) - { - if (this.memoryChunk is null) + if (moveToNextChunk) { - return 0; + this.currentChunkIndex = 0; + this.currentChunk++; } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) + else { - return -1; + this.currentChunkIndex += n; } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; } - return chunkBuffer.GetSpan()[this.readOffset++]; + this.position += bytesRead; + return bytesRead; } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Write(byte[] buffer, int offset, int count) { Guard.NotNull(buffer, nameof(buffer)); @@ -344,157 +183,200 @@ internal sealed class ChunkedMemoryStream : Stream const string bufferMessage = "Offset subtracted from the buffer length is less than count."; Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - this.WriteImpl(buffer.AsSpan(offset, count)); + this.Write(buffer.AsSpan(offset, count)); } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); - - private void WriteImpl(ReadOnlySpan buffer) + public override void Write(ReadOnlySpan buffer) { this.EnsureNotDisposed(); - if (this.memoryChunk is null) + int offset = 0; + int count = buffer.Length; + int bytesWritten = 0; + long bytesToWrite = this.memoryChunkBuffer.Length - this.position; + + // Ensure we have enough capacity to write the data. + while (bytesToWrite < count) { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; + this.memoryChunkBuffer.Expand(); + bytesToWrite = this.memoryChunkBuffer.Length - this.position; } - Guard.NotNull(this.writeChunk); + if (bytesToWrite > count) + { + bytesToWrite = count; + } - Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); - int chunkSize = this.writeChunk.Length; - int count = buffer.Length; - int offset = 0; - while (count > 0) + while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { - if (this.writeOffset == chunkSize) + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToWrite, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer.GetSpan(); - chunkSize = this.writeChunk.Length; + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; } - int copyCount = Math.Min(count, chunkSize - this.writeOffset); - buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); + // Write n bytes to the current chunk + buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.currentChunkIndex, n)); + bytesToWrite -= n; + offset += n; + bytesWritten += n; - offset += copyCount; - count -= copyCount; - this.writeOffset += copyCount; + if (moveToNextChunk) + { + this.currentChunkIndex = 0; + this.currentChunk++; + } + else + { + this.currentChunkIndex += n; + } } + + this.position += bytesWritten; + this.length += bytesWritten; } - /// - public override void WriteByte(byte value) + /// + /// Writes the entire contents of this memory stream to another stream. + /// + /// The stream to write this memory stream to. + /// is . + /// The current or target stream is closed. + public void WriteTo(Stream stream) { + Guard.NotNull(stream, nameof(stream)); this.EnsureNotDisposed(); - if (this.memoryChunk is null) + this.Position = 0; + + int bytesRead = 0; + long bytesToRead = this.length - this.position; + if (bytesToRead <= 0) { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; + // Already at the end of the stream, nothing to read + return; } - Guard.NotNull(this.writeChunk); + while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + { + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToRead, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) + { + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; + } - IMemoryOwner chunkBuffer = this.writeChunk.Buffer; - int chunkSize = this.writeChunk.Length; + // Read n bytes from the current chunk + stream.Write(chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n)); + bytesToRead -= n; + bytesRead += n; - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer; + if (moveToNextChunk) + { + this.currentChunkIndex = 0; + this.currentChunk++; + } + else + { + this.currentChunkIndex += n; + } } - chunkBuffer.GetSpan()[this.writeOffset++] = value; + this.position += bytesRead; } /// - /// Copy entire buffer into an array. + /// Writes the stream contents to a byte array, regardless of the property. /// - /// The . + /// A new . public byte[] ToArray() { - int length = (int)this.Length; // This will throw if stream is closed - byte[] copy = new byte[this.Length]; - - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - this.Read(copy, 0, length); - - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; + this.EnsureNotDisposed(); + long position = this.position; + byte[] copy = new byte[this.length]; + this.Position = 0; + this.Read(copy, 0, copy.Length); + this.Position = position; return copy; } - /// - /// Write remainder of this stream to another stream. - /// - /// The stream to write to. - public void WriteTo(Stream stream) + /// + protected override void Dispose(bool disposing) { - this.EnsureNotDisposed(); - - Guard.NotNull(stream, nameof(stream)); + if (this.isDisposed) + { + return; + } - if (this.readChunk is null) + try { - if (this.memoryChunk is null) + this.isDisposed = true; + if (disposing) { - return; + this.memoryChunkBuffer.Dispose(); } - this.readChunk = this.memoryChunk; - this.readOffset = 0; + this.currentChunk = 0; + this.currentChunkIndex = 0; + this.position = 0; + this.length = 0; + } + finally + { + base.Dispose(disposing); + } + } + + private void SetPosition(long value) + { + long newPosition = value; + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); } - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) + this.position = newPosition; + + // Find the current chunk & current chunk index + int currentChunkIndex = 0; + long offset = newPosition; + + // If the new position is greater than the length of the stream, set the position to the end of the stream + if (offset > 0 && offset >= this.memoryChunkBuffer.Length) { - chunkSize = this.writeOffset; + this.currentChunk = this.memoryChunkBuffer.ChunkCount - 1; + this.currentChunkIndex = this.memoryChunkBuffer[this.currentChunk].Length - 1; + return; } - // Following code mirrors Read() logic (readChunk/readOffset should - // point just past last byte of last chunk when done) - // loop until end of chunks is found - while (true) + // Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk + // from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk. + while (offset != 0) { - if (this.readOffset == chunkSize) + int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length; + if (offset < chunkLength) { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } + // Found the correct chunk and the corresponding index + break; } - int writeCount = chunkSize - this.readOffset; - stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); - this.readOffset = chunkSize; + offset -= chunkLength; + currentChunkIndex++; } + + this.currentChunk = currentChunkIndex; + + // Safe to cast here as we know the offset is less than the chunk length. + this.currentChunkIndex = (int)offset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -507,48 +389,82 @@ internal sealed class ChunkedMemoryStream : Stream } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed."); - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); + private sealed class MemoryChunkBuffer : IEnumerable, IDisposable + { + private readonly List memoryChunks = new(); + private readonly MemoryAllocator allocator; + private readonly int allocatorCapacity; + private bool isDisposed; - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); + public MemoryChunkBuffer(MemoryAllocator allocator) + { + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); + this.allocator = allocator; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private MemoryChunk AllocateMemoryChunk() - { - // Tweak our buffer sizes to take the minimum of the provided buffer sizes - // or the allocator buffer capacity which provides us with the largest - // available contiguous buffer size. - IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); + public int ChunkCount => this.memoryChunks.Count; + + public long Length { get; private set; } - return new MemoryChunk(buffer) + public MemoryChunk this[int index] => this.memoryChunks[index]; + + public void Expand() { - Next = null, - Length = buffer.Length() - }; - } + IMemoryOwner buffer = + this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount))); - private static void ReleaseMemoryChunks(MemoryChunk? chunk) - { - while (chunk != null) + MemoryChunk chunk = new(buffer) + { + Length = buffer.Length() + }; + + this.memoryChunks.Add(chunk); + this.Length += chunk.Length; + } + + public void Dispose() { - chunk.Dispose(); - chunk = chunk.Next; + this.Dispose(true); + GC.SuppressFinalize(this); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetChunkSize(int i) - { - // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. - // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 -#pragma warning disable IDE1006 // Naming Styles - const int _128K = 1 << 17; - const int _4M = 1 << 22; - return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; -#pragma warning restore IDE1006 // Naming Styles + public IEnumerator GetEnumerator() + => ((IEnumerable)this.memoryChunks).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this.memoryChunks).GetEnumerator(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the + // same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 + const int b128K = 1 << 17; + const int b4M = 1 << 22; + return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M; + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + foreach (MemoryChunk chunk in this.memoryChunks) + { + chunk.Dispose(); + } + + this.memoryChunks.Clear(); + } + + this.Length = 0; + this.isDisposed = true; + } + } } private sealed class MemoryChunk : IDisposable @@ -559,8 +475,6 @@ internal sealed class ChunkedMemoryStream : Stream public IMemoryOwner Buffer { get; } - public MemoryChunk? Next { get; set; } - public int Length { get; init; } private void Dispose(bool disposing) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 031a9ba059..c6751e2a66 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -544,6 +544,44 @@ public class WebpEncoderTests [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public void CanSave_NonSeekableStream(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + using MemoryStream memoryStream = new(); + using NonSeekableStream nonSeekable = new(memoryStream); + + image.Save(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public async Task CanSave_NonSeekableStream_Async(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + await using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + await using MemoryStream memoryStream = new(); + await using NonSeekableStream nonSeekable = new(memoryStream); + + await image.SaveAsync(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 1803cfddb9..8d7ea9a33e 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -30,7 +30,7 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_GetPositionTest_Negative() { - using var ms = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream ms = new(this.allocator); long iCurrentPos = ms.Position; for (int i = -1; i > -6; i--) { @@ -42,7 +42,7 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_ReadTest_Negative() { - var ms2 = new ChunkedMemoryStream(this.allocator); + ChunkedMemoryStream ms2 = new(this.allocator); Assert.Throws(() => ms2.Read(null, 0, 0)); Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); @@ -63,8 +63,8 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -84,8 +84,8 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -107,8 +107,8 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferSpanTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -125,7 +125,7 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_WriteToTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) { byte[] bytArrRet; byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -133,7 +133,7 @@ public class ChunkedMemoryStreamTests // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); - using var readonlyStream = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream readonlyStream = new(this.allocator); ms2.WriteTo(readonlyStream); readonlyStream.Flush(); readonlyStream.Position = 0; @@ -146,8 +146,8 @@ public class ChunkedMemoryStreamTests } // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) + using (ChunkedMemoryStream ms3 = new(this.allocator)) { byte[] bytArrRet; byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -167,7 +167,7 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_WriteToSpanTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) { Span bytArrRet; Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -175,10 +175,12 @@ public class ChunkedMemoryStreamTests // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); - using var readonlyStream = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream readonlyStream = new(this.allocator); ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); for (int i = 0; i < bytArr.Length; i++) @@ -188,13 +190,14 @@ public class ChunkedMemoryStreamTests } // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) + using (ChunkedMemoryStream ms3 = new(this.allocator)) { Span bytArrRet; Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); ms3.Position = 0; bytArrRet = new byte[(int)ms3.Length]; @@ -209,37 +212,35 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_WriteByteTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + using ChunkedMemoryStream ms2 = new(this.allocator); + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - for (int i = 0; i < bytArr.Length; i++) - { - ms2.WriteByte(bytArr[i]); - } + for (int i = 0; i < bytArr.Length; i++) + { + ms2.WriteByte(bytArr[i]); + } - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } + using ChunkedMemoryStream readonlyStream = new(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); } } [Fact] public void MemoryStream_WriteToTests_Negative() { - using var ms2 = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream ms2 = new(this.allocator); Assert.Throws(() => ms2.WriteTo(null)); ms2.Write(new byte[] { 1 }, 0, 1); - var readonlyStream = new MemoryStream(new byte[1028], false); + MemoryStream readonlyStream = new(new byte[1028], false); Assert.Throws(() => ms2.WriteTo(readonlyStream)); readonlyStream.Dispose(); @@ -286,7 +287,7 @@ public class ChunkedMemoryStreamTests [MemberData(nameof(CopyToData))] public void CopyTo(Stream source, byte[] expected) { - using var destination = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream destination = new(this.allocator); source.CopyTo(destination); Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. Assert.Equal(expected, destination.ToArray()); @@ -297,10 +298,10 @@ public class ChunkedMemoryStreamTests IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); - var result = new List(); + List result = new(); foreach (string path in allImageFiles) { - result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); + result.Add(path[TestEnvironment.InputImagesDirectoryFullPath.Length..]); } return result; @@ -334,9 +335,9 @@ public class ChunkedMemoryStreamTests ((TestImageProvider.FileProvider)provider).FilePath); using FileStream fs = File.OpenRead(fullPath); - using var nonSeekableStream = new NonSeekableStream(fs); + using NonSeekableStream nonSeekableStream = new(fs); - var actual = Image.Load(nonSeekableStream); + Image actual = Image.Load(nonSeekableStream); ImageComparer.Exact.VerifySimilarity(expected, actual); } @@ -345,27 +346,27 @@ public class ChunkedMemoryStreamTests { // Stream is positioned @ beginning of data byte[] data1 = new byte[] { 1, 2, 3 }; - var stream1 = new MemoryStream(data1); + MemoryStream stream1 = new(data1); yield return new object[] { stream1, data1 }; // Stream is positioned in the middle of data byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; - var stream2 = new MemoryStream(data2) { Position = 1 }; + MemoryStream stream2 = new(data2) { Position = 1 }; yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; // Stream is positioned after end of data byte[] data3 = data2; - var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; + MemoryStream stream3 = new(data3) { Position = data3.Length + 1 }; yield return new object[] { stream3, Array.Empty() }; } - private MemoryStream CreateTestStream(int length) + private static MemoryStream CreateTestStream(int length) { byte[] buffer = new byte[length]; - var random = new Random(); + Random random = new(); random.NextBytes(buffer); return new MemoryStream(buffer); diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 4b1f6e1568..2941490e9a 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ internal class NonSeekableStream : Stream public override bool CanSeek => false; - public override bool CanWrite => false; + public override bool CanWrite => this.dataStream.CanWrite; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ internal class NonSeekableStream : Stream => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => throw new NotImplementedException(); + => this.dataStream.Write(buffer, offset, count); } From f96f8ca285693e70fe095d953bc8f930bb7a81ba Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 12:23:34 +1000 Subject: [PATCH 13/24] Revert "Update BufferedStreams.cs" This reverts commit 1a150780fd5dc79194e02568503a12b36cbc42cb. --- .../General/IO/BufferedStreams.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs index a7b22e7ab8..2a926d1cd8 100644 --- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs @@ -19,8 +19,12 @@ public class BufferedStreams private MemoryStream stream4; private MemoryStream stream5; private MemoryStream stream6; + private ChunkedMemoryStream chunkedMemoryStream1; + private ChunkedMemoryStream chunkedMemoryStream2; private BufferedReadStream bufferedStream1; private BufferedReadStream bufferedStream2; + private BufferedReadStream bufferedStream3; + private BufferedReadStream bufferedStream4; private BufferedReadStreamWrapper bufferedStreamWrap1; private BufferedReadStreamWrapper bufferedStreamWrap2; @@ -35,8 +39,18 @@ public class BufferedStreams this.stream6 = new MemoryStream(this.buffer); this.stream6 = new MemoryStream(this.buffer); + this.chunkedMemoryStream1 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); + this.chunkedMemoryStream1.Write(this.buffer); + this.chunkedMemoryStream1.Position = 0; + + this.chunkedMemoryStream2 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); + this.chunkedMemoryStream2.Write(this.buffer); + this.chunkedMemoryStream2.Position = 0; + this.bufferedStream1 = new BufferedReadStream(Configuration.Default, this.stream3); this.bufferedStream2 = new BufferedReadStream(Configuration.Default, this.stream4); + this.bufferedStream3 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream1); + this.bufferedStream4 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream2); this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5); this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6); } @@ -46,8 +60,12 @@ public class BufferedStreams { this.bufferedStream1?.Dispose(); this.bufferedStream2?.Dispose(); + this.bufferedStream3?.Dispose(); + this.bufferedStream4?.Dispose(); this.bufferedStreamWrap1?.Dispose(); this.bufferedStreamWrap2?.Dispose(); + this.chunkedMemoryStream1?.Dispose(); + this.chunkedMemoryStream2?.Dispose(); this.stream1?.Dispose(); this.stream2?.Dispose(); this.stream3?.Dispose(); @@ -86,6 +104,21 @@ public class BufferedStreams return r; } + [Benchmark] + public int BufferedReadStreamChunkedRead() + { + int r = 0; + BufferedReadStream reader = this.bufferedStream3; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; + } + [Benchmark] public int BufferedReadStreamWrapRead() { @@ -129,6 +162,20 @@ public class BufferedStreams return r; } + [Benchmark] + public int BufferedReadStreamChunkedReadByte() + { + int r = 0; + BufferedReadStream reader = this.bufferedStream4; + + for (int i = 0; i < reader.Length; i++) + { + r += reader.ReadByte(); + } + + return r; + } + [Benchmark] public int BufferedReadStreamWrapReadByte() { From 03343c4abee4d33c128f70050505613022e754df Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 14:18:26 +1000 Subject: [PATCH 14/24] Simplify and optimize position checking --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 39 +++++++++++++----------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index f178764603..06074b25bc 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -127,24 +127,26 @@ public class ChunkedMemoryStream : Stream int offset = 0; int count = buffer.Length; - int bytesRead = 0; - long bytesToRead = this.length - this.position; - if (bytesToRead > count) + + long remaining = this.length - this.position; + if (remaining > count) { - bytesToRead = count; + remaining = count; } - if (bytesToRead <= 0) + if (remaining <= 0) { // Already at the end of the stream, nothing to read return 0; } + int bytesToRead = (int)remaining; + int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToRead, int.MaxValue); + int n = bytesToRead; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { @@ -193,26 +195,28 @@ public class ChunkedMemoryStream : Stream int offset = 0; int count = buffer.Length; - int bytesWritten = 0; - long bytesToWrite = this.memoryChunkBuffer.Length - this.position; + + long remaining = this.memoryChunkBuffer.Length - this.position; // Ensure we have enough capacity to write the data. - while (bytesToWrite < count) + while (remaining < count) { this.memoryChunkBuffer.Expand(); - bytesToWrite = this.memoryChunkBuffer.Length - this.position; + remaining = this.memoryChunkBuffer.Length - this.position; } - if (bytesToWrite > count) + if (remaining > count) { - bytesToWrite = count; + remaining = count; } + int bytesToWrite = (int)remaining; + int bytesWritten = 0; while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToWrite, int.MaxValue); + int n = bytesToWrite; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { @@ -254,19 +258,20 @@ public class ChunkedMemoryStream : Stream this.Position = 0; - int bytesRead = 0; - long bytesToRead = this.length - this.position; - if (bytesToRead <= 0) + long remaining = this.length - this.position; + if (remaining <= 0) { // Already at the end of the stream, nothing to read return; } + int bytesToRead = (int)remaining; + int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToRead, int.MaxValue); + int n = bytesToRead; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { From b74d2e425774333e4be9b1e776f81b10fb003eee Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 30 Oct 2024 09:34:07 +1000 Subject: [PATCH 15/24] Add WriteByte and optimize ReadByte --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 06074b25bc..59c42ec387 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.IO; @@ -13,11 +14,10 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -/// Provides an in-memory stream composed of non-contiguous chunks. public class ChunkedMemoryStream : Stream { private readonly MemoryChunkBuffer memoryChunkBuffer; - private readonly byte[] singleReadBuffer = new byte[1]; + private readonly byte[] singleByteBuffer = new byte[1]; private long length; private long position; @@ -101,8 +101,8 @@ public class ChunkedMemoryStream : Stream return -1; } - _ = this.Read(this.singleReadBuffer, 0, 1); - return this.singleReadBuffer[^1]; + _ = this.Read(this.singleByteBuffer, 0, 1); + return MemoryMarshal.GetReference(this.singleByteBuffer); } /// @@ -129,17 +129,17 @@ public class ChunkedMemoryStream : Stream int count = buffer.Length; long remaining = this.length - this.position; - if (remaining > count) - { - remaining = count; - } - if (remaining <= 0) { // Already at the end of the stream, nothing to read return 0; } + if (remaining > count) + { + remaining = count; + } + int bytesToRead = (int)remaining; int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) @@ -175,6 +175,14 @@ public class ChunkedMemoryStream : Stream return bytesRead; } + /// + public override void WriteByte(byte value) + { + this.EnsureNotDisposed(); + MemoryMarshal.Write(this.singleByteBuffer, ref value); + this.Write(this.singleByteBuffer, 0, 1); + } + /// public override void Write(byte[] buffer, int offset, int count) { @@ -309,7 +317,7 @@ public class ChunkedMemoryStream : Stream byte[] copy = new byte[this.length]; this.Position = 0; - this.Read(copy, 0, copy.Length); + _ = this.Read(copy, 0, copy.Length); this.Position = position; return copy; } From 630166211c7c3d1b6b56f250bf198cc967d3e42b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Nov 2024 10:12:11 +1000 Subject: [PATCH 16/24] Feedback updates and massively expand write tests --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 148 ++++++------------ .../IO/ChunkedMemoryStreamTests.cs | 88 ++++++++--- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 59c42ec387..53de2c3cb5 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -14,15 +13,13 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -public class ChunkedMemoryStream : Stream +internal sealed class ChunkedMemoryStream : Stream { private readonly MemoryChunkBuffer memoryChunkBuffer; - private readonly byte[] singleByteBuffer = new byte[1]; - private long length; private long position; - private int currentChunk; - private int currentChunkIndex; + private int bufferIndex; + private int chunkIndex; private bool isDisposed; /// @@ -95,21 +92,13 @@ public class ChunkedMemoryStream : Stream /// public override int ReadByte() { - this.EnsureNotDisposed(); - if (this.position >= this.length) - { - return -1; - } - - _ = this.Read(this.singleByteBuffer, 0, 1); - return MemoryMarshal.GetReference(this.singleByteBuffer); + Unsafe.SkipInit(out byte b); + return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1; } /// public override int Read(byte[] buffer, int offset, int count) { - this.EnsureNotDisposed(); - Guard.NotNull(buffer, nameof(buffer)); Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); @@ -135,19 +124,14 @@ public class ChunkedMemoryStream : Stream return 0; } - if (remaining > count) - { - remaining = count; - } - - int bytesToRead = (int)remaining; + int bytesToRead = count; int bytesRead = 0; - while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToRead; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -155,19 +139,19 @@ public class ChunkedMemoryStream : Stream } // Read n bytes from the current chunk - chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n).CopyTo(buffer.Slice(offset, n)); + chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n)); bytesToRead -= n; offset += n; bytesRead += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -177,11 +161,7 @@ public class ChunkedMemoryStream : Stream /// public override void WriteByte(byte value) - { - this.EnsureNotDisposed(); - MemoryMarshal.Write(this.singleByteBuffer, ref value); - this.Write(this.singleByteBuffer, 0, 1); - } + => this.Write(MemoryMarshal.CreateSpan(ref value, 1)); /// public override void Write(byte[] buffer, int offset, int count) @@ -213,19 +193,14 @@ public class ChunkedMemoryStream : Stream remaining = this.memoryChunkBuffer.Length - this.position; } - if (remaining > count) - { - remaining = count; - } - - int bytesToWrite = (int)remaining; + int bytesToWrite = count; int bytesWritten = 0; - while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToWrite; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -233,19 +208,19 @@ public class ChunkedMemoryStream : Stream } // Write n bytes to the current chunk - buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.currentChunkIndex, n)); + buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n)); bytesToWrite -= n; offset += n; bytesWritten += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -275,12 +250,12 @@ public class ChunkedMemoryStream : Stream int bytesToRead = (int)remaining; int bytesRead = 0; - while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToRead; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -288,18 +263,18 @@ public class ChunkedMemoryStream : Stream } // Read n bytes from the current chunk - stream.Write(chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n)); + stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n)); bytesToRead -= n; bytesRead += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -338,8 +313,8 @@ public class ChunkedMemoryStream : Stream this.memoryChunkBuffer.Dispose(); } - this.currentChunk = 0; - this.currentChunkIndex = 0; + this.bufferIndex = 0; + this.chunkIndex = 0; this.position = 0; this.length = 0; } @@ -366,8 +341,8 @@ public class ChunkedMemoryStream : Stream // If the new position is greater than the length of the stream, set the position to the end of the stream if (offset > 0 && offset >= this.memoryChunkBuffer.Length) { - this.currentChunk = this.memoryChunkBuffer.ChunkCount - 1; - this.currentChunkIndex = this.memoryChunkBuffer[this.currentChunk].Length - 1; + this.bufferIndex = this.memoryChunkBuffer.ChunkCount - 1; + this.chunkIndex = this.memoryChunkBuffer[this.bufferIndex].Length - 1; return; } @@ -386,10 +361,10 @@ public class ChunkedMemoryStream : Stream currentChunkIndex++; } - this.currentChunk = currentChunkIndex; + this.bufferIndex = currentChunkIndex; // Safe to cast here as we know the offset is less than the chunk length. - this.currentChunkIndex = (int)offset; + this.chunkIndex = (int)offset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -404,7 +379,7 @@ public class ChunkedMemoryStream : Stream [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed."); - private sealed class MemoryChunkBuffer : IEnumerable, IDisposable + private sealed class MemoryChunkBuffer : IDisposable { private readonly List memoryChunks = new(); private readonly MemoryAllocator allocator; @@ -439,15 +414,19 @@ public class ChunkedMemoryStream : Stream public void Dispose() { - this.Dispose(true); - GC.SuppressFinalize(this); - } + if (!this.isDisposed) + { + foreach (MemoryChunk chunk in this.memoryChunks) + { + chunk.Dispose(); + } - public IEnumerator GetEnumerator() - => ((IEnumerable)this.memoryChunks).GetEnumerator(); + this.memoryChunks.Clear(); - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)this.memoryChunks).GetEnumerator(); + this.Length = 0; + this.isDisposed = true; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetChunkSize(int i) @@ -459,25 +438,6 @@ public class ChunkedMemoryStream : Stream const int b4M = 1 << 22; return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M; } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - foreach (MemoryChunk chunk in this.memoryChunks) - { - chunk.Dispose(); - } - - this.memoryChunks.Clear(); - } - - this.Length = 0; - this.isDisposed = true; - } - } } private sealed class MemoryChunk : IDisposable @@ -490,23 +450,13 @@ public class ChunkedMemoryStream : Stream public int Length { get; init; } - private void Dispose(bool disposing) + public void Dispose() { if (!this.isDisposed) { - if (disposing) - { - this.Buffer.Dispose(); - } - + this.Buffer.Dispose(); this.isDisposed = true; } } - - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 8d7ea9a33e..b1bb7a9f5a 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Tests.IO; /// public class ChunkedMemoryStreamTests { + private readonly Random bufferFiller = new(); + /// /// The default length in bytes of each buffer chunk when allocating large buffers. /// @@ -63,7 +65,7 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -84,7 +86,7 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -105,9 +107,10 @@ public class ChunkedMemoryStreamTests [InlineData(DefaultSmallChunkSize * 4)] [InlineData((int)(DefaultSmallChunkSize * 5.5))] [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] public void MemoryStream_ReadByteBufferSpanTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -122,13 +125,19 @@ public class ChunkedMemoryStreamTests } } - [Fact] - public void MemoryStream_WriteToTests() + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] + public void MemoryStream_WriteToTests(int length) { using (ChunkedMemoryStream ms2 = new(this.allocator)) { byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + byte[] bytArr = this.CreateTestBuffer(length); // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); @@ -150,7 +159,7 @@ public class ChunkedMemoryStreamTests using (ChunkedMemoryStream ms3 = new(this.allocator)) { byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + byte[] bytArr = this.CreateTestBuffer(length); ms2.Write(bytArr, 0, bytArr.Length); ms2.WriteTo(ms3); @@ -164,13 +173,19 @@ public class ChunkedMemoryStreamTests } } - [Fact] - public void MemoryStream_WriteToSpanTests() + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] + public void MemoryStream_WriteToSpanTests(int length) { using (ChunkedMemoryStream ms2 = new(this.allocator)) { Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + Span bytArr = this.CreateTestBuffer(length); // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); @@ -194,7 +209,7 @@ public class ChunkedMemoryStreamTests using (ChunkedMemoryStream ms3 = new(this.allocator)) { Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + Span bytArr = this.CreateTestBuffer(length); ms2.Write(bytArr, 0, bytArr.Length); @@ -307,7 +322,7 @@ public class ChunkedMemoryStreamTests return result; } - public static IEnumerable AllTestImages = GetAllTestImages(); + public static IEnumerable AllTestImages { get; } = GetAllTestImages(); [Theory] [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] @@ -337,9 +352,45 @@ public class ChunkedMemoryStreamTests using FileStream fs = File.OpenRead(fullPath); using NonSeekableStream nonSeekableStream = new(fs); - Image actual = Image.Load(nonSeekableStream); + using Image actual = Image.Load(nonSeekableStream); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + expected.Dispose(); + } + + [Theory] + [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] + public void EncoderIntegrationTest(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + Image expected; + try + { + expected = provider.GetImage(); + } + catch + { + // The image is invalid + return; + } + + string fullPath = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + ((TestImageProvider.FileProvider)provider).FilePath); + + using MemoryStream ms = new(); + using NonSeekableStream nonSeekableStream = new(ms); + expected.SaveAsWebp(nonSeekableStream); + + using Image actual = Image.Load(nonSeekableStream); ImageComparer.Exact.VerifySimilarity(expected, actual); + expected.Dispose(); } public static IEnumerable CopyToData() @@ -363,12 +414,13 @@ public class ChunkedMemoryStreamTests yield return new object[] { stream3, Array.Empty() }; } - private static MemoryStream CreateTestStream(int length) + private byte[] CreateTestBuffer(int length) { byte[] buffer = new byte[length]; - Random random = new(); - random.NextBytes(buffer); - - return new MemoryStream(buffer); + this.bufferFiller.NextBytes(buffer); + return buffer; } + + private MemoryStream CreateTestStream(int length) + => new(this.CreateTestBuffer(length)); } From c45702df5b2c0464098bb3f7def7bf396bd537fd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Nov 2024 13:14:16 +1000 Subject: [PATCH 17/24] Fix read bug. --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 53de2c3cb5..760d1d3345 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -124,7 +124,13 @@ internal sealed class ChunkedMemoryStream : Stream return 0; } - int bytesToRead = count; + if (remaining > count) + { + remaining = count; + } + + // 'remaining' can be less than the provided buffer length. + int bytesToRead = (int)remaining; int bytesRead = 0; while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { @@ -422,7 +428,6 @@ internal sealed class ChunkedMemoryStream : Stream } this.memoryChunks.Clear(); - this.Length = 0; this.isDisposed = true; } From c4fd666018266f1db59efc584dd4d0e195ed1736 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Nov 2024 09:54:19 +1000 Subject: [PATCH 18/24] Update tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs Co-authored-by: Anton Firszov --- tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index b1bb7a9f5a..390170cfef 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.IO; /// public class ChunkedMemoryStreamTests { - private readonly Random bufferFiller = new(); + private readonly Random bufferFiller = new(123); /// /// The default length in bytes of each buffer chunk when allocating large buffers. From 87d6f00e8283ecc19fc15f30d5f94f0d3af758c2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2024 19:28:40 +1000 Subject: [PATCH 19/24] Test against NET 9 --- .github/workflows/build-and-test.yml | 27 ++++++++++++++++++- src/ImageSharp/ImageSharp.csproj | 5 ++-- .../ImageSharp.Benchmarks.csproj | 2 +- .../ImageSharp.Tests.ProfilingSandbox.csproj | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 2 +- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a450aebf43..435c629bc6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,6 +19,31 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: + - os: ubuntu-latest + framework: net9.0 + sdk: 9.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + framework: net9.0 + sdk: 9.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: windows-latest + framework: net9.0 + sdk: 9.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: buildjet-4vcpu-ubuntu-2204-arm + framework: net9.0 + sdk: 9.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: ubuntu-latest framework: net8.0 sdk: 8.0.x @@ -100,7 +125,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 8.0.x + 9.0.x - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index d3c4034717..0d36340bf8 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,6 +13,7 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release + true @@ -29,14 +30,12 @@ - net8.0 - true + net8.0;net9.0 net8.0 - true diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 4408159ef4..a705b24b28 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -23,7 +23,7 @@ - net8.0 + net8.0;net9.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index b93d011910..832f3d171f 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -19,7 +19,7 @@ - net8.0 + net8.0;net9.0 diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 8b4aa3d706..09cfe2cbea 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -719,7 +719,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester // ImageMagick cannot decode this image. image.DebugSave(provider); image.CompareToReferenceOutput( - ImageComparer.Exact, + ImageComparer.TolerantPercentage(0.0018F), // NET 9+ Uses zlib-ng to decompress, which manages to decode 2 extra pixels. provider, appendPixelTypeToFileName: false); } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 41e6e525f8..9af4f41a81 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -12,7 +12,7 @@ - net8.0 + net8.0;net9.0 From 62744c698d40099bd9e00401ed7fa957fa9a8def Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 14 Nov 2024 20:18:48 +1000 Subject: [PATCH 20/24] FIx build --- src/ImageSharp.ruleset | 3 +++ .../Formats/Jpeg/JpegDecoderCore.cs | 3 +-- .../Formats/Webp/Lossy/Vp8Encoding.cs | 20 +++++++++---------- .../Formats/Webp/Lossy/YuvConversion.cs | 16 +++++++-------- src/ImageSharp/Image{TPixel}.cs | 4 ++-- .../Metadata/Profiles/Exif/ExifWriter.cs | 2 +- .../DataReader/IccDataReader.TagDataEntry.cs | 2 +- .../Metadata/Profiles/ICC/IccProfile.cs | 6 +++--- .../Parameters/BokehBlurKernelDataProvider.cs | 6 +++--- 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp.ruleset b/src/ImageSharp.ruleset index b609890200..dee0393cd7 100644 --- a/src/ImageSharp.ruleset +++ b/src/ImageSharp.ruleset @@ -1,4 +1,7 @@  + + + \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 2320fe1791..707baa1a88 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -16,7 +16,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg; @@ -1473,7 +1472,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData this.Frame.ComponentOrder[i / 2] = (byte)componentIndex; - IJpegComponent component = this.Frame.Components[componentIndex]; + JpegComponent component = this.Frame.Components[componentIndex]; // 1 byte: Huffman table selectors. // 4 bits - dc diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs index 82f00e8760..c645816d4b 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs @@ -667,12 +667,12 @@ internal static unsafe class Vp8Encoding // V block. dst = dst[8..]; - if (top != default) + if (!top.IsEmpty) { top = top[8..]; } - if (left != default) + if (!left.IsEmpty) { left = left[16..]; } @@ -701,7 +701,7 @@ internal static unsafe class Vp8Encoding private static void VerticalPred(Span dst, Span top, int size) { - if (top != default) + if (!top.IsEmpty) { for (int j = 0; j < size; j++) { @@ -716,7 +716,7 @@ internal static unsafe class Vp8Encoding public static void HorizontalPred(Span dst, Span left, int size) { - if (left != default) + if (!left.IsEmpty) { left = left[1..]; // in the reference implementation, left starts at - 1. for (int j = 0; j < size; j++) @@ -732,9 +732,9 @@ internal static unsafe class Vp8Encoding public static void TrueMotion(Span dst, Span left, Span top, int size) { - if (left != default) + if (!left.IsEmpty) { - if (top != default) + if (!top.IsEmpty) { Span clip = Clip1.AsSpan(255 - left[0]); // left [0] instead of left[-1], original left starts at -1 for (int y = 0; y < size; y++) @@ -759,7 +759,7 @@ internal static unsafe class Vp8Encoding // is equivalent to VE prediction where you just copy the top samples. // Note that if top samples are not available, the default value is // then 129, and not 127 as in the VerticalPred case. - if (top != default) + if (!top.IsEmpty) { VerticalPred(dst, top, size); } @@ -774,14 +774,14 @@ internal static unsafe class Vp8Encoding { int dc = 0; int j; - if (top != default) + if (!top.IsEmpty) { for (j = 0; j < size; j++) { dc += top[j]; } - if (left != default) + if (!left.IsEmpty) { // top and left present. left = left[1..]; // in the reference implementation, left starts at -1. @@ -798,7 +798,7 @@ internal static unsafe class Vp8Encoding dc = (dc + round) >> shift; } - else if (left != default) + else if (!left.IsEmpty) { // left but no top. left = left[1..]; // in the reference implementation, left starts at -1. diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index f8e664ed03..40146c6af8 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -48,7 +48,7 @@ internal static class YuvConversion uint uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst); - if (bottomY != default) + if (!bottomY.IsEmpty) { uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; YuvToBgr(bottomY[0], (int)uv0 & 0xff, (int)(uv0 >> 16), bottomDst); @@ -69,7 +69,7 @@ internal static class YuvConversion YuvToBgr(topY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((xMul2 - 1) * xStep)..]); YuvToBgr(topY[xMul2 - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst[((xMul2 - 0) * xStep)..]); - if (bottomY != default) + if (!bottomY.IsEmpty) { uv0 = (diag03 + luv) >> 1; uv1 = (diag12 + uv) >> 1; @@ -85,7 +85,7 @@ internal static class YuvConversion { uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; YuvToBgr(topY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((len - 1) * xStep)..]); - if (bottomY != default) + if (!bottomY.IsEmpty) { uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; YuvToBgr(bottomY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst[((len - 1) * xStep)..]); @@ -120,7 +120,7 @@ internal static class YuvConversion int u0t = (topU[0] + uDiag) >> 1; int v0t = (topV[0] + vDiag) >> 1; YuvToBgr(topY[0], u0t, v0t, topDst); - if (bottomY != default) + if (!bottomY.IsEmpty) { int u0b = (curU[0] + uDiag) >> 1; int v0b = (curV[0] + vDiag) >> 1; @@ -134,7 +134,7 @@ internal static class YuvConversion ref byte topVRef = ref MemoryMarshal.GetReference(topV); ref byte curURef = ref MemoryMarshal.GetReference(curU); ref byte curVRef = ref MemoryMarshal.GetReference(curV); - if (bottomY != default) + if (!bottomY.IsEmpty) { for (pos = 1, uvPos = 0; pos + 32 + 1 <= len; pos += 32, uvPos += 16) { @@ -160,12 +160,12 @@ internal static class YuvConversion Span tmpTopDst = ru[(4 * 32)..]; Span tmpBottomDst = tmpTopDst[(4 * 32)..]; Span tmpTop = tmpBottomDst[(4 * 32)..]; - Span tmpBottom = (bottomY == default) ? null : tmpTop[32..]; + Span tmpBottom = bottomY.IsEmpty ? null : tmpTop[32..]; UpSampleLastBlock(topU[uvPos..], curU[uvPos..], leftOver, ru); UpSampleLastBlock(topV[uvPos..], curV[uvPos..], leftOver, rv); topY[pos..len].CopyTo(tmpTop); - if (bottomY != default) + if (!bottomY.IsEmpty) { bottomY[pos..len].CopyTo(tmpBottom); ConvertYuvToBgrWithBottomYSse41(tmpTop, tmpBottom, tmpTopDst, tmpBottomDst, ru, rv, 0, xStep); @@ -176,7 +176,7 @@ internal static class YuvConversion } tmpTopDst[..((len - pos) * xStep)].CopyTo(topDst[(pos * xStep)..]); - if (bottomY != default) + if (!bottomY.IsEmpty) { tmpBottomDst[..((len - pos) * xStep)].CopyTo(bottomDst[(pos * xStep)..]); } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 02403923d2..7ec7918381 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -160,7 +160,7 @@ public sealed class Image : Image /// /// Gets the root frame. /// - private IPixelSource PixelSourceUnsafe => this.frames.RootFrameUnsafe; + private ImageFrame PixelSourceUnsafe => this.frames.RootFrameUnsafe; /// /// Gets or sets the pixel at the specified position. @@ -324,7 +324,7 @@ public sealed class Image : Image } /// - /// Clones the current image + /// Clones the current image. /// /// Returns a new image with all the same metadata as the original. public Image Clone() => this.Clone(this.Configuration); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index 1d2dca8700..cf4a421b44 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -241,7 +241,7 @@ internal sealed class ExifWriter return true; } - private static uint GetLength(IList values) + private static uint GetLength(List values) { if (values.Count == 0) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs index 0d50b98095..ddfc625152 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs @@ -144,7 +144,7 @@ internal sealed partial class IccDataReader ushort channelCount = this.ReadUInt16(); var colorant = (IccColorantEncoding)this.ReadUInt16(); - if (Enum.IsDefined(typeof(IccColorantEncoding), colorant) && colorant != IccColorantEncoding.Unknown) + if (Enum.IsDefined(colorant) && colorant != IccColorantEncoding.Unknown) { // The type is known and so are the values (they are constant) // channelCount should always be 3 but it doesn't really matter if it's not diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index be7350bc44..ac78318f2b 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -155,9 +155,9 @@ public sealed class IccProfile : IDeepCloneable } return arrayValid && - Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && - Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && - Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && + Enum.IsDefined(this.Header.DataColorSpace) && + Enum.IsDefined(this.Header.ProfileConnectionSpace) && + Enum.IsDefined(this.Header.RenderingIntent) && this.Header.Size is >= minSize and < maxSize; } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs index a680393c8c..565a5746d1 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs @@ -21,12 +21,12 @@ internal static class BokehBlurKernelDataProvider /// /// Gets the kernel scales to adjust the component values in each kernel /// - private static IReadOnlyList KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; + private static float[] KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; /// /// Gets the available bokeh blur kernel parameters /// - private static IReadOnlyList KernelComponents { get; } = new[] + private static Vector4[][] KernelComponents { get; } = new[] { // 1 component new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) }, @@ -112,7 +112,7 @@ internal static class BokehBlurKernelDataProvider private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount) { // Prepare the kernel components - int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Count)); + int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Length)); return (KernelComponents[index], KernelScales[index]); } From 3bd1efbbf819083e2ec49ebeddc3ce1b7e943fbc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 19 Nov 2024 13:29:02 +1000 Subject: [PATCH 21/24] Normalize parameter order. --- .../Convolution/BokehBlurExtensions.cs | 8 +- .../Convolution/BoxBlurExtensions.cs | 4 +- .../Convolution/DetectEdgesExtensions.cs | 124 +++++++----------- .../Convolution/GaussianBlurExtensions.cs | 21 +-- .../Convolution/GaussianSharpenExtensions.cs | 28 ++-- .../Convolution/MedianBlurExtensions.cs | 17 ++- .../Processing/Convolution/DetectEdgesTest.cs | 12 +- .../Convolution/GaussianBlurTest.cs | 8 +- .../Convolution/GaussianSharpenTest.cs | 8 +- .../Processing/Convolution/MedianBlurTest.cs | 6 +- .../Processors/Convolution/BokehBlurTest.cs | 3 +- .../Convolution/GaussianBlurTest.cs | 2 +- .../Convolution/GaussianSharpenTest.cs | 2 +- .../Processors/Convolution/MedianBlurTest.cs | 2 +- 14 files changed, 113 insertions(+), 132 deletions(-) diff --git a/src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs index e3e6f13ed6..71252e0bb0 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs @@ -44,13 +44,13 @@ public static class BokehBlurExtensions /// Applies a bokeh blur to the image. /// /// The current image processing context. - /// The 'radius' value representing the size of the area to sample. - /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. - /// The gamma highlight factor to use to emphasize bright spots in the source image /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'radius' value representing the size of the area to sample. + /// The 'components' value representing the number of kernels to use to approximate the bokeh effect. + /// The gamma highlight factor to use to emphasize bright spots in the source image /// The . - public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, Rectangle rectangle) + public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle, int radius, int components, float gamma) => source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma), rectangle); } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs index 11a7fc6a7a..73e40b57aa 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs @@ -44,10 +44,10 @@ public static class BoxBlurExtensions /// Applies a box blur to the image. /// /// The current image processing context. - /// The 'radius' value representing the size of the area to sample. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'radius' value representing the size of the area to sample. /// /// The to use when mapping the pixels outside of the border, in X direction. /// @@ -57,8 +57,8 @@ public static class BoxBlurExtensions /// The . public static IImageProcessingContext BoxBlur( this IImageProcessingContext source, - int radius, Rectangle rectangle, + int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) => source.ApplyProcessor(new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY), rectangle); diff --git a/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs index b044c3966f..c8fb230559 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs @@ -16,8 +16,8 @@ public static class DetectEdgesExtensions /// /// The current image processing context. /// The . - public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) => - DetectEdges(source, KnownEdgeDetectorKernels.Sobel); + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) + => DetectEdges(source, KnownEdgeDetectorKernels.Sobel); /// /// Detects any edges within the image. @@ -28,10 +28,8 @@ public static class DetectEdgesExtensions /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext DetectEdges( - this IImageProcessingContext source, - Rectangle rectangle) => - DetectEdges(source, KnownEdgeDetectorKernels.Sobel, rectangle); + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, Rectangle rectangle) + => DetectEdges(source, rectangle, KnownEdgeDetectorKernels.Sobel); /// /// Detects any edges within the image operating in grayscale mode. @@ -39,10 +37,8 @@ public static class DetectEdgesExtensions /// The current image processing context. /// The 2D edge detector kernel. /// The . - public static IImageProcessingContext DetectEdges( - this IImageProcessingContext source, - EdgeDetector2DKernel kernel) => - DetectEdges(source, kernel, true); + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetector2DKernel kernel) + => DetectEdges(source, kernel, true); /// /// Detects any edges within the image using a . @@ -57,49 +53,41 @@ public static class DetectEdgesExtensions this IImageProcessingContext source, EdgeDetector2DKernel kernel, bool grayscale) - { - var processor = new EdgeDetector2DProcessor(kernel, grayscale); - source.ApplyProcessor(processor); - return source; - } + => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale)); /// /// Detects any edges within the image operating in grayscale mode. /// /// The current image processing context. - /// The 2D edge detector kernel. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 2D edge detector kernel. /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetector2DKernel kernel, - Rectangle rectangle) => - DetectEdges(source, kernel, true, rectangle); + Rectangle rectangle, + EdgeDetector2DKernel kernel) + => DetectEdges(source, rectangle, kernel, true); /// /// Detects any edges within the image using a . /// /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// /// The 2D edge detector kernel. /// /// Whether to convert the image to grayscale before performing edge detection. /// - /// - /// The structure that specifies the portion of the image object to alter. - /// /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, + Rectangle rectangle, EdgeDetector2DKernel kernel, - bool grayscale, - Rectangle rectangle) - { - var processor = new EdgeDetector2DProcessor(kernel, grayscale); - source.ApplyProcessor(processor, rectangle); - return source; - } + bool grayscale) + => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale), rectangle); /// /// Detects any edges within the image operating in grayscale mode. @@ -107,10 +95,8 @@ public static class DetectEdgesExtensions /// The current image processing context. /// The edge detector kernel. /// The . - public static IImageProcessingContext DetectEdges( - this IImageProcessingContext source, - EdgeDetectorKernel kernel) => - DetectEdges(source, kernel, true); + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorKernel kernel) + => DetectEdges(source, kernel, true); /// /// Detects any edges within the image using a . @@ -125,66 +111,56 @@ public static class DetectEdgesExtensions this IImageProcessingContext source, EdgeDetectorKernel kernel, bool grayscale) - { - var processor = new EdgeDetectorProcessor(kernel, grayscale); - source.ApplyProcessor(processor); - return source; - } + => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale)); /// /// Detects any edges within the image operating in grayscale mode. /// /// The current image processing context. - /// The edge detector kernel. /// /// The structure that specifies the portion of the image object to alter. /// + /// The edge detector kernel. /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetectorKernel kernel, - Rectangle rectangle) => - DetectEdges(source, kernel, true, rectangle); + Rectangle rectangle, + EdgeDetectorKernel kernel) + => DetectEdges(source, rectangle, kernel, true); /// /// Detects any edges within the image using a . /// /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// /// The edge detector kernel. /// /// Whether to convert the image to grayscale before performing edge detection. /// - /// - /// The structure that specifies the portion of the image object to alter. - /// /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, + Rectangle rectangle, EdgeDetectorKernel kernel, - bool grayscale, - Rectangle rectangle) - { - var processor = new EdgeDetectorProcessor(kernel, grayscale); - source.ApplyProcessor(processor, rectangle); - return source; - } + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale), rectangle); /// /// Detects any edges within the image operating in grayscale mode. /// /// The current image processing context. - /// Thecompass edge detector kernel. + /// The compass edge detector kernel. /// The . - public static IImageProcessingContext DetectEdges( - this IImageProcessingContext source, - EdgeDetectorCompassKernel kernel) => - DetectEdges(source, kernel, true); + public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorCompassKernel kernel) + => DetectEdges(source, kernel, true); /// /// Detects any edges within the image using a . /// /// The current image processing context. - /// Thecompass edge detector kernel. + /// The compass edge detector kernel. /// /// Whether to convert the image to grayscale before performing edge detection. /// @@ -193,47 +169,39 @@ public static class DetectEdgesExtensions this IImageProcessingContext source, EdgeDetectorCompassKernel kernel, bool grayscale) - { - var processor = new EdgeDetectorCompassProcessor(kernel, grayscale); - source.ApplyProcessor(processor); - return source; - } + => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale)); /// /// Detects any edges within the image operating in grayscale mode. /// /// The current image processing context. - /// Thecompass edge detector kernel. /// /// The structure that specifies the portion of the image object to alter. /// + /// The compass edge detector kernel. /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetectorCompassKernel kernel, - Rectangle rectangle) => - DetectEdges(source, kernel, true, rectangle); + Rectangle rectangle, + EdgeDetectorCompassKernel kernel) + => DetectEdges(source, rectangle, kernel, true); /// /// Detects any edges within the image using a . /// /// The current image processing context. - /// Thecompass edge detector kernel. - /// - /// Whether to convert the image to grayscale before performing edge detection. - /// /// /// The structure that specifies the portion of the image object to alter. /// + /// The compass edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// /// The . public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, + Rectangle rectangle, EdgeDetectorCompassKernel kernel, - bool grayscale, - Rectangle rectangle) - { - var processor = new EdgeDetectorCompassProcessor(kernel, grayscale); - source.ApplyProcessor(processor, rectangle); - return source; - } + bool grayscale) + => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale), rectangle); } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs index 64e0be3eac..d406bf8d10 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs @@ -32,22 +32,25 @@ public static class GaussianBlurExtensions /// Applies a Gaussian blur to the image. /// /// The current image processing context. - /// The 'sigma' value representing the weight of the blur. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'sigma' value representing the weight of the blur. /// The . - public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle) + public static IImageProcessingContext GaussianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma) => source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle); /// /// Applies a Gaussian blur to the image. /// /// The current image processing context. - /// The 'sigma' value representing the weight of the blur. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'sigma' value representing the weight of the blur. /// /// The to use when mapping the pixels outside of the border, in X direction. /// @@ -55,9 +58,11 @@ public static class GaussianBlurExtensions /// The to use when mapping the pixels outside of the border, in Y direction. /// /// The . - public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) - { - GaussianBlurProcessor processor = new(sigma, borderWrapModeX, borderWrapModeY); - return source.ApplyProcessor(processor, rectangle); - } + public static IImageProcessingContext GaussianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle); } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs index 4a94df0963..9470cdbdc0 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs @@ -16,8 +16,8 @@ public static class GaussianSharpenExtensions /// /// The current image processing context. /// The . - public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source) => - source.ApplyProcessor(new GaussianSharpenProcessor()); + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source) + => source.ApplyProcessor(new GaussianSharpenProcessor()); /// /// Applies a Gaussian sharpening filter to the image. @@ -25,32 +25,32 @@ public static class GaussianSharpenExtensions /// The current image processing context. /// The 'sigma' value representing the weight of the blur. /// The . - public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma) => - source.ApplyProcessor(new GaussianSharpenProcessor(sigma)); + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma) + => source.ApplyProcessor(new GaussianSharpenProcessor(sigma)); /// /// Applies a Gaussian sharpening filter to the image. /// /// The current image processing context. - /// The 'sigma' value representing the weight of the blur. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'sigma' value representing the weight of the blur. /// The . public static IImageProcessingContext GaussianSharpen( this IImageProcessingContext source, - float sigma, - Rectangle rectangle) => + Rectangle rectangle, + float sigma) => source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle); /// /// Applies a Gaussian sharpening filter to the image. /// /// The current image processing context. - /// The 'sigma' value representing the weight of the blur. /// /// The structure that specifies the portion of the image object to alter. /// + /// The 'sigma' value representing the weight of the blur. /// /// The to use when mapping the pixels outside of the border, in X direction. /// @@ -58,9 +58,11 @@ public static class GaussianSharpenExtensions /// The to use when mapping the pixels outside of the border, in Y direction. /// /// The . - public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) - { - var processor = new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY); - return source.ApplyProcessor(processor, rectangle); - } + public static IImageProcessingContext GaussianSharpen( + this IImageProcessingContext source, + Rectangle rectangle, + float sigma, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + => source.ApplyProcessor(new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle); } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs index a08a398b75..bc6fef62a6 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs @@ -20,21 +20,28 @@ public static class MedianBlurExtensions /// Whether the filter is applied to alpha as well as the color channels. /// /// The . - public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha) + public static IImageProcessingContext MedianBlur( + this IImageProcessingContext source, + int radius, + bool preserveAlpha) => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha)); /// /// Applies a median blur on the image. /// /// The current image processing context. + /// + /// The structure that specifies the portion of the image object to alter. + /// /// The radius of the area to find the median for. /// /// Whether the filter is applied to alpha as well as the color channels. /// - /// - /// The structure that specifies the portion of the image object to alter. - /// /// The . - public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha, Rectangle rectangle) + public static IImageProcessingContext MedianBlur( + this IImageProcessingContext source, + Rectangle rectangle, + int radius, + bool preserveAlpha) => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle); } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs index 2a1273ebb7..713daa0e9f 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs @@ -59,7 +59,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetector2DKernelData))] public void DetectEdges_Rect_EdgeDetector2DProcessor_DefaultGrayScale_Set(EdgeDetector2DKernel kernel, bool _) { - this.operations.DetectEdges(kernel, this.rect); + this.operations.DetectEdges(this.rect, kernel); EdgeDetector2DProcessor processor = this.Verify(this.rect); Assert.True(processor.Grayscale); @@ -81,7 +81,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetector2DKernelData))] public void DetectEdges_Rect_EdgeDetector2DProcessorSet(EdgeDetector2DKernel kernel, bool grayscale) { - this.operations.DetectEdges(kernel, grayscale, this.rect); + this.operations.DetectEdges(this.rect, kernel, grayscale); EdgeDetector2DProcessor processor = this.Verify(this.rect); Assert.Equal(grayscale, processor.Grayscale); @@ -114,7 +114,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetectorKernelData))] public void DetectEdges_Rect_EdgeDetectorProcessor_DefaultGrayScale_Set(EdgeDetectorKernel kernel, bool _) { - this.operations.DetectEdges(kernel, this.rect); + this.operations.DetectEdges(this.rect, kernel); EdgeDetectorProcessor processor = this.Verify(this.rect); Assert.True(processor.Grayscale); @@ -136,7 +136,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetectorKernelData))] public void DetectEdges_Rect_EdgeDetectorProcessorSet(EdgeDetectorKernel kernel, bool grayscale) { - this.operations.DetectEdges(kernel, grayscale, this.rect); + this.operations.DetectEdges(this.rect, kernel, grayscale); EdgeDetectorProcessor processor = this.Verify(this.rect); Assert.Equal(grayscale, processor.Grayscale); @@ -167,7 +167,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetectorCompassKernelData))] public void DetectEdges_Rect_EdgeDetectorCompassProcessor_DefaultGrayScale_Set(EdgeDetectorCompassKernel kernel, bool _) { - this.operations.DetectEdges(kernel, this.rect); + this.operations.DetectEdges(this.rect, kernel); EdgeDetectorCompassProcessor processor = this.Verify(this.rect); Assert.True(processor.Grayscale); @@ -189,7 +189,7 @@ public class DetectEdgesTest : BaseImageOperationsExtensionTest [MemberData(nameof(EdgeDetectorCompassKernelData))] public void DetectEdges_Rect_EdgeDetectorCompassProcessorSet(EdgeDetectorCompassKernel kernel, bool grayscale) { - this.operations.DetectEdges(kernel, grayscale, this.rect); + this.operations.DetectEdges(this.rect, kernel, grayscale); EdgeDetectorCompassProcessor processor = this.Verify(this.rect); Assert.Equal(grayscale, processor.Grayscale); diff --git a/tests/ImageSharp.Tests/Processing/Convolution/GaussianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/GaussianBlurTest.cs index 5142791603..410862ebfa 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/GaussianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/GaussianBlurTest.cs @@ -13,7 +13,7 @@ public class GaussianBlurTest : BaseImageOperationsExtensionTest public void GaussianBlur_GaussianBlurProcessorDefaultsSet() { this.operations.GaussianBlur(); - var processor = this.Verify(); + GaussianBlurProcessor processor = this.Verify(); Assert.Equal(3f, processor.Sigma); } @@ -22,7 +22,7 @@ public class GaussianBlurTest : BaseImageOperationsExtensionTest public void GaussianBlur_amount_GaussianBlurProcessorDefaultsSet() { this.operations.GaussianBlur(0.2f); - var processor = this.Verify(); + GaussianBlurProcessor processor = this.Verify(); Assert.Equal(.2f, processor.Sigma); } @@ -30,8 +30,8 @@ public class GaussianBlurTest : BaseImageOperationsExtensionTest [Fact] public void GaussianBlur_amount_rect_GaussianBlurProcessorDefaultsSet() { - this.operations.GaussianBlur(0.6f, this.rect); - var processor = this.Verify(this.rect); + this.operations.GaussianBlur(this.rect, 0.6f); + GaussianBlurProcessor processor = this.Verify(this.rect); Assert.Equal(.6f, processor.Sigma); } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/GaussianSharpenTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/GaussianSharpenTest.cs index b48ebac0dc..fd9e64c467 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/GaussianSharpenTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/GaussianSharpenTest.cs @@ -13,7 +13,7 @@ public class GaussianSharpenTest : BaseImageOperationsExtensionTest public void GaussianSharpen_GaussianSharpenProcessorDefaultsSet() { this.operations.GaussianSharpen(); - var processor = this.Verify(); + GaussianSharpenProcessor processor = this.Verify(); Assert.Equal(3f, processor.Sigma); } @@ -22,7 +22,7 @@ public class GaussianSharpenTest : BaseImageOperationsExtensionTest public void GaussianSharpen_amount_GaussianSharpenProcessorDefaultsSet() { this.operations.GaussianSharpen(0.2f); - var processor = this.Verify(); + GaussianSharpenProcessor processor = this.Verify(); Assert.Equal(.2f, processor.Sigma); } @@ -30,8 +30,8 @@ public class GaussianSharpenTest : BaseImageOperationsExtensionTest [Fact] public void GaussianSharpen_amount_rect_GaussianSharpenProcessorDefaultsSet() { - this.operations.GaussianSharpen(0.6f, this.rect); - var processor = this.Verify(this.rect); + this.operations.GaussianSharpen(this.rect, 0.6f); + GaussianSharpenProcessor processor = this.Verify(this.rect); Assert.Equal(.6f, processor.Sigma); } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs index dc497628fb..77fbc70823 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs @@ -13,7 +13,7 @@ public class MedianBlurTest : BaseImageOperationsExtensionTest public void Median_radius_MedianProcessorDefaultsSet() { this.operations.MedianBlur(3, true); - var processor = this.Verify(); + MedianBlurProcessor processor = this.Verify(); Assert.Equal(3, processor.Radius); Assert.True(processor.PreserveAlpha); @@ -22,8 +22,8 @@ public class MedianBlurTest : BaseImageOperationsExtensionTest [Fact] public void Median_radius_rect_MedianProcessorDefaultsSet() { - this.operations.MedianBlur(5, false, this.rect); - var processor = this.Verify(this.rect); + this.operations.MedianBlur(this.rect, 5, false); + MedianBlurProcessor processor = this.Verify(this.rect); Assert.Equal(5, processor.Radius); Assert.False(processor.PreserveAlpha); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs index c94983ecd5..f045c981eb 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Text.RegularExpressions; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Convolution; @@ -165,7 +164,7 @@ public class BokehBlurTest { Size size = x.GetCurrentSize(); Rectangle bounds = new(10, 10, size.Width / 2, size.Height / 2); - x.BokehBlur(value.Radius, value.Components, value.Gamma, bounds); + x.BokehBlur(bounds, value.Radius, value.Components, value.Gamma); }, testOutputDetails: value.ToString(), ImageComparer.TolerantPercentage(0.05f), diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs index 47ccaa46cd..0728bb5d84 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs @@ -12,5 +12,5 @@ public class GaussianBlurTest : Basic1ParameterConvolutionTests protected override void Apply(IImageProcessingContext ctx, int value) => ctx.GaussianBlur(value); protected override void Apply(IImageProcessingContext ctx, int value, Rectangle bounds) => - ctx.GaussianBlur(value, bounds); + ctx.GaussianBlur(bounds, value); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianSharpenTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianSharpenTest.cs index 96c58f7010..c6b7c82363 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianSharpenTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianSharpenTest.cs @@ -12,5 +12,5 @@ public class GaussianSharpenTest : Basic1ParameterConvolutionTests protected override void Apply(IImageProcessingContext ctx, int value) => ctx.GaussianSharpen(value); protected override void Apply(IImageProcessingContext ctx, int value, Rectangle bounds) => - ctx.GaussianSharpen(value, bounds); + ctx.GaussianSharpen(bounds, value); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs index 7cc0ef9d50..048b843580 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs @@ -12,5 +12,5 @@ public class MedianBlurTest : Basic1ParameterConvolutionTests protected override void Apply(IImageProcessingContext ctx, int value) => ctx.MedianBlur(value, true); protected override void Apply(IImageProcessingContext ctx, int value, Rectangle bounds) => - ctx.MedianBlur(value, true, bounds); + ctx.MedianBlur(bounds, value, true); } From b4567a4ac7f74c804df590e4bcd3d88ba4c84b89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2024 13:01:16 +1000 Subject: [PATCH 22/24] Normalize encoders and update refs --- .../Formats/FormatConnectingMetadata.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoder.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 94 ++++++++++++++++--- .../Formats/IAnimatedImageEncoder.cs | 43 +++++++++ .../Formats/IQuantizingImageEncoder.cs | 50 ++++++++++ src/ImageSharp/Formats/Png/PngEncoder.cs | 3 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 48 ++++++++-- .../Formats/QuantizingImageEncoder.cs | 22 ----- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 4 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 50 ++++++++-- .../Formats/Webp/WebpFrameMetadata.cs | 22 +++-- tests/Directory.Build.targets | 16 ++-- .../Codecs/Tga/DecodeTga.cs | 10 +- .../Codecs/Webp/DecodeWebp.cs | 25 ++--- .../Codecs/Webp/EncodeWebp.cs | 10 +- .../ImageSharp.Benchmarks.csproj | 4 +- .../LoadResizeSaveStressRunner.cs | 63 +++++++------ .../Formats/Gif/GifEncoderTests.cs | 2 +- .../Formats/Png/PngEncoderTests.cs | 4 +- .../Formats/WebP/WebpEncoderTests.cs | 12 +-- .../Normalization/MagickCompareTests.cs | 42 ++++----- .../ImageComparison/ImageComparingUtils.cs | 39 ++++---- .../ReferenceCodecs/MagickReferenceDecoder.cs | 4 +- 26 files changed, 391 insertions(+), 194 deletions(-) create mode 100644 src/ImageSharp/Formats/IAnimatedImageEncoder.cs create mode 100644 src/ImageSharp/Formats/IQuantizingImageEncoder.cs delete mode 100644 src/ImageSharp/Formats/QuantizingImageEncoder.cs diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index baf0a35457..9cfe40f385 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -45,7 +45,7 @@ public class FormatConnectingMetadata /// Gets the default background color of the canvas when animating. /// This color may be used to fill the unused space on the canvas around the frames, /// as well as the transparent pixels of the first frame. - /// The background color is also used when the disposal mode is . + /// The background color is also used when a frame disposal mode is . /// /// /// Defaults to . diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 6cb8f9d8ce..37b585c618 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Image encoder for writing image data to a stream in gif format. /// -public sealed class GifEncoder : QuantizingImageEncoder +public sealed class GifEncoder : QuantizingAnimatedImageEncoder { /// /// Gets the color table mode: Global or local. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 2e05ef782f..757b8320dc 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -54,6 +54,19 @@ internal sealed class GifEncoderCore /// private readonly IPixelSamplingStrategy pixelSamplingStrategy; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + /// /// Initializes a new instance of the class. /// @@ -68,6 +81,8 @@ internal sealed class GifEncoderCore this.hasQuantizer = encoder.Quantizer is not null; this.colorTableMode = encoder.ColorTableMode; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; } /// @@ -141,9 +156,17 @@ internal sealed class GifEncoderCore frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); } - byte backgroundIndex = derivedTransparencyIndex >= 0 - ? frameMetadata.TransparencyIndex - : gifMetadata.BackgroundColorIndex; + byte backgroundIndex; + if (this.backgroundColor.HasValue) + { + backgroundIndex = GetBackgroundIndex(quantized, this.backgroundColor.Value); + } + else + { + backgroundIndex = derivedTransparencyIndex >= 0 + ? frameMetadata.TransparencyIndex + : gifMetadata.BackgroundColorIndex; + } // Get the number of bits. int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); @@ -161,7 +184,7 @@ internal sealed class GifEncoderCore // Write application extensions. XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; - this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); + this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile); } this.EncodeFirstFrame(stream, frameMetadata, quantized); @@ -169,7 +192,13 @@ internal sealed class GifEncoderCore // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); - this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode); + this.EncodeAdditionalFrames( + stream, + image, + globalPalette, + derivedTransparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); stream.WriteByte(GifConstants.EndIntroducer); @@ -194,7 +223,8 @@ internal sealed class GifEncoderCore Image image, ReadOnlyMemory globalPalette, int globalTransparencyIndex, - FrameDisposalMode previousDisposalMode) + FrameDisposalMode previousDisposalMode, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (image.Frames.Count == 1) @@ -213,6 +243,16 @@ internal sealed class GifEncoderCore for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + if (hasPaletteQuantizer) + { + paletteQuantizer.Dispose(); + } + + return; + } + // Gather the metadata for this frame. ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; @@ -291,6 +331,10 @@ internal sealed class GifEncoderCore ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame; + Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + // Deduplicate and quantize the frame capturing only required parts. (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -299,7 +343,7 @@ internal sealed class GifEncoderCore currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, true); using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( @@ -428,14 +472,12 @@ internal sealed class GifEncoderCore private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue); /// - /// Returns the index of the most transparent color in the palette. + /// Returns the index of the transparent color in the palette. /// /// The current quantized frame. /// The current gif frame metadata. /// The pixel format. - /// - /// The . - /// + /// The . private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata) where TPixel : unmanaged, IPixel { @@ -463,6 +505,36 @@ internal sealed class GifEncoderCore return index; } + /// + /// Returns the index of the background color in the palette. + /// + /// The current quantized frame. + /// The background color to match. + /// The pixel format. + /// The . + private static byte GetBackgroundIndex(IndexedImageFrame? quantized, Color background) + where TPixel : unmanaged, IPixel + { + int index = -1; + if (quantized != null) + { + TPixel backgroundPixel = background.ToPixel(); + ReadOnlySpan palette = quantized.Palette.Span; + for (int i = 0; i < palette.Length; i++) + { + if (!backgroundPixel.Equals(palette[i])) + { + continue; + } + + index = i; + break; + } + } + + return (byte)Numerics.Clamp(index, 0, 255); + } + /// /// Writes the file header signature and version to the stream. /// diff --git a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs new file mode 100644 index 0000000000..44431aa9a4 --- /dev/null +++ b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for all image encoders that allow encoding animation sequences. +/// +public interface IAnimatedImageEncoder +{ + /// + /// Gets the default background color of the canvas when animating in supported encoders. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + Color? BackgroundColor { get; } + + /// + /// Gets the number of times any animation is repeated in supported encoders. + /// + ushort? RepeatCount { get; } + + /// + /// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders. + /// + bool? AnimateRootFrame { get; } +} + +/// +/// Acts as a base class for all image encoders that allow encoding animation sequences. +/// +public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder +{ + /// + public Color? BackgroundColor { get; init; } + + /// + public ushort? RepeatCount { get; init; } + + /// + public bool? AnimateRootFrame { get; init; } = true; +} diff --git a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs new file mode 100644 index 0000000000..e88b3ecf02 --- /dev/null +++ b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for all image encoders that allow color palette generation via quantization. +/// +public interface IQuantizingImageEncoder +{ + /// + /// Gets the quantizer used to generate the color palette. + /// + IQuantizer? Quantizer { get; } + + /// + /// Gets the used for quantization when building color palettes. + /// + IPixelSamplingStrategy PixelSamplingStrategy { get; } +} + +/// +/// Acts as a base class for all image encoders that allow color palette generation via quantization. +/// +public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder +{ + /// + public IQuantizer? Quantizer { get; init; } + + /// + public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); +} + +/// +/// Acts as a base class for all image encoders that allow color palette generation via quantization when +/// encoding animation sequences. +/// +public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder +{ + /// + public Color? BackgroundColor { get; } + + /// + public ushort? RepeatCount { get; } + + /// + public bool? AnimateRootFrame { get; } +} diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index dcbaf3140d..d9f71e1b56 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -1,6 +1,5 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Image encoder for writing image data to a stream in png format. /// -public class PngEncoder : QuantizingImageEncoder +public class PngEncoder : QuantizingAnimatedImageEncoder { /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 978b9184e9..398c80634c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -123,6 +123,24 @@ internal sealed class PngEncoderCore : IDisposable /// private int derivedTransparencyIndex = -1; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + + /// + /// Whether the root frame is shown as part of the animated sequence. + /// + private readonly bool? animateRootFrame; + /// /// A reusable Crc32 hashing instance. /// @@ -139,6 +157,9 @@ internal sealed class PngEncoderCore : IDisposable this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; this.quantizer = encoder.Quantizer; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; + this.animateRootFrame = encoder.AnimateRootFrame; } /// @@ -171,7 +192,7 @@ internal sealed class PngEncoderCore : IDisposable if (clearTransparency) { currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame); + ClearTransparentPixels(currentFrame, Color.Transparent); } // Do not move this. We require an accurate bit depth for the header chunk. @@ -194,11 +215,15 @@ internal sealed class PngEncoderCore : IDisposable if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount); + this.WriteAnimationControlChunk( + stream, + (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), + this.repeatCount ?? pngMetadata.RepeatCount); } // If the first frame isn't animated, write it as usual and skip it when writing animated frames - if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1) + bool userAnimateRootFrame = this.animateRootFrame == true; + if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1) { FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); @@ -235,12 +260,20 @@ internal sealed class PngEncoderCore : IDisposable for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; currentFrame = image.Frames[currentFrameIndex]; ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; frameMetadata = currentFrame.Metadata.GetPngMetadata(); bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -249,12 +282,12 @@ internal sealed class PngEncoderCore : IDisposable currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend); if (clearTransparency) { - ClearTransparentPixels(encodingFrame); + ClearTransparentPixels(encodingFrame, background); } // Each frame control sequence number must be incremented by the number of frame data chunks that follow. @@ -291,12 +324,13 @@ internal sealed class PngEncoderCore : IDisposable /// /// The type of the pixel. /// The cloned image frame where the transparent pixels will be changed. - private static void ClearTransparentPixels(ImageFrame clone) + /// The color to replace transparent pixels with. + private static void ClearTransparentPixels(ImageFrame clone, Color color) where TPixel : unmanaged, IPixel => clone.ProcessPixelRows(accessor => { // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 transparent = Color.Transparent.ToPixel(); + Rgba32 transparent = color.ToPixel(); for (int y = 0; y < accessor.Height; y++) { Span span = accessor.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/QuantizingImageEncoder.cs b/src/ImageSharp/Formats/QuantizingImageEncoder.cs deleted file mode 100644 index 330d8988c7..0000000000 --- a/src/ImageSharp/Formats/QuantizingImageEncoder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors.Quantization; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Acts as a base class for all image encoders that allow color palette generation via quantization. -/// -public abstract class QuantizingImageEncoder : ImageEncoder -{ - /// - /// Gets the quantizer used to generate the color palette. - /// - public IQuantizer? Quantizer { get; init; } - - /// - /// Gets the used for quantization when building color palettes. - /// - public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy(); -} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 244691e77e..e077249696 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation) + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation, ushort? repeatCount) where TPixel : unmanaged, IPixel { // Write bytes from the bit-writer buffer to the stream. @@ -258,7 +258,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); - BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount); } return vp8x; @@ -315,8 +315,8 @@ internal class Vp8LEncoder : IDisposable (uint)bounds.Width, (uint)bounds.Height, frameMetadata.FrameDelay, - frameMetadata.BlendMethod, - frameMetadata.DisposalMethod) + frameMetadata.BlendMode, + frameMetadata.DisposalMode) .WriteHeaderTo(stream); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3ad72f7d00..d22d357fe3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -495,8 +495,8 @@ internal class Vp8Encoder : IDisposable (uint)bounds.Width, (uint)bounds.Height, frameMetadata.FrameDelay, - frameMetadata.BlendMethod, - frameMetadata.DisposalMethod) + frameMetadata.BlendMode, + frameMetadata.DisposalMode) .WriteHeaderTo(stream); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 72405e480e..bfaaa831e1 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -220,8 +220,8 @@ internal class WebpAnimationDecoder : IDisposable { WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); frameMetadata.FrameDelay = frameData.Duration; - frameMetadata.BlendMethod = frameData.BlendingMethod; - frameMetadata.DisposalMethod = frameData.DisposalMethod; + frameMetadata.BlendMode = frameData.BlendingMethod; + frameMetadata.DisposalMode = frameData.DisposalMethod; } /// diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bc93df3a5b..226719c792 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image encoder for writing an image to a stream in the Webp format. /// -public sealed class WebpEncoder : ImageEncoder +public sealed class WebpEncoder : AnimatedImageEncoder { /// /// Gets the webp file format used. Either lossless or lossy. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 733801d636..37d2ae0a19 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -78,6 +77,19 @@ internal sealed class WebpEncoderCore /// private readonly WebpFileFormatType? fileFormat; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private readonly Color? backgroundColor; + + /// + /// The number of times any animation is repeated. + /// + private readonly ushort? repeatCount; + /// /// The global configuration. /// @@ -103,6 +115,8 @@ internal sealed class WebpEncoderCore this.skipMetadata = encoder.SkipMetadata; this.nearLossless = encoder.NearLossless; this.nearLosslessQuality = encoder.NearLosslessQuality; + this.backgroundColor = encoder.BackgroundColor; + this.repeatCount = encoder.RepeatCount; } /// @@ -147,7 +161,7 @@ internal sealed class WebpEncoderCore long initialPosition = stream.Position; bool hasAlpha = false; - WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation); + WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount); // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; @@ -156,7 +170,7 @@ internal sealed class WebpEncoderCore if (hasAnimation) { - FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. @@ -164,12 +178,20 @@ internal sealed class WebpEncoderCore for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; frameMetadata = currentFrame.Metadata.GetWebpMetadata(); - bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -178,7 +200,7 @@ internal sealed class WebpEncoderCore currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend, ClampingMode.Even); @@ -197,7 +219,7 @@ internal sealed class WebpEncoderCore hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; + previousDisposal = frameMetadata.DisposalMode; } } @@ -229,7 +251,7 @@ internal sealed class WebpEncoderCore // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); - FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; + FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); @@ -239,12 +261,20 @@ internal sealed class WebpEncoderCore for (int i = 1; i < image.Frames.Count; i++) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; frameMetadata = currentFrame.Metadata.GetWebpMetadata(); - bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over; + bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; + Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( @@ -253,7 +283,7 @@ internal sealed class WebpEncoderCore currentFrame, nextFrame, encodingFrame, - Color.Transparent, + background, blend, ClampingMode.Even); @@ -273,7 +303,7 @@ internal sealed class WebpEncoderCore hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; + previousDisposal = frameMetadata.DisposalMode; } encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index 3865f9837f..3f976a6401 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -24,19 +24,21 @@ public class WebpFrameMetadata : IFormatFrameMetadata private WebpFrameMetadata(WebpFrameMetadata other) { this.FrameDelay = other.FrameDelay; - this.DisposalMethod = other.DisposalMethod; - this.BlendMethod = other.BlendMethod; + this.DisposalMode = other.DisposalMode; + this.BlendMode = other.BlendMode; } /// - /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels + /// of the previous canvas. /// - public FrameBlendMode BlendMethod { get; set; } + public FrameBlendMode BlendMode { get; set; } /// - /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// Gets or sets how the current frame is to be treated after it has been displayed + /// (before rendering the next frame) on the canvas. /// - public FrameDisposalMode DisposalMethod { get; set; } + public FrameDisposalMode DisposalMode { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, @@ -49,8 +51,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata => new() { FrameDelay = (uint)metadata.Duration.TotalMilliseconds, - BlendMethod = metadata.BlendMode, - DisposalMethod = GetMode(metadata.DisposalMode) + BlendMode = metadata.BlendMode, + DisposalMode = GetMode(metadata.DisposalMode) }; /// @@ -59,8 +61,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata { ColorTableMode = FrameColorTableMode.Global, Duration = TimeSpan.FromMilliseconds(this.FrameDelay), - DisposalMode = this.DisposalMethod, - BlendMode = this.BlendMethod, + DisposalMode = this.DisposalMode, + BlendMode = this.BlendMode, }; /// diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index d6b35d003f..a88884a407 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,18 +18,18 @@ - - + + - - - - - + + + + + - + diff --git a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs index 2ebab5e001..7a3fc0b395 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs @@ -29,22 +29,22 @@ public class DecodeTga [Benchmark(Baseline = true, Description = "ImageMagick Tga")] public int TgaImageMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.Tga }; - using var image = new MagickImage(new MemoryStream(this.data), settings); - return image.Width; + MagickReadSettings settings = new() { Format = MagickFormat.Tga }; + using MagickImage image = new(new MemoryStream(this.data), settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Tga")] public int TgaImageSharp() { - using var image = Image.Load(this.data); + using Image image = Image.Load(this.data); return image.Width; } [Benchmark(Description = "Pfim Tga")] public int TgaPfim() { - using var image = Targa.Create(this.data, this.pfimConfig); + using Targa image = Targa.Create(this.data, this.pfimConfig); return image.Width; } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs index 6c71a62b50..4d6252c2b6 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs @@ -44,34 +44,35 @@ public class DecodeWebp [Benchmark(Description = "Magick Lossy Webp")] public int WebpLossyMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.WebP }; - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = new MagickImage(memoryStream, settings); - return image.Width; + MagickReadSettings settings = new() { Format = MagickFormat.WebP }; + using MemoryStream memoryStream = new(this.webpLossyBytes); + using MagickImage image = new(memoryStream, settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Lossy Webp")] public int WebpLossy() { - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = Image.Load(memoryStream); + using MemoryStream memoryStream = new(this.webpLossyBytes); + using Image image = Image.Load(memoryStream); return image.Height; } [Benchmark(Description = "Magick Lossless Webp")] public int WebpLosslessMagick() { - var settings = new MagickReadSettings { Format = MagickFormat.WebP }; - using var memoryStream = new MemoryStream(this.webpLossyBytes); - using var image = new MagickImage(memoryStream, settings); - return image.Width; + MagickReadSettings settings = new() + { Format = MagickFormat.WebP }; + using MemoryStream memoryStream = new(this.webpLossyBytes); + using MagickImage image = new(memoryStream, settings); + return (int)image.Width; } [Benchmark(Description = "ImageSharp Lossless Webp")] public int WebpLossless() { - using var memoryStream = new MemoryStream(this.webpLosslessBytes); - using var image = Image.Load(memoryStream); + using MemoryStream memoryStream = new(this.webpLosslessBytes); + using Image image = Image.Load(memoryStream); return image.Height; } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs index 5be46a2220..d78640549d 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs @@ -13,7 +13,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs; [MarkdownExporter] [HtmlExporter] [Config(typeof(Config.Short))] +#pragma warning disable CA1001 // Types that own disposable fields should be disposable public class EncodeWebp +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { private MagickImage webpMagick; private Image webp; @@ -52,10 +54,7 @@ public class EncodeWebp AlphaCompression = WebPAlphaCompression.None, FilterStrength = 60, SnsStrength = 50, - Pass = 1, - - // 100 means off. - NearLossless = 100 + Pass = 1 }; this.webpMagick.Quality = 75; @@ -85,9 +84,6 @@ public class EncodeWebp { Lossless = true, Method = 4, - - // 100 means off. - NearLossless = 100 }; this.webpMagick.Quality = 75; diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index a705b24b28..37a991248c 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -41,8 +41,8 @@ - - + + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index a06784f1b1..62f41fe912 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -19,6 +19,7 @@ using SystemDrawingImage = System.Drawing.Image; namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave; +[Flags] public enum JpegKind { Baseline = 1, @@ -30,7 +31,7 @@ public class LoadResizeSaveStressRunner { private const int Quality = 75; - // Set the quality for ImagSharp + // Set the quality for ImageSharp private readonly JpegEncoder imageSharpJpegEncoder = new() { Quality = Quality }; private readonly ImageCodecInfo systemDrawingJpegCodec = ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid); @@ -126,7 +127,7 @@ public class LoadResizeSaveStressRunner : Environment.ProcessorCount; int partitionSize = (int)Math.Ceiling((double)this.Images.Length / maxDegreeOfParallelism); - List tasks = new(); + List tasks = []; for (int i = 0; i < this.Images.Length; i += partitionSize) { int end = Math.Min(i + partitionSize, this.Images.Length); @@ -176,13 +177,13 @@ public class LoadResizeSaveStressRunner public void SystemDrawingResize(string input) { - using var image = SystemDrawingImage.FromFile(input, true); + using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true); this.LogImageProcessed(image.Width, image.Height); - (int Width, int Height) scaled = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); - var resized = new Bitmap(scaled.Width, scaled.Height); - using var graphics = Graphics.FromImage(resized); - using var attributes = new ImageAttributes(); + (int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); + Bitmap resized = new(width, height); + using Graphics graphics = Graphics.FromImage(resized); + using ImageAttributes attributes = new(); attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.CompositingMode = CompositingMode.SourceCopy; @@ -191,8 +192,8 @@ public class LoadResizeSaveStressRunner graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes); // Save the results - using var encoderParams = new EncoderParameters(1); - using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality); + using EncoderParameters encoderParams = new(1); + using EncoderParameter qualityParam = new(Encoder.Quality, (long)Quality); encoderParams.Param[0] = qualityParam; resized.Save(this.OutputPath(input), this.systemDrawingJpegCodec, encoderParams); } @@ -223,7 +224,7 @@ public class LoadResizeSaveStressRunner public async Task ImageSharpResizeAsync(string input) { - using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); + await using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); // Resize it to fit a 150x150 square. DecoderOptions options = new() @@ -246,11 +247,11 @@ public class LoadResizeSaveStressRunner public void MagickResize(string input) { - using var image = new MagickImage(input); - this.LogImageProcessed(image.Width, image.Height); + using MagickImage image = new(input); + this.LogImageProcessed((int)image.Width, (int)image.Height); // Resize it to fit a 150x150 square - image.Resize(this.ThumbnailSize, this.ThumbnailSize); + image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); // Reduce the size of the file image.Strip(); @@ -264,7 +265,7 @@ public class LoadResizeSaveStressRunner public void MagicScalerResize(string input) { - var settings = new ProcessImageSettings() + ProcessImageSettings settings = new() { Width = this.ThumbnailSize, Height = this.ThumbnailSize, @@ -273,19 +274,19 @@ public class LoadResizeSaveStressRunner }; // TODO: Is there a way to capture input dimensions for IncreaseTotalMegapixels? - using var output = new FileStream(this.OutputPath(input), FileMode.Create); + using FileStream output = new(this.OutputPath(input), FileMode.Create); MagicImageProcessor.ProcessImage(input, output, settings); } public void SkiaCanvasResize(string input) { - using var original = SKBitmap.Decode(input); + using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); - using var surface = SKSurface.Create(new SKImageInfo(scaled.Width, scaled.Height, original.ColorType, original.AlphaType)); - using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High }; + (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType)); + using SKPaint paint = new() { FilterQuality = SKFilterQuality.High }; SKCanvas canvas = surface.Canvas; - canvas.Scale((float)scaled.Width / original.Width); + canvas.Scale((float)width / original.Width); canvas.DrawBitmap(original, 0, 0, paint); canvas.Flush(); @@ -297,16 +298,16 @@ public class LoadResizeSaveStressRunner public void SkiaBitmapResize(string input) { - using var original = SKBitmap.Decode(input); + using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); - using var resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); + (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); if (resized == null) { return; } - using var image = SKImage.FromBitmap(resized); + using SKImage image = SKImage.FromBitmap(resized); using FileStream output = File.OpenWrite(this.OutputPath(input)); image.Encode(SKEncodedImageFormat.Jpeg, Quality) .SaveTo(output); @@ -314,21 +315,21 @@ public class LoadResizeSaveStressRunner public void SkiaBitmapDecodeToTargetSize(string input) { - using var codec = SKCodec.Create(input); + using SKCodec codec = SKCodec.Create(input); SKImageInfo info = codec.Info; this.LogImageProcessed(info.Width, info.Height); - (int Width, int Height) scaled = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); - SKSizeI supportedScale = codec.GetScaledDimensions((float)scaled.Width / info.Width); + (int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); + SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width); - using var original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); - using SKBitmap resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); + using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); + using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); if (resized == null) { return; } - using var image = SKImage.FromBitmap(resized); + using SKImage image = SKImage.FromBitmap(resized); using FileStream output = File.OpenWrite(this.OutputPath(input, nameof(this.SkiaBitmapDecodeToTargetSize))); image.Encode(SKEncodedImageFormat.Jpeg, Quality) @@ -338,7 +339,7 @@ public class LoadResizeSaveStressRunner public void NetVipsResize(string input) { // Thumbnail to fit a 150x150 square - using var thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize); + using NetVipsImage thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize); // Save the results thumb.Jpegsave(this.OutputPath(input), q: Quality, keep: NetVips.Enums.ForeignKeep.None); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 77ac51e8a1..c08db84eb6 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -334,7 +334,7 @@ public class GifEncoderTests Assert.Equal(webpF.FrameDelay, (uint)(gifF.FrameDelay * 10)); - switch (webpF.DisposalMethod) + switch (webpF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMode); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 009327c17f..53a795c649 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -524,7 +524,7 @@ public partial class PngEncoderTests Assert.Equal(webpF.FrameDelay, (uint)(pngF.FrameDelay.ToDouble() * 1000)); - switch (webpF.BlendMethod) + switch (webpF.BlendMode) { case FrameBlendMode.Source: Assert.Equal(FrameBlendMode.Source, pngF.BlendMode); @@ -535,7 +535,7 @@ public partial class PngEncoderTests break; } - switch (webpF.DisposalMethod) + switch (webpF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: Assert.Equal(FrameDisposalMode.RestoreToBackground, pngF.DisposalMode); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 11b1749abb..0aa56e252e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -98,13 +98,13 @@ public class WebpEncoderTests switch (gifF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: - Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMode); break; case FrameDisposalMode.RestoreToPrevious: case FrameDisposalMode.Unspecified: case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMode); break; } } @@ -147,22 +147,22 @@ public class WebpEncoderTests switch (pngF.BlendMode) { case FrameBlendMode.Source: - Assert.Equal(FrameBlendMode.Source, webpF.BlendMethod); + Assert.Equal(FrameBlendMode.Source, webpF.BlendMode); break; case FrameBlendMode.Over: default: - Assert.Equal(FrameBlendMode.Over, webpF.BlendMethod); + Assert.Equal(FrameBlendMode.Over, webpF.BlendMode); break; } switch (pngF.DisposalMode) { case FrameDisposalMode.RestoreToBackground: - Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMode); break; case FrameDisposalMode.DoNotDispose: default: - Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod); + Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMode); break; } } diff --git a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs index 5fb0a4e934..2b609a9b2a 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs @@ -1,13 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using ImageMagick; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Normalization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using ImageMagick; - namespace SixLabors.ImageSharp.Tests.Processing.Normalization; // ReSharper disable InconsistentNaming @@ -22,46 +21,43 @@ public class MagickCompareTests Image imageFromMagick; using (Stream stream = LoadAsStream(provider)) { - var magickImage = new MagickImage(stream); + using MagickImage magickImage = new(stream); // Apply Auto Level using the Grey (BT.709) channel. magickImage.AutoLevel(Channels.Gray); imageFromMagick = ConvertImageFromMagick(magickImage); } - using (Image image = provider.GetImage()) + using Image image = provider.GetImage(); + HistogramEqualizationOptions options = new() { - var options = new HistogramEqualizationOptions - { - Method = HistogramEqualizationMethod.AutoLevel, - LuminanceLevels = 256, - SyncChannels = true - }; - image.Mutate(x => x.HistogramEqualization(options)); - image.DebugSave(provider); - ExactImageComparer.Instance.CompareImages(imageFromMagick, image); - } + Method = HistogramEqualizationMethod.AutoLevel, + LuminanceLevels = 256, + SyncChannels = true + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + ExactImageComparer.Instance.CompareImages(imageFromMagick, image); + + imageFromMagick.Dispose(); } - private Stream LoadAsStream(TestImageProvider provider) + private static FileStream LoadAsStream(TestImageProvider provider) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToMagick() works only with file providers!"); - } + string path = TestImageProvider.GetFilePathOrNull(provider) + ?? throw new InvalidOperationException("CompareToMagick() works only with file providers!"); - var testFile = TestFile.Create(path); + TestFile testFile = TestFile.Create(path); return new FileStream(testFile.FullPath, FileMode.Open); } - private Image ConvertImageFromMagick(MagickImage magickImage) + private static Image ConvertImageFromMagick(MagickImage magickImage) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { Configuration configuration = Configuration.Default.Clone(); configuration.PreferContiguousImageBuffers = true; - var result = new Image(configuration, magickImage.Width, magickImage.Height); + Image result = new(configuration, (int)magickImage.Width, (int)magickImage.Height); Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs index f5c70b0885..05f65cfbbc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs @@ -15,13 +15,10 @@ public static class ImageComparingUtils float compareTolerance = 0.01f) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - } + string path = TestImageProvider.GetFilePathOrNull(provider) + ?? throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - var testFile = TestFile.Create(path); + TestFile testFile = TestFile.Create(path); using Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); if (useExactComparer) { @@ -38,25 +35,23 @@ public static class ImageComparingUtils { Configuration configuration = Configuration.Default.Clone(); configuration.PreferContiguousImageBuffers = true; - using (var magickImage = new MagickImage(fileInfo)) - { - magickImage.AutoOrient(); - var result = new Image(configuration, magickImage.Width, magickImage.Height); + using MagickImage magickImage = new(fileInfo); + magickImage.AutoOrient(); + Image result = new(configuration, (int)magickImage.Width, (int)magickImage.Height); - Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); + Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); - using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels.Span, - resultPixels.Length); - } + using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - return result; + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels.Span, + resultPixels.Length); } + + return result; } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index f96dc19ee0..3cb0381950 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -58,7 +58,7 @@ public class MagickReferenceDecoder : ImageDecoder MagickReadSettings settings = new() { - FrameCount = (int)options.MaxFrames + FrameCount = options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines); @@ -67,7 +67,7 @@ public class MagickReferenceDecoder : ImageDecoder List> framesList = []; foreach (IMagickImage magicFrame in magickImageCollection) { - ImageFrame frame = new(configuration, magicFrame.Width, magicFrame.Height); + ImageFrame frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height); framesList.Add(frame); MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; From 45965112589857ff8891740e4b80b1656e0c284b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 20 Nov 2024 21:39:34 +1000 Subject: [PATCH 23/24] Clean up background index check --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 757b8320dc..0ed7e8c98d 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -156,12 +156,7 @@ internal sealed class GifEncoderCore frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); } - byte backgroundIndex; - if (this.backgroundColor.HasValue) - { - backgroundIndex = GetBackgroundIndex(quantized, this.backgroundColor.Value); - } - else + if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex)) { backgroundIndex = derivedTransparencyIndex >= 0 ? frameMetadata.TransparencyIndex @@ -510,15 +505,19 @@ internal sealed class GifEncoderCore /// /// The current quantized frame. /// The background color to match. + /// The index in the palette of the background color. /// The pixel format. - /// The . - private static byte GetBackgroundIndex(IndexedImageFrame? quantized, Color background) + /// The . + private static bool TryGetBackgroundIndex( + IndexedImageFrame? quantized, + Color? background, + out byte index) where TPixel : unmanaged, IPixel { - int index = -1; - if (quantized != null) + int match = -1; + if (quantized != null && background.HasValue) { - TPixel backgroundPixel = background.ToPixel(); + TPixel backgroundPixel = background.Value.ToPixel(); ReadOnlySpan palette = quantized.Palette.Span; for (int i = 0; i < palette.Length; i++) { @@ -527,12 +526,19 @@ internal sealed class GifEncoderCore continue; } - index = i; + match = i; break; } } - return (byte)Numerics.Clamp(index, 0, 255); + if (match >= 0) + { + index = (byte)Numerics.Clamp(match, 0, 255); + return true; + } + + index = 0; + return false; } /// From f4923598bb784a33daf8a979c460d4f3cf7d9b1b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 21 Nov 2024 20:18:54 +1000 Subject: [PATCH 24/24] Limit Magick.NET version for now --- tests/Directory.Build.targets | 7 ++++++- .../LoadResizeSave/LoadResizeSaveStressRunner.cs | 4 ++-- .../ReferenceCodecs/MagickReferenceDecoder.cs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index a88884a407..e20717cdd3 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,7 +19,12 @@ - + + + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 62f41fe912..e7d240acd3 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -248,10 +248,10 @@ public class LoadResizeSaveStressRunner public void MagickResize(string input) { using MagickImage image = new(input); - this.LogImageProcessed((int)image.Width, (int)image.Height); + this.LogImageProcessed(image.Width, image.Height); // Resize it to fit a 150x150 square - image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); + image.Resize(this.ThumbnailSize, this.ThumbnailSize); // Reduce the size of the file image.Strip(); diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 3cb0381950..74015a4eff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -58,7 +58,7 @@ public class MagickReferenceDecoder : ImageDecoder MagickReadSettings settings = new() { - FrameCount = options.MaxFrames + FrameCount = (int)options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines);