Browse Source

Merge pull request #888 from SixLabors/af/resize-sandbox

Limit ResizeProcessor memory consumption
pull/893/head
Anton Firsov 7 years ago
committed by GitHub
parent
commit
87c0a664ae
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      src/ImageSharp/Configuration.cs
  2. 160
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  3. 30
      src/ImageSharp/Memory/RowInterval.cs
  4. 18
      src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs
  5. 339
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs
  6. 28
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  7. 11
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  8. 129
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
  9. 193
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
  10. BIN
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx
  11. 102
      tests/ImageSharp.Benchmarks/General/ArrayCopy.cs
  12. 231
      tests/ImageSharp.Benchmarks/General/CopyBuffers.cs
  13. 61
      tests/ImageSharp.Benchmarks/Samplers/Resize.cs
  14. 2
      tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj
  15. 6
      tests/ImageSharp.Sandbox46/Program.cs
  16. 17
      tests/ImageSharp.Tests/ConfigurationTests.cs
  17. 49
      tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs
  18. 51
      tests/ImageSharp.Tests/Memory/Buffer2DTests.cs
  19. 65
      tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs
  20. 1
      tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs
  21. 69
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs
  22. 35
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs
  23. 2
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
  24. 12
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
  25. 480
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  26. 37
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs
  27. 65
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs
  28. 5
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs
  29. 2
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  30. 3
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs
  31. 8
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs
  32. 8
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs
  33. 13
      tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs
  34. 39
      tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs
  35. 452
      tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs
  36. 2
      tests/Images/External

26
src/ImageSharp/Configuration.cs

@ -102,6 +102,15 @@ namespace SixLabors.ImageSharp
/// </summary>
internal IFileSystem FileSystem { get; set; } = new LocalFileSystem();
/// <summary>
/// Gets or sets the working buffer size hint for image processors.
/// The default value is 1MB.
/// </summary>
/// <remarks>
/// Currently only used by Resize.
/// </remarks>
internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024;
/// <summary>
/// Gets or sets the image operations provider factory.
/// </summary>
@ -118,9 +127,9 @@ namespace SixLabors.ImageSharp
}
/// <summary>
/// Creates a shallow copy of the <see cref="Configuration"/>
/// Creates a shallow copy of the <see cref="Configuration"/>.
/// </summary>
/// <returns>A new configuration instance</returns>
/// <returns>A new configuration instance.</returns>
public Configuration Clone()
{
return new Configuration
@ -130,18 +139,19 @@ namespace SixLabors.ImageSharp
MemoryAllocator = this.MemoryAllocator,
ImageOperationsProvider = this.ImageOperationsProvider,
ReadOrigin = this.ReadOrigin,
FileSystem = this.FileSystem
FileSystem = this.FileSystem,
WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes,
};
}
/// <summary>
/// Creates the default instance with the following <see cref="IConfigurationModule"/>s preregistered:
/// <para><see cref="PngConfigurationModule"/></para>
/// <para><see cref="JpegConfigurationModule"/></para>
/// <para><see cref="GifConfigurationModule"/></para>
/// <para><see cref="BmpConfigurationModule"/></para>
/// <see cref="PngConfigurationModule"/>
/// <see cref="JpegConfigurationModule"/>
/// <see cref="GifConfigurationModule"/>
/// <see cref="BmpConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/></returns>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance()
{
return new Configuration(

160
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.Primitives;
@ -14,55 +17,135 @@ namespace SixLabors.ImageSharp.Memory
internal static class Buffer2DExtensions
{
/// <summary>
/// Gets a <see cref="Span{T}"/> to the backing buffer of <paramref name="buffer"/>.
/// Copy <paramref name="columnCount"/> columns of <paramref name="buffer"/> inplace,
/// from positions starting at <paramref name="sourceIndex"/> to positions at <paramref name="destIndex"/>.
/// </summary>
internal static Span<T> GetSpan<T>(this Buffer2D<T> buffer)
public static unsafe void CopyColumns<T>(
this Buffer2D<T> buffer,
int sourceIndex,
int destIndex,
int columnCount)
where T : struct
{
return buffer.MemorySource.GetSpan();
DebugGuard.NotNull(buffer, nameof(buffer));
DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex));
DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex));
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount);
int elementSize = Unsafe.SizeOf<T>();
int width = buffer.Width * elementSize;
int sOffset = sourceIndex * elementSize;
int dOffset = destIndex * elementSize;
long count = columnCount * elementSize;
Span<byte> span = MemoryMarshal.AsBytes(buffer.Memory.Span);
fixed (byte* ptr = span)
{
byte* basePtr = (byte*)ptr;
for (int y = 0; y < buffer.Height; y++)
{
byte* sPtr = basePtr + sOffset;
byte* dPtr = basePtr + dOffset;
Buffer.MemoryCopy(sPtr, dPtr, count, count);
basePtr += width;
}
}
}
/// <summary>
/// Gets a <see cref="Span{T}"/> to the row 'y' beginning from the pixel at 'x'.
/// Returns a <see cref="Rectangle"/> representing the full area of the buffer.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns>
public static Rectangle FullRectangle<T>(this Buffer2D<T> buffer)
where T : struct
{
return new Rectangle(0, 0, buffer.Width, buffer.Height);
}
/// <summary>
/// Return a <see cref="BufferArea{T}"/> to the subarea represented by 'rectangle'
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <param name="rectangle">The rectangle subarea</param>
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, in Rectangle rectangle)
where T : struct =>
new BufferArea<T>(buffer, rectangle);
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : struct =>
new BufferArea<T>(buffer, new Rectangle(x, y, width, height));
/// <summary>
/// Return a <see cref="BufferArea{T}"/> to the whole area of 'buffer'
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer)
where T : struct =>
new BufferArea<T>(buffer);
public static BufferArea<T> GetAreaBetweenRows<T>(this Buffer2D<T> buffer, int minY, int maxY)
where T : struct =>
new BufferArea<T>(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY));
/// <summary>
/// Gets a span for all the pixels in <paramref name="buffer"/> defined by <paramref name="rows"/>
/// </summary>
public static Span<T> GetMultiRowSpan<T>(this Buffer2D<T> buffer, in RowInterval rows)
where T : struct
{
return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width);
}
/// <summary>
/// Gets a <see cref="Memory{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <param name="x">The x coordinate (position in the row)</param>
/// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> GetRowSpan<T>(this Buffer2D<T> buffer, int x, int y)
public static Memory<T> GetRowMemory<T>(this Buffer2D<T> buffer, int y)
where T : struct
{
return buffer.GetSpan().Slice((y * buffer.Width) + x, buffer.Width - x);
return buffer.MemorySource.Memory.Slice(y * buffer.Width, buffer.Width);
}
/// <summary>
/// Gets a <see cref="Span{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// Gets a <see cref="Span{T}"/> to the row 'y' beginning from the pixel at 'x'.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <param name="x">The x coordinate (position in the row)</param>
/// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> GetRowSpan<T>(this Buffer2D<T> buffer, int y)
public static Span<T> GetRowSpan<T>(this Buffer2D<T> buffer, int x, int y)
where T : struct
{
return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width);
return buffer.GetSpan().Slice((y * buffer.Width) + x, buffer.Width - x);
}
/// <summary>
/// Gets a <see cref="Memory{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// Gets a <see cref="Span{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Memory<T> GetRowMemory<T>(this Buffer2D<T> buffer, int y)
public static Span<T> GetRowSpan<T>(this Buffer2D<T> buffer, int y)
where T : struct
{
return buffer.MemorySource.Memory.Slice(y * buffer.Width, buffer.Width);
return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width);
}
/// <summary>
@ -78,49 +161,28 @@ namespace SixLabors.ImageSharp.Memory
}
/// <summary>
/// Returns a <see cref="Rectangle"/> representing the full area of the buffer.
/// Gets a <see cref="Span{T}"/> to the backing buffer of <paramref name="buffer"/>.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns>
public static Rectangle FullRectangle<T>(this Buffer2D<T> buffer)
internal static Span<T> GetSpan<T>(this Buffer2D<T> buffer)
where T : struct
{
return new Rectangle(0, 0, buffer.Width, buffer.Height);
return buffer.MemorySource.GetSpan();
}
/// <summary>
/// Return a <see cref="BufferArea{T}"/> to the subarea represented by 'rectangle'
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <param name="rectangle">The rectangle subarea</param>
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, in Rectangle rectangle)
where T : struct => new BufferArea<T>(buffer, rectangle);
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : struct => new BufferArea<T>(buffer, new Rectangle(x, y, width, height));
public static BufferArea<T> GetAreaBetweenRows<T>(this Buffer2D<T> buffer, int minY, int maxY)
where T : struct => new BufferArea<T>(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY));
/// <summary>
/// Return a <see cref="BufferArea{T}"/> to the whole area of 'buffer'
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer)
where T : struct => new BufferArea<T>(buffer);
/// <summary>
/// Gets a span for all the pixels in <paramref name="buffer"/> defined by <paramref name="rows"/>
/// </summary>
public static Span<T> GetMultiRowSpan<T>(this Buffer2D<T> buffer, in RowInterval rows)
[Conditional("DEBUG")]
private static void CheckColumnRegionsDoNotOverlap<T>(
Buffer2D<T> buffer,
int sourceIndex,
int destIndex,
int columnCount)
where T : struct
{
return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width);
int minIndex = Math.Min(sourceIndex, destIndex);
int maxIndex = Math.Max(sourceIndex, destIndex);
if (maxIndex < minIndex + columnCount || maxIndex > buffer.Width - columnCount)
{
throw new InvalidOperationException("Column regions should not overlap!");
}
}
}
}

30
src/ImageSharp/Memory/RowInterval.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Memory
@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Memory
/// <summary>
/// Represents an interval of rows in a <see cref="Rectangle"/> and/or <see cref="Buffer2D{T}"/>
/// </summary>
internal readonly struct RowInterval
internal readonly struct RowInterval : IEquatable<RowInterval>
{
/// <summary>
/// Initializes a new instance of the <see cref="RowInterval"/> struct.
@ -36,7 +38,33 @@ namespace SixLabors.ImageSharp.Memory
/// </summary>
public int Height => this.Max - this.Min;
public static bool operator ==(RowInterval left, RowInterval right)
{
return left.Equals(right);
}
public static bool operator !=(RowInterval left, RowInterval right)
{
return !left.Equals(right);
}
/// <inheritdoc />
public override string ToString() => $"RowInterval [{this.Min}->{this.Max}]";
public RowInterval Slice(int start) => new RowInterval(this.Min + start, this.Max);
public RowInterval Slice(int start, int length) => new RowInterval(this.Min + start, this.Min + start + length);
public bool Equals(RowInterval other)
{
return this.Min == other.Min && this.Max == other.Max;
}
public override bool Equals(object obj)
{
return !ReferenceEquals(null, obj) && obj is RowInterval other && this.Equals(other);
}
public override int GetHashCode() => HashCode.Combine(this.Min, this.Max);
}
}

18
src/ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs

@ -5,6 +5,9 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.PixelFormats
{
/// <summary>
/// Extension and utility methods for <see cref="PixelConversionModifiers"/>.
/// </summary>
internal static class PixelConversionModifiersExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -16,5 +19,20 @@ namespace SixLabors.ImageSharp.PixelFormats
this PixelConversionModifiers modifiers,
PixelConversionModifiers removeThis) =>
modifiers & ~removeThis;
/// <summary>
/// Applies the union of <see cref="PixelConversionModifiers.Scale"/> and <see cref="PixelConversionModifiers.SRgbCompand"/>,
/// if <paramref name="compand"/> is true, returns unmodified <paramref name="originalModifiers"/> otherwise.
/// </summary>
/// <remarks>
/// <see cref="PixelConversionModifiers.Scale"/> and <see cref="PixelConversionModifiers.SRgbCompand"/>
/// should be always used together!
/// </remarks>
public static PixelConversionModifiers ApplyCompanding(
this PixelConversionModifiers originalModifiers,
bool compand) =>
compand
? originalModifiers | PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand
: originalModifiers;
}
}

339
src/ImageSharp/Processing/ResizeHelper.cs → src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs

@ -1,11 +1,13 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Linq;
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Provides methods to help calculate the target rectangle when resizing using the
@ -13,6 +15,16 @@ namespace SixLabors.ImageSharp.Processing
/// </summary>
internal static class ResizeHelper
{
public static unsafe int CalculateResizeWorkerHeightInWindowBands(
int windowBandHeight,
int width,
int sizeLimitHintInBytes)
{
int sizeLimitHint = sizeLimitHintInBytes / sizeof(Vector4);
int sizeOfOneWindow = windowBandHeight * width;
return Math.Max(2, sizeLimitHint / sizeOfOneWindow);
}
/// <summary>
/// Calculates the target location and bounds to perform the resize operation against.
/// </summary>
@ -21,9 +33,13 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="width">The target width</param>
/// <param name="height">The target height</param>
/// <returns>
/// The <see cref="ValueTuple{Size,Rectangle}"/>.
/// The tuple representing the location and the bounds
/// </returns>
public static (Size, Rectangle) CalculateTargetLocationAndBounds(Size sourceSize, ResizeOptions options, int width, int height)
public static (Size, Rectangle) CalculateTargetLocationAndBounds(
Size sourceSize,
ResizeOptions options,
int width,
int height)
{
switch (options.Mode)
{
@ -44,7 +60,90 @@ namespace SixLabors.ImageSharp.Processing
}
}
private static (Size, Rectangle) CalculateCropRectangle(Size source, ResizeOptions options, int width, int height)
private static (Size, Rectangle) CalculateBoxPadRectangle(
Size source,
ResizeOptions options,
int width,
int height)
{
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
int sourceWidth = source.Width;
int sourceHeight = source.Height;
// Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight);
float percentWidth = MathF.Abs(width / (float)sourceWidth);
int boxPadHeight = height > 0 ? height : (int)MathF.Round(sourceHeight * percentWidth);
int boxPadWidth = width > 0 ? width : (int)MathF.Round(sourceWidth * percentHeight);
// Only calculate if upscaling.
if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight)
{
int destinationX;
int destinationY;
int destinationWidth = sourceWidth;
int destinationHeight = sourceHeight;
width = boxPadWidth;
height = boxPadHeight;
switch (options.Position)
{
case AnchorPositionMode.Left:
destinationY = (height - sourceHeight) / 2;
destinationX = 0;
break;
case AnchorPositionMode.Right:
destinationY = (height - sourceHeight) / 2;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.TopRight:
destinationY = 0;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.Top:
destinationY = 0;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPositionMode.TopLeft:
destinationY = 0;
destinationX = 0;
break;
case AnchorPositionMode.BottomRight:
destinationY = height - sourceHeight;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.Bottom:
destinationY = height - sourceHeight;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPositionMode.BottomLeft:
destinationY = height - sourceHeight;
destinationX = 0;
break;
default:
destinationY = (height - sourceHeight) / 2;
destinationX = (width - sourceWidth) / 2;
break;
}
return (new Size(width, height),
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
// Switch to pad mode to downscale and calculate from there.
return CalculatePadRectangle(source, options, width, height);
}
private static (Size, Rectangle) CalculateCropRectangle(
Size source,
ResizeOptions options,
int width,
int height)
{
if (width <= 0 || height <= 0)
{
@ -147,152 +246,15 @@ namespace SixLabors.ImageSharp.Processing
destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight);
}
return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
private static (Size, Rectangle) CalculatePadRectangle(Size source, ResizeOptions options, int width, int height)
{
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
float ratio;
int sourceWidth = source.Width;
int sourceHeight = source.Height;
int destinationX = 0;
int destinationY = 0;
int destinationWidth = width;
int destinationHeight = height;
// Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight);
float percentWidth = MathF.Abs(width / (float)sourceWidth);
if (percentHeight < percentWidth)
{
ratio = percentHeight;
destinationWidth = (int)MathF.Round(sourceWidth * percentHeight);
switch (options.Position)
{
case AnchorPositionMode.Left:
case AnchorPositionMode.TopLeft:
case AnchorPositionMode.BottomLeft:
destinationX = 0;
break;
case AnchorPositionMode.Right:
case AnchorPositionMode.TopRight:
case AnchorPositionMode.BottomRight:
destinationX = (int)MathF.Round(width - (sourceWidth * ratio));
break;
default:
destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F);
break;
}
}
else
{
ratio = percentWidth;
destinationHeight = (int)MathF.Round(sourceHeight * percentWidth);
switch (options.Position)
{
case AnchorPositionMode.Top:
case AnchorPositionMode.TopLeft:
case AnchorPositionMode.TopRight:
destinationY = 0;
break;
case AnchorPositionMode.Bottom:
case AnchorPositionMode.BottomLeft:
case AnchorPositionMode.BottomRight:
destinationY = (int)MathF.Round(height - (sourceHeight * ratio));
break;
default:
destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F);
break;
}
}
return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
private static (Size, Rectangle) CalculateBoxPadRectangle(Size source, ResizeOptions options, int width, int height)
{
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
int sourceWidth = source.Width;
int sourceHeight = source.Height;
// Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight);
float percentWidth = MathF.Abs(width / (float)sourceWidth);
int boxPadHeight = height > 0 ? height : (int)MathF.Round(sourceHeight * percentWidth);
int boxPadWidth = width > 0 ? width : (int)MathF.Round(sourceWidth * percentHeight);
// Only calculate if upscaling.
if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight)
{
int destinationX;
int destinationY;
int destinationWidth = sourceWidth;
int destinationHeight = sourceHeight;
width = boxPadWidth;
height = boxPadHeight;
switch (options.Position)
{
case AnchorPositionMode.Left:
destinationY = (height - sourceHeight) / 2;
destinationX = 0;
break;
case AnchorPositionMode.Right:
destinationY = (height - sourceHeight) / 2;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.TopRight:
destinationY = 0;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.Top:
destinationY = 0;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPositionMode.TopLeft:
destinationY = 0;
destinationX = 0;
break;
case AnchorPositionMode.BottomRight:
destinationY = height - sourceHeight;
destinationX = width - sourceWidth;
break;
case AnchorPositionMode.Bottom:
destinationY = height - sourceHeight;
destinationX = (width - sourceWidth) / 2;
break;
case AnchorPositionMode.BottomLeft:
destinationY = height - sourceHeight;
destinationX = 0;
break;
default:
destinationY = (height - sourceHeight) / 2;
destinationX = (width - sourceWidth) / 2;
break;
}
return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
// Switch to pad mode to downscale and calculate from there.
return CalculatePadRectangle(source, options, width, height);
return (new Size(width, height),
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
private static (Size, Rectangle) CalculateMaxRectangle(Size source, ResizeOptions options, int width, int height)
private static (Size, Rectangle) CalculateMaxRectangle(
Size source,
ResizeOptions options,
int width,
int height)
{
int destinationWidth = width;
int destinationHeight = height;
@ -320,7 +282,11 @@ namespace SixLabors.ImageSharp.Processing
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight));
}
private static (Size, Rectangle) CalculateMinRectangle(Size source, ResizeOptions options, int width, int height)
private static (Size, Rectangle) CalculateMinRectangle(
Size source,
ResizeOptions options,
int width,
int height)
{
int sourceWidth = source.Width;
int sourceHeight = source.Height;
@ -372,5 +338,78 @@ namespace SixLabors.ImageSharp.Processing
// Replace the size to match the rectangle.
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight));
}
private static (Size, Rectangle) CalculatePadRectangle(
Size source,
ResizeOptions options,
int width,
int height)
{
if (width <= 0 || height <= 0)
{
return (new Size(source.Width, source.Height), new Rectangle(0, 0, source.Width, source.Height));
}
float ratio;
int sourceWidth = source.Width;
int sourceHeight = source.Height;
int destinationX = 0;
int destinationY = 0;
int destinationWidth = width;
int destinationHeight = height;
// Fractional variants for preserving aspect ratio.
float percentHeight = MathF.Abs(height / (float)sourceHeight);
float percentWidth = MathF.Abs(width / (float)sourceWidth);
if (percentHeight < percentWidth)
{
ratio = percentHeight;
destinationWidth = (int)MathF.Round(sourceWidth * percentHeight);
switch (options.Position)
{
case AnchorPositionMode.Left:
case AnchorPositionMode.TopLeft:
case AnchorPositionMode.BottomLeft:
destinationX = 0;
break;
case AnchorPositionMode.Right:
case AnchorPositionMode.TopRight:
case AnchorPositionMode.BottomRight:
destinationX = (int)MathF.Round(width - (sourceWidth * ratio));
break;
default:
destinationX = (int)MathF.Round((width - (sourceWidth * ratio)) / 2F);
break;
}
}
else
{
ratio = percentWidth;
destinationHeight = (int)MathF.Round(sourceHeight * percentWidth);
switch (options.Position)
{
case AnchorPositionMode.Top:
case AnchorPositionMode.TopLeft:
case AnchorPositionMode.TopRight:
destinationY = 0;
break;
case AnchorPositionMode.Bottom:
case AnchorPositionMode.BottomLeft:
case AnchorPositionMode.BottomRight:
destinationY = (int)MathF.Round(height - (sourceHeight * ratio));
break;
default:
destinationY = (int)MathF.Round((height - (sourceHeight * ratio)) / 2F);
break;
}
}
return (new Size(width, height),
new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight));
}
}
}

28
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -19,27 +19,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Initializes a new instance of the <see cref="ResizeKernel"/> struct.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel(int left, float* bufferPtr, int length)
internal ResizeKernel(int startIndex, float* bufferPtr, int length)
{
this.Left = left;
this.StartIndex = startIndex;
this.bufferPtr = bufferPtr;
this.Length = length;
}
/// <summary>
/// Gets the left index for the destination row
/// Gets the start index for the destination row.
/// </summary>
public int Left { get; }
public int StartIndex { get; }
/// <summary>
/// Gets the the length of the kernel
/// Gets the the length of the kernel.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the span representing the portion of the <see cref="ResizeKernelMap"/> that this window covers
/// Gets the span representing the portion of the <see cref="ResizeKernelMap"/> that this window covers.
/// </summary>
/// <value>The <see cref="Span{T}"/>
/// <value>The <see cref="Span{T}"/>.
/// </value>
public Span<float> Values
{
@ -54,10 +54,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>The weighted sum</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Vector4 Convolve(Span<Vector4> rowSpan)
{
return this.ConvolveCore(ref rowSpan[this.StartIndex]);
}
[MethodImpl(InliningOptions.ShortMethod)]
public Vector4 ConvolveCore(ref Vector4 rowStartRef)
{
ref float horizontalValues = ref Unsafe.AsRef<float>(this.bufferPtr);
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left);
// Destination color components
Vector4 result = Vector4.Zero;
@ -65,7 +69,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int i = 0; i < this.Length; i++)
{
float weight = Unsafe.Add(ref horizontalValues, i);
Vector4 v = Unsafe.Add(ref vecPtr, i);
// Vector4 v = offsetedRowSpan[i];
Vector4 v = Unsafe.Add(ref rowStartRef, i);
result += v * weight;
}
@ -73,7 +79,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
/// <summary>
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="Left"/>
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="StartIndex"/>
/// to the value <paramref name="left"/>.
/// </summary>
internal ResizeKernel AlterLeftValue(int left)

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

@ -54,11 +54,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.radius = radius;
this.sourceLength = sourceLength;
this.DestinationLength = destinationLength;
int maxWidth = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean);
this.MaxDiameter = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[maxWidth];
this.tempValues = new double[this.MaxDiameter];
}
/// <summary>
@ -66,6 +66,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary>
public int DestinationLength { get; }
/// <summary>
/// Gets the maximum diameter of the kernels.
/// </summary>
public int MaxDiameter { get; }
/// <summary>
/// Gets a string of information to help debugging
/// </summary>

129
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
@ -65,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.Sampler = options.Sampler;
this.Width = size.Width;
this.Height = size.Height;
this.ResizeRectangle = rectangle;
this.TargetRectangle = rectangle;
this.Compand = options.Compand;
}
@ -88,11 +89,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="width">The target width.</param>
/// <param name="height">The target height.</param>
/// <param name="sourceSize">The source image size</param>
/// <param name="resizeRectangle">
/// <param name="targetRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the target image object to draw to.
/// </param>
/// <param name="compand">Whether to compress or expand individual pixel color values on processing.</param>
public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle resizeRectangle, bool compand)
public ResizeProcessor(IResampler sampler, int width, int height, Size sourceSize, Rectangle targetRectangle, bool compand)
{
Guard.NotNull(sampler, nameof(sampler));
@ -103,13 +104,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (width == 0 && height > 0)
{
width = (int)MathF.Max(min, MathF.Round(sourceSize.Width * height / (float)sourceSize.Height));
resizeRectangle.Width = width;
targetRectangle.Width = width;
}
if (height == 0 && width > 0)
{
height = (int)MathF.Max(min, MathF.Round(sourceSize.Height * width / (float)sourceSize.Width));
resizeRectangle.Height = height;
targetRectangle.Height = height;
}
Guard.MustBeGreaterThan(width, 0, nameof(width));
@ -118,7 +119,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.Sampler = sampler;
this.Width = width;
this.Height = height;
this.ResizeRectangle = resizeRectangle;
this.TargetRectangle = targetRectangle;
this.Compand = compand;
}
@ -140,7 +141,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Gets the resize rectangle.
/// </summary>
public Rectangle ResizeRectangle { get; }
public Rectangle TargetRectangle { get; }
/// <summary>
/// Gets a value indicating whether to compress or expand individual pixel color values on processing.
@ -166,13 +167,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
MemoryAllocator memoryAllocator = source.GetMemoryAllocator();
this.horizontalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Width,
this.TargetRectangle.Width,
sourceRectangle.Width,
memoryAllocator);
this.verticalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Height,
this.TargetRectangle.Height,
sourceRectangle.Height,
memoryAllocator);
}
@ -182,7 +183,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{
// Handle resize dimensions identical to the original
if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.ResizeRectangle)
if (source.Width == destination.Width && source.Height == destination.Height && sourceRectangle == this.TargetRectangle)
{
// The cloned will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
@ -193,26 +194,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = this.Height;
int sourceX = sourceRectangle.X;
int sourceY = sourceRectangle.Y;
int startY = this.ResizeRectangle.Y;
int endY = this.ResizeRectangle.Bottom;
int startX = this.ResizeRectangle.X;
int endX = this.ResizeRectangle.Right;
int startY = this.TargetRectangle.Y;
int startX = this.TargetRectangle.X;
int minX = Math.Max(0, startX);
int maxX = Math.Min(width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(height, endY);
var targetWorkingRect = Rectangle.Intersect(
this.TargetRectangle,
new Rectangle(0, 0, width, height));
if (this.Sampler is NearestNeighborResampler)
{
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height;
float widthFactor = sourceRectangle.Width / (float)this.TargetRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.TargetRectangle.Height;
ParallelHelper.IterateRows(
workingRect,
targetWorkingRect,
configuration,
rows =>
{
@ -223,7 +219,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY));
Span<TPixel> targetRow = destination.GetPixelRowSpan(y);
for (int x = minX; x < maxX; x++)
for (int x = targetWorkingRect.Left; x < targetWorkingRect.Right; x++)
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
@ -236,74 +232,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int sourceHeight = source.Height;
PixelConversionModifiers conversionModifiers = PixelConversionModifiers.Premultiply;
if (this.Compand)
{
conversionModifiers |= PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand;
}
// Interpolate the image using the calculated weights.
// A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm
// First process the columns. Since we are not using multiple threads startY and endY
// are the upper and lower bounds of the source rectangle.
using (Buffer2D<Vector4> firstPassPixelsTransposed = source.MemoryAllocator.Allocate2D<Vector4>(sourceHeight, width))
PixelConversionModifiers conversionModifiers =
PixelConversionModifiers.Premultiply.ApplyCompanding(this.Compand);
BufferArea<TPixel> sourceArea = source.PixelBuffer.GetArea(sourceRectangle);
// To reintroduce parallel processing, we to launch multiple workers
// for different row intervals of the image.
using (var worker = new ResizeWorker<TPixel>(
configuration,
sourceArea,
conversionModifiers,
this.horizontalKernelMap,
this.verticalKernelMap,
width,
targetWorkingRect,
this.TargetRectangle.Location))
{
firstPassPixelsTransposed.MemorySource.Clear();
var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom);
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
processColsRect,
configuration,
(rows, tempRowBuffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(sourceX);
Span<Vector4> tempRowSpan = tempRowBuffer.Span.Slice(sourceX);
PixelOperations<TPixel>.Instance.ToVector4(configuration, sourceRow, tempRowSpan, conversionModifiers);
ref Vector4 firstPassBaseRef = ref firstPassPixelsTransposed.Span[y];
for (int x = minX; x < maxX; x++)
{
ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX);
Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = kernel.Convolve(tempRowSpan);
}
}
});
var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY);
// Now process the rows.
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
processRowsRect,
configuration,
(rows, tempRowBuffer) =>
{
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
// Ensure offsets are normalized for cropping and padding.
ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY);
worker.Initialize();
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan);
for (int x = 0; x < width; x++)
{
Span<Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY);
// Destination color components
Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn);
}
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, tempRowSpan, targetRowSpan, conversionModifiers);
}
});
var workingInterval = new RowInterval(targetWorkingRect.Top, targetWorkingRect.Bottom);
worker.FillDestinationPixels(workingInterval, destination.PixelBuffer);
}
}

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

@ -0,0 +1,193 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Implements the resize algorithm using a sliding window of size
/// maximized by <see cref="Configuration.WorkingBufferSizeHintInBytes"/>.
/// The height of the window is a multiple of the vertical kernel's maximum diameter.
/// When sliding the window, the contents of the bottom window band are copied to the new top band.
/// For more details, and visual explanation, see "ResizeWorker.pptx".
/// </summary>
internal class ResizeWorker<TPixel> : IDisposable
where TPixel : struct, IPixel<TPixel>
{
private readonly Buffer2D<Vector4> transposedFirstPassBuffer;
private readonly Configuration configuration;
private readonly PixelConversionModifiers conversionModifiers;
private readonly ResizeKernelMap horizontalKernelMap;
private readonly BufferArea<TPixel> source;
private readonly Rectangle sourceRectangle;
private readonly IMemoryOwner<Vector4> tempRowBuffer;
private readonly IMemoryOwner<Vector4> tempColumnBuffer;
private readonly ResizeKernelMap verticalKernelMap;
private readonly int destWidth;
private readonly Rectangle targetWorkingRect;
private readonly Point targetOrigin;
private readonly int windowBandHeight;
private readonly int workerHeight;
private RowInterval currentWindow;
public ResizeWorker(
Configuration configuration,
BufferArea<TPixel> source,
PixelConversionModifiers conversionModifiers,
ResizeKernelMap horizontalKernelMap,
ResizeKernelMap verticalKernelMap,
int destWidth,
Rectangle targetWorkingRect,
Point targetOrigin)
{
this.configuration = configuration;
this.source = source;
this.sourceRectangle = source.Rectangle;
this.conversionModifiers = conversionModifiers;
this.horizontalKernelMap = horizontalKernelMap;
this.verticalKernelMap = verticalKernelMap;
this.destWidth = destWidth;
this.targetWorkingRect = targetWorkingRect;
this.targetOrigin = targetOrigin;
this.windowBandHeight = verticalKernelMap.MaxDiameter;
int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands(
this.windowBandHeight,
destWidth,
configuration.WorkingBufferSizeHintInBytes);
this.workerHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandHeight);
this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D<Vector4>(
this.workerHeight,
destWidth,
AllocationOptions.Clean);
this.tempRowBuffer = configuration.MemoryAllocator.Allocate<Vector4>(this.sourceRectangle.Width);
this.tempColumnBuffer = configuration.MemoryAllocator.Allocate<Vector4>(destWidth);
this.currentWindow = new RowInterval(0, this.workerHeight);
}
public void Dispose()
{
this.transposedFirstPassBuffer.Dispose();
this.tempRowBuffer.Dispose();
this.tempColumnBuffer.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<Vector4> GetColumnSpan(int x, int startY)
{
return this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min);
}
public void Initialize()
{
this.CalculateFirstPassValues(this.currentWindow);
}
public void FillDestinationPixels(RowInterval rowInterval, Buffer2D<TPixel> destination)
{
Span<Vector4> tempColSpan = this.tempColumnBuffer.GetSpan();
for (int y = rowInterval.Min; y < rowInterval.Max; y++)
{
// Ensure offsets are normalized for cropping and padding.
ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - this.targetOrigin.Y);
if (kernel.StartIndex + kernel.Length > this.currentWindow.Max)
{
this.Slide();
}
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan);
int top = kernel.StartIndex - this.currentWindow.Min;
ref Vector4 fpBase = ref this.transposedFirstPassBuffer.Span[top];
for (int x = 0; x < this.destWidth; x++)
{
ref Vector4 firstPassColumnBase = ref Unsafe.Add(ref fpBase, x * this.workerHeight);
// Destination color components
Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase);
}
Span<TPixel> targetRowSpan = destination.GetRowSpan(y);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers);
}
}
private void Slide()
{
int minY = this.currentWindow.Max - this.windowBandHeight;
int maxY = Math.Min(minY + this.workerHeight, this.sourceRectangle.Height);
// Copy previous bottom band to the new top:
// (rows <--> columns, because the buffer is transposed)
this.transposedFirstPassBuffer.CopyColumns(
this.workerHeight - this.windowBandHeight,
0,
this.windowBandHeight);
this.currentWindow = new RowInterval(minY, maxY);
// Calculate the remainder:
this.CalculateFirstPassValues(this.currentWindow.Slice(this.windowBandHeight));
}
private void CalculateFirstPassValues(RowInterval calculationInterval)
{
Span<Vector4> tempRowSpan = this.tempRowBuffer.GetSpan();
for (int y = calculationInterval.Min; y < calculationInterval.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
sourceRow,
tempRowSpan,
this.conversionModifiers);
// Span<Vector4> firstPassSpan = this.transposedFirstPassBuffer.Span.Slice(y - this.currentWindow.Min);
ref Vector4 firstPassBaseRef = ref this.transposedFirstPassBuffer.Span[y - this.currentWindow.Min];
for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++)
{
ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.targetOrigin.X);
// firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan);
Unsafe.Add(ref firstPassBaseRef, x * this.workerHeight) = kernel.Convolve(tempRowSpan);
}
}
}
}
}

BIN
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx

Binary file not shown.

102
tests/ImageSharp.Benchmarks/General/ArrayCopy.cs

@ -1,102 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
namespace SixLabors.ImageSharp.Benchmarks.General
{
[Config(typeof(Config.ShortClr))]
public class ArrayCopy
{
[Params(10, 100, 1000, 10000)]
public int Count { get; set; }
byte[] source;
byte[] destination;
[GlobalSetup]
public void SetUp()
{
this.source = new byte[this.Count];
this.destination = new byte[this.Count];
}
[Benchmark(Baseline = true, Description = "Copy using Array.Copy()")]
public void CopyArray()
{
Array.Copy(this.source, this.destination, this.Count);
}
[Benchmark(Description = "Copy using Unsafe<T>")]
public unsafe void CopyUnsafe()
{
fixed (byte* pinnedDestination = this.destination)
fixed (byte* pinnedSource = this.source)
{
Unsafe.CopyBlock(pinnedSource, pinnedDestination, (uint)this.Count);
}
}
[Benchmark(Description = "Copy using Buffer.BlockCopy()")]
public void CopyUsingBufferBlockCopy()
{
Buffer.BlockCopy(this.source, 0, this.destination, 0, this.Count);
}
[Benchmark(Description = "Copy using Buffer.MemoryCopy<T>")]
public unsafe void CopyUsingBufferMemoryCopy()
{
fixed (byte* pinnedDestination = this.destination)
fixed (byte* pinnedSource = this.source)
{
Buffer.MemoryCopy(pinnedSource, pinnedDestination, this.Count, this.Count);
}
}
[Benchmark(Description = "Copy using Marshal.Copy<T>")]
public unsafe void CopyUsingMarshalCopy()
{
fixed (byte* pinnedDestination = this.destination)
{
Marshal.Copy(this.source, 0, (IntPtr)pinnedDestination, this.Count);
}
}
/*****************************************************************************************************************
*************** RESULTS on i7-4810MQ 2.80GHz + Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1085.0 ********************
*****************************************************************************************************************
*
* Method | Count | Mean | StdErr | StdDev | Scaled | Scaled-StdDev |
* ---------------------------------- |------ |------------ |----------- |----------- |------- |-------------- |
* 'Copy using Array.Copy()' | 10 | 20.3074 ns | 0.1194 ns | 0.2068 ns | 1.00 | 0.00 |
* 'Copy using Unsafe<T>' | 10 | 6.1002 ns | 0.1981 ns | 0.3432 ns | 0.30 | 0.01 |
* 'Copy using Buffer.BlockCopy()' | 10 | 10.7879 ns | 0.0984 ns | 0.1705 ns | 0.53 | 0.01 |
* 'Copy using Buffer.MemoryCopy<T>' | 10 | 4.9625 ns | 0.0200 ns | 0.0347 ns | 0.24 | 0.00 |
* 'Copy using Marshal.Copy<T>' | 10 | 16.1782 ns | 0.0919 ns | 0.1592 ns | 0.80 | 0.01 |
*
* 'Copy using Array.Copy()' | 100 | 31.5945 ns | 0.2908 ns | 0.5037 ns | 1.00 | 0.00 |
* 'Copy using Unsafe<T>' | 100 | 10.2722 ns | 0.5202 ns | 0.9010 ns | 0.33 | 0.02 |
* 'Copy using Buffer.BlockCopy()' | 100 | 22.0322 ns | 0.0284 ns | 0.0493 ns | 0.70 | 0.01 |
* 'Copy using Buffer.MemoryCopy<T>' | 100 | 10.2472 ns | 0.0359 ns | 0.0622 ns | 0.32 | 0.00 |
* 'Copy using Marshal.Copy<T>' | 100 | 34.3820 ns | 1.1868 ns | 2.0555 ns | 1.09 | 0.05 |
*
* 'Copy using Array.Copy()' | 1000 | 40.9743 ns | 0.0521 ns | 0.0902 ns | 1.00 | 0.00 |
* 'Copy using Unsafe<T>' | 1000 | 42.7840 ns | 2.0139 ns | 3.4882 ns | 1.04 | 0.07 |
* 'Copy using Buffer.BlockCopy()' | 1000 | 33.7361 ns | 0.0751 ns | 0.1300 ns | 0.82 | 0.00 |
* 'Copy using Buffer.MemoryCopy<T>' | 1000 | 35.7541 ns | 0.0480 ns | 0.0832 ns | 0.87 | 0.00 |
* 'Copy using Marshal.Copy<T>' | 1000 | 42.2028 ns | 0.2769 ns | 0.4795 ns | 1.03 | 0.01 |
*
* 'Copy using Array.Copy()' | 10000 | 200.0438 ns | 0.2251 ns | 0.3899 ns | 1.00 | 0.00 |
* 'Copy using Unsafe<T>' | 10000 | 389.6957 ns | 13.2770 ns | 22.9964 ns | 1.95 | 0.09 |
* 'Copy using Buffer.BlockCopy()' | 10000 | 191.3478 ns | 0.1557 ns | 0.2697 ns | 0.96 | 0.00 |
* 'Copy using Buffer.MemoryCopy<T>' | 10000 | 196.4679 ns | 0.2731 ns | 0.4730 ns | 0.98 | 0.00 |
* 'Copy using Marshal.Copy<T>' | 10000 | 202.5392 ns | 0.5561 ns | 0.9631 ns | 1.01 | 0.00 |
*
*/
}
}

231
tests/ImageSharp.Benchmarks/General/CopyBuffers.cs

@ -0,0 +1,231 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
namespace SixLabors.ImageSharp.Benchmarks.General
{
/// <summary>
/// Compare different methods for copying native and/or managed buffers.
/// Conclusions:
/// - Span.CopyTo() has terrible performance on classic .NET Framework
/// - Buffer.MemoryCopy() performance is good enough for all sizes (but needs pinning)
/// </summary>
[Config(typeof(Config.ShortClr))]
public class CopyBuffers
{
private byte[] destArray;
private MemoryHandle destHandle;
private Memory<byte> destMemory;
private byte[] sourceArray;
private MemoryHandle sourceHandle;
private Memory<byte> sourceMemory;
[Params(10, 50, 100, 1000, 10000)]
public int Count { get; set; }
[GlobalSetup]
public void Setup()
{
this.sourceArray = new byte[this.Count];
this.sourceMemory = new Memory<byte>(this.sourceArray);
this.sourceHandle = this.sourceMemory.Pin();
this.destArray = new byte[this.Count];
this.destMemory = new Memory<byte>(this.destArray);
this.destHandle = this.destMemory.Pin();
}
[GlobalCleanup]
public void Cleanup()
{
this.sourceHandle.Dispose();
this.destHandle.Dispose();
}
[Benchmark(Baseline = true, Description = "Array.Copy()")]
public void ArrayCopy()
{
Array.Copy(this.sourceArray, this.destArray, this.Count);
}
[Benchmark(Description = "Buffer.BlockCopy()")]
public void BufferBlockCopy()
{
Buffer.BlockCopy(this.sourceArray, 0, this.destArray, 0, this.Count);
}
[Benchmark(Description = "Buffer.MemoryCopy()")]
public unsafe void BufferMemoryCopy()
{
void* pinnedDestination = this.destHandle.Pointer;
void* pinnedSource = this.sourceHandle.Pointer;
Buffer.MemoryCopy(pinnedSource, pinnedDestination, this.Count, this.Count);
}
[Benchmark(Description = "Marshal.Copy()")]
public unsafe void MarshalCopy()
{
void* pinnedDestination = this.destHandle.Pointer;
Marshal.Copy(this.sourceArray, 0, (IntPtr)pinnedDestination, this.Count);
}
[Benchmark(Description = "Span.CopyTo()")]
public void SpanCopyTo()
{
this.sourceMemory.Span.CopyTo(this.destMemory.Span);
}
[Benchmark(Description = "Unsafe.CopyBlock(ref)")]
public unsafe void UnsafeCopyBlockReferences()
{
Unsafe.CopyBlock(ref this.destArray[0], ref this.sourceArray[0], (uint)this.Count);
}
[Benchmark(Description = "Unsafe.CopyBlock(ptr)")]
public unsafe void UnsafeCopyBlockPointers()
{
void* pinnedDestination = this.destHandle.Pointer;
void* pinnedSource = this.sourceHandle.Pointer;
Unsafe.CopyBlock(pinnedDestination, pinnedSource, (uint)this.Count);
}
[Benchmark(Description = "Unsafe.CopyBlockUnaligned(ref)")]
public unsafe void UnsafeCopyBlockUnalignedReferences()
{
Unsafe.CopyBlockUnaligned(ref this.destArray[0], ref this.sourceArray[0], (uint)this.Count);
}
[Benchmark(Description = "Unsafe.CopyBlockUnaligned(ptr)")]
public unsafe void UnsafeCopyBlockUnalignedPointers()
{
void* pinnedDestination = this.destHandle.Pointer;
void* pinnedSource = this.sourceHandle.Pointer;
Unsafe.CopyBlockUnaligned(pinnedDestination, pinnedSource, (uint)this.Count);
}
// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
// Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
// Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC
// .NET Core SDK=2.2.202
// [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
// Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0
// Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Job | Runtime | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------------------------------- |----- |-------- |------ |-----------:|-----------:|----------:|------:|--------:|------:|------:|------:|----------:|
// | Array.Copy() | Clr | Clr | 10 | 23.636 ns | 2.5299 ns | 0.1387 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Clr | Clr | 10 | 11.420 ns | 2.3341 ns | 0.1279 ns | 0.48 | 0.01 | - | - | - | - |
// | Buffer.MemoryCopy() | Clr | Clr | 10 | 2.861 ns | 0.5059 ns | 0.0277 ns | 0.12 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Clr | Clr | 10 | 14.870 ns | 2.4541 ns | 0.1345 ns | 0.63 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Clr | Clr | 10 | 31.906 ns | 1.2213 ns | 0.0669 ns | 1.35 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Clr | Clr | 10 | 3.513 ns | 0.7392 ns | 0.0405 ns | 0.15 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Clr | Clr | 10 | 3.053 ns | 0.2010 ns | 0.0110 ns | 0.13 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 10 | 3.497 ns | 0.4911 ns | 0.0269 ns | 0.15 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 10 | 3.109 ns | 0.5958 ns | 0.0327 ns | 0.13 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Core | Core | 10 | 19.709 ns | 2.1867 ns | 0.1199 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Core | Core | 10 | 7.377 ns | 1.1582 ns | 0.0635 ns | 0.37 | 0.01 | - | - | - | - |
// | Buffer.MemoryCopy() | Core | Core | 10 | 2.581 ns | 1.1607 ns | 0.0636 ns | 0.13 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Core | Core | 10 | 15.197 ns | 2.8446 ns | 0.1559 ns | 0.77 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Core | Core | 10 | 25.394 ns | 0.9782 ns | 0.0536 ns | 1.29 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Core | Core | 10 | 2.254 ns | 0.1590 ns | 0.0087 ns | 0.11 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Core | Core | 10 | 1.878 ns | 0.1035 ns | 0.0057 ns | 0.10 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 10 | 2.263 ns | 0.1383 ns | 0.0076 ns | 0.11 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 10 | 1.877 ns | 0.0602 ns | 0.0033 ns | 0.10 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Clr | Clr | 50 | 35.068 ns | 5.9137 ns | 0.3242 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Clr | Clr | 50 | 23.299 ns | 2.3797 ns | 0.1304 ns | 0.66 | 0.01 | - | - | - | - |
// | Buffer.MemoryCopy() | Clr | Clr | 50 | 3.598 ns | 4.8536 ns | 0.2660 ns | 0.10 | 0.01 | - | - | - | - |
// | Marshal.Copy() | Clr | Clr | 50 | 27.720 ns | 4.6602 ns | 0.2554 ns | 0.79 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Clr | Clr | 50 | 35.673 ns | 16.2972 ns | 0.8933 ns | 1.02 | 0.03 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Clr | Clr | 50 | 5.534 ns | 2.8119 ns | 0.1541 ns | 0.16 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Clr | Clr | 50 | 4.511 ns | 0.9555 ns | 0.0524 ns | 0.13 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 50 | 5.613 ns | 1.6679 ns | 0.0914 ns | 0.16 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 50 | 4.884 ns | 7.3153 ns | 0.4010 ns | 0.14 | 0.01 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Core | Core | 50 | 20.232 ns | 1.5720 ns | 0.0862 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Core | Core | 50 | 8.142 ns | 0.7860 ns | 0.0431 ns | 0.40 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Core | Core | 50 | 2.962 ns | 0.0611 ns | 0.0033 ns | 0.15 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Core | Core | 50 | 16.802 ns | 2.9686 ns | 0.1627 ns | 0.83 | 0.00 | - | - | - | - |
// | Span.CopyTo() | Core | Core | 50 | 26.571 ns | 0.9228 ns | 0.0506 ns | 1.31 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Core | Core | 50 | 2.219 ns | 0.7191 ns | 0.0394 ns | 0.11 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Core | Core | 50 | 1.751 ns | 0.1884 ns | 0.0103 ns | 0.09 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 50 | 2.177 ns | 0.4489 ns | 0.0246 ns | 0.11 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 50 | 1.806 ns | 0.1063 ns | 0.0058 ns | 0.09 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Clr | Clr | 100 | 39.158 ns | 4.3068 ns | 0.2361 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Clr | Clr | 100 | 27.623 ns | 0.4611 ns | 0.0253 ns | 0.71 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Clr | Clr | 100 | 5.018 ns | 0.5689 ns | 0.0312 ns | 0.13 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Clr | Clr | 100 | 33.527 ns | 1.9019 ns | 0.1042 ns | 0.86 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Clr | Clr | 100 | 35.604 ns | 2.7039 ns | 0.1482 ns | 0.91 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Clr | Clr | 100 | 7.853 ns | 0.4925 ns | 0.0270 ns | 0.20 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Clr | Clr | 100 | 7.406 ns | 1.9733 ns | 0.1082 ns | 0.19 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 100 | 7.822 ns | 0.6837 ns | 0.0375 ns | 0.20 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 100 | 7.392 ns | 1.2832 ns | 0.0703 ns | 0.19 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Core | Core | 100 | 22.909 ns | 2.9754 ns | 0.1631 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Core | Core | 100 | 10.687 ns | 1.1262 ns | 0.0617 ns | 0.47 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Core | Core | 100 | 4.063 ns | 0.1607 ns | 0.0088 ns | 0.18 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Core | Core | 100 | 18.067 ns | 4.0557 ns | 0.2223 ns | 0.79 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Core | Core | 100 | 28.352 ns | 1.2762 ns | 0.0700 ns | 1.24 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Core | Core | 100 | 4.130 ns | 0.2013 ns | 0.0110 ns | 0.18 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Core | Core | 100 | 4.096 ns | 0.2460 ns | 0.0135 ns | 0.18 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 100 | 4.160 ns | 0.3174 ns | 0.0174 ns | 0.18 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 100 | 3.480 ns | 1.1683 ns | 0.0640 ns | 0.15 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Clr | Clr | 1000 | 49.059 ns | 2.0729 ns | 0.1136 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Clr | Clr | 1000 | 38.270 ns | 23.6970 ns | 1.2989 ns | 0.78 | 0.03 | - | - | - | - |
// | Buffer.MemoryCopy() | Clr | Clr | 1000 | 27.599 ns | 6.8328 ns | 0.3745 ns | 0.56 | 0.01 | - | - | - | - |
// | Marshal.Copy() | Clr | Clr | 1000 | 42.752 ns | 5.1357 ns | 0.2815 ns | 0.87 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Clr | Clr | 1000 | 69.983 ns | 2.1860 ns | 0.1198 ns | 1.43 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Clr | Clr | 1000 | 44.822 ns | 0.1625 ns | 0.0089 ns | 0.91 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Clr | Clr | 1000 | 45.072 ns | 1.4053 ns | 0.0770 ns | 0.92 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 1000 | 45.306 ns | 5.2646 ns | 0.2886 ns | 0.92 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 1000 | 44.813 ns | 0.9117 ns | 0.0500 ns | 0.91 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Core | Core | 1000 | 51.907 ns | 3.1827 ns | 0.1745 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Core | Core | 1000 | 40.700 ns | 3.1488 ns | 0.1726 ns | 0.78 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Core | Core | 1000 | 23.711 ns | 1.3004 ns | 0.0713 ns | 0.46 | 0.00 | - | - | - | - |
// | Marshal.Copy() | Core | Core | 1000 | 42.586 ns | 2.5390 ns | 0.1392 ns | 0.82 | 0.00 | - | - | - | - |
// | Span.CopyTo() | Core | Core | 1000 | 44.109 ns | 4.5604 ns | 0.2500 ns | 0.85 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Core | Core | 1000 | 33.926 ns | 5.1633 ns | 0.2830 ns | 0.65 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Core | Core | 1000 | 33.267 ns | 0.2708 ns | 0.0148 ns | 0.64 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 1000 | 34.018 ns | 2.3238 ns | 0.1274 ns | 0.66 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 1000 | 33.667 ns | 2.1983 ns | 0.1205 ns | 0.65 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Clr | Clr | 10000 | 153.429 ns | 6.1735 ns | 0.3384 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Clr | Clr | 10000 | 201.373 ns | 4.3670 ns | 0.2394 ns | 1.31 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Clr | Clr | 10000 | 211.768 ns | 71.3510 ns | 3.9110 ns | 1.38 | 0.02 | - | - | - | - |
// | Marshal.Copy() | Clr | Clr | 10000 | 215.299 ns | 17.2677 ns | 0.9465 ns | 1.40 | 0.01 | - | - | - | - |
// | Span.CopyTo() | Clr | Clr | 10000 | 486.325 ns | 32.4445 ns | 1.7784 ns | 3.17 | 0.01 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Clr | Clr | 10000 | 452.314 ns | 33.0593 ns | 1.8121 ns | 2.95 | 0.02 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Clr | Clr | 10000 | 455.600 ns | 56.7534 ns | 3.1108 ns | 2.97 | 0.02 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Clr | Clr | 10000 | 452.279 ns | 8.6457 ns | 0.4739 ns | 2.95 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Clr | Clr | 10000 | 453.146 ns | 12.3776 ns | 0.6785 ns | 2.95 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | |
// | Array.Copy() | Core | Core | 10000 | 204.508 ns | 3.1652 ns | 0.1735 ns | 1.00 | 0.00 | - | - | - | - |
// | Buffer.BlockCopy() | Core | Core | 10000 | 193.345 ns | 1.3742 ns | 0.0753 ns | 0.95 | 0.00 | - | - | - | - |
// | Buffer.MemoryCopy() | Core | Core | 10000 | 196.978 ns | 18.3279 ns | 1.0046 ns | 0.96 | 0.01 | - | - | - | - |
// | Marshal.Copy() | Core | Core | 10000 | 206.878 ns | 6.9938 ns | 0.3834 ns | 1.01 | 0.00 | - | - | - | - |
// | Span.CopyTo() | Core | Core | 10000 | 215.733 ns | 15.4824 ns | 0.8486 ns | 1.05 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ref) | Core | Core | 10000 | 186.894 ns | 8.7617 ns | 0.4803 ns | 0.91 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlock(ptr) | Core | Core | 10000 | 186.662 ns | 10.6059 ns | 0.5813 ns | 0.91 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ref) | Core | Core | 10000 | 187.489 ns | 13.1527 ns | 0.7209 ns | 0.92 | 0.00 | - | - | - | - |
// | Unsafe.CopyBlockUnaligned(ptr) | Core | Core | 10000 | 186.586 ns | 4.6274 ns | 0.2536 ns | 0.91 | 0.00 | - | - | - | - |
}
}

61
tests/ImageSharp.Benchmarks/Samplers/Resize.cs

@ -3,6 +3,7 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using BenchmarkDotNet.Attributes;
@ -22,15 +23,21 @@ namespace SixLabors.ImageSharp.Benchmarks
private Bitmap sourceBitmap;
[Params(3032)]
public int SourceSize { get; set; }
[Params("3032-400")]
public virtual string SourceToDest { get; set; }
protected int SourceSize { get; private set; }
protected int DestSize { get; private set; }
[Params(400)]
public int DestSize { get; set; }
[GlobalSetup]
public void Setup()
public virtual void Setup()
{
string[] stuff = this.SourceToDest.Split('-');
this.SourceSize = int.Parse(stuff[0], CultureInfo.InvariantCulture);
this.DestSize = int.Parse(stuff[1], CultureInfo.InvariantCulture);
this.sourceImage = new Image<TPixel>(this.Configuration, this.SourceSize, this.SourceSize);
this.sourceBitmap = new Bitmap(this.SourceSize, this.SourceSize);
}
@ -94,26 +101,44 @@ namespace SixLabors.ImageSharp.Benchmarks
ctx.Resize(this.DestSize, this.DestSize, KnownResamplers.Bicubic);
}
// RESULTS (2019 April):
// RESULTS - 2019 April - ResizeWorker:
//
// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.648 (1803/April2018Update/Redstone4)
// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
// Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
// Frequency=2742192 Hz, Resolution=364.6718 ns, Timer=TSC
// .NET Core SDK=2.1.602
// Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC
// .NET Core SDK=2.2.202
// [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
// Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0
// Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0
// Core : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// Method | Job | Runtime | SourceSize | DestSize | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
// ----------------------------------------- |----- |-------- |----------- |--------- |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:|
// SystemDrawing | Clr | Clr | 3032 | 400 | 118.71 ms | 4.884 ms | 0.2677 ms | 1.00 | - | - | - | 2048 B |
// 'ImageSharp, MaxDegreeOfParallelism = 1' | Clr | Clr | 3032 | 400 | 94.55 ms | 16.160 ms | 0.8858 ms | 0.80 | - | - | - | 16384 B |
// | | | | | | | | | | | | |
// SystemDrawing | Core | Core | 3032 | 400 | 118.38 ms | 2.814 ms | 0.1542 ms | 1.00 | - | - | - | 96 B |
// 'ImageSharp, MaxDegreeOfParallelism = 1' | Core | Core | 3032 | 400 | 90.28 ms | 4.679 ms | 0.2565 ms | 0.76 | - | - | - | 15712 B |
// Method | Job | Runtime | SourceToDest | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
// ----------------------------------------- |----- |-------- |------------- |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
// SystemDrawing | Clr | Clr | 3032-400 | 120.11 ms | 1.435 ms | 0.0786 ms | 1.00 | 0.00 | - | - | - | 1638 B |
// 'ImageSharp, MaxDegreeOfParallelism = 1' | Clr | Clr | 3032-400 | 75.32 ms | 34.143 ms | 1.8715 ms | 0.63 | 0.02 | - | - | - | 16384 B |
// | | | | | | | | | | | | |
// SystemDrawing | Core | Core | 3032-400 | 120.33 ms | 6.669 ms | 0.3656 ms | 1.00 | 0.00 | - | - | - | 96 B |
// 'ImageSharp, MaxDegreeOfParallelism = 1' | Core | Core | 3032-400 | 88.56 ms | 1.864 ms | 0.1022 ms | 0.74 | 0.00 | - | - | - | 15568 B |
}
/// <summary>
/// Is it worth to set a larger working buffer limit for resize?
/// Conclusion: It doesn't really have an effect.
/// </summary>
public class Resize_Bicubic_Rgba32_CompareWorkBufferSizes : Resize_Bicubic_Rgba32
{
[Params(128, 512, 1024, 8 * 1024)]
public int WorkingBufferSizeHintInKilobytes { get; set; }
[Params("3032-400", "4000-300")]
public override string SourceToDest { get; set; }
public override void Setup()
{
this.Configuration.WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInKilobytes * 1024;
base.Setup();
}
}
public class Resize_Bicubic_Bgra32 : ResizeBenchmarkBase<Bgra32>

2
tests/ImageSharp.Sandbox46/ImageSharp.Sandbox46.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
<TargetFramework>net472</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>

6
tests/ImageSharp.Sandbox46/Program.cs

@ -33,11 +33,11 @@ namespace SixLabors.ImageSharp.Sandbox46
/// </param>
public static void Main(string[] args)
{
RunJpegColorProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest();
// RunResizeProfilingTest();
RunResizeProfilingTest();
Console.ReadLine();
}
@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Sandbox46
private static void RunResizeProfilingTest()
{
var test = new ResizeProfilingBenchmarks(new ConsoleOutput());
test.ResizeBicubic(2000, 2000);
test.ResizeBicubic(4000, 4000);
}
private static void RunToVector4ProfilingTest()

17
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -39,13 +39,13 @@ namespace SixLabors.ImageSharp.Tests
/// Test that the default configuration is not null.
/// </summary>
[Fact]
public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null);
public void TestDefaultConfigurationIsNotNull() => Assert.True(this.DefaultConfiguration != null);
/// <summary>
/// Test that the default configuration read origin options is set to begin.
/// </summary>
[Fact]
public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current);
public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(this.DefaultConfiguration.ReadOrigin == ReadOrigin.Current);
/// <summary>
/// Test that the default configuration parallel options max degrees of parallelism matches the
@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void TestDefaultConfigurationMaxDegreeOfParallelism()
{
Assert.True(Configuration.Default.MaxDegreeOfParallelism == Environment.ProcessorCount);
Assert.True(this.DefaultConfiguration.MaxDegreeOfParallelism == Environment.ProcessorCount);
var cfg = new Configuration();
Assert.True(cfg.MaxDegreeOfParallelism == Environment.ProcessorCount);
@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests
public void ConfigurationCannotAddDuplicates()
{
const int count = 4;
Configuration config = Configuration.Default;
Configuration config = this.DefaultConfiguration;
Assert.Equal(count, config.ImageFormats.Count());
@ -105,9 +105,16 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void DefaultConfigurationHasCorrectFormatCount()
{
Configuration config = Configuration.Default;
Configuration config = Configuration.CreateDefaultInstance();
Assert.Equal(4, config.ImageFormats.Count());
}
[Fact]
public void WorkingBufferSizeHint_DefaultIsCorrect()
{
Configuration config = this.DefaultConfiguration;
Assert.True(config.WorkingBufferSizeHintInBytes > 1024);
}
}
}

49
tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs

@ -34,5 +34,54 @@ namespace SixLabors.ImageSharp.Tests.Helpers
Assert.True(Unsafe.AreSame(ref expected0, ref actual0));
}
}
[Fact]
public void Slice1()
{
RowInterval rowInterval = new RowInterval(10, 20);
RowInterval sliced = rowInterval.Slice(5);
Assert.Equal(15, sliced.Min);
Assert.Equal(20, sliced.Max);
}
[Fact]
public void Slice2()
{
RowInterval rowInterval = new RowInterval(10, 20);
RowInterval sliced = rowInterval.Slice(3, 5);
Assert.Equal(13, sliced.Min);
Assert.Equal(18, sliced.Max);
}
[Fact]
public void Equality_WhenTrue()
{
RowInterval a = new RowInterval(42, 123);
RowInterval b = new RowInterval(42, 123);
Assert.True(a.Equals(b));
Assert.True(a.Equals((object)b));
Assert.True(a == b);
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void Equality_WhenFalse()
{
RowInterval a = new RowInterval(42, 123);
RowInterval b = new RowInterval(42, 125);
RowInterval c = new RowInterval(40, 123);
Assert.False(a.Equals(b));
Assert.False(c.Equals(a));
Assert.False(b.Equals(c));
Assert.False(a.Equals((object)b));
Assert.False(a.Equals(null));
Assert.False(a == b);
Assert.True(a != c);
}
}
}

51
tests/ImageSharp.Tests/Memory/Buffer2DTests.cs

@ -127,5 +127,56 @@ namespace SixLabors.ImageSharp.Tests.Memory
Assert.Equal(new Size(10, 5), b.Size());
}
}
[Theory]
[InlineData(100, 20, 0, 90, 10)]
[InlineData(100, 3, 0, 50, 50)]
[InlineData(123, 23, 10, 80, 13)]
[InlineData(10, 1, 3, 6, 3)]
[InlineData(2, 2, 0, 1, 1)]
[InlineData(5, 1, 1, 3, 2)]
public void CopyColumns(int width, int height, int startIndex, int destIndex, int columnCount)
{
Random rnd = new Random(123);
using (Buffer2D<float> b = this.MemoryAllocator.Allocate2D<float>(width, height))
{
rnd.RandomFill(b.Span, 0, 1);
b.CopyColumns(startIndex, destIndex, columnCount);
for (int y = 0; y < b.Height; y++)
{
Span<float> row = b.GetRowSpan(y);
Span<float> s = row.Slice(startIndex, columnCount);
Span<float> d = row.Slice(destIndex, columnCount);
Xunit.Assert.True(s.SequenceEqual(d));
}
}
}
[Fact]
public void CopyColumns_InvokeMultipleTimes()
{
Random rnd = new Random(123);
using (Buffer2D<float> b = this.MemoryAllocator.Allocate2D<float>(100, 100))
{
rnd.RandomFill(b.Span, 0, 1);
b.CopyColumns(0, 50, 22);
b.CopyColumns(0, 50, 22);
for (int y = 0; y < b.Height; y++)
{
Span<float> row = b.GetRowSpan(y);
Span<float> s = row.Slice(0, 22);
Span<float> d = row.Slice(50, 22);
Xunit.Assert.True(s.SequenceEqual(d));
}
}
}
}
}

65
tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelConversionModifiersExtensionsTests.cs

@ -0,0 +1,65 @@
// // Copyright (c) Six Labors and contributors.
// // Licensed under the Apache License, Version 2.0.
// // Copyright (c) Six Labors and contributors.
// // Licensed under the Apache License, Version 2.0.
// // Copyright (c) Six Labors and contributors.
// // Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations
{
public class PixelConversionModifiersExtensionsTests
{
[Theory]
[InlineData(PixelConversionModifiers.None, PixelConversionModifiers.None, true)]
[InlineData(PixelConversionModifiers.None, PixelConversionModifiers.Premultiply, false)]
[InlineData(PixelConversionModifiers.SRgbCompand, PixelConversionModifiers.Premultiply, false)]
[InlineData(
PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale,
PixelConversionModifiers.Premultiply,
true)]
[InlineData(
PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale,
PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale,
true)]
[InlineData(
PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale,
PixelConversionModifiers.Scale,
true)]
internal void IsDefined(
PixelConversionModifiers baselineModifiers,
PixelConversionModifiers checkModifiers,
bool expected)
{
Assert.Equal(expected, baselineModifiers.IsDefined(checkModifiers));
}
[Theory]
[InlineData(PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale | PixelConversionModifiers.SRgbCompand,
PixelConversionModifiers.Scale, PixelConversionModifiers.Premultiply | PixelConversionModifiers.SRgbCompand)]
[InlineData(PixelConversionModifiers.None, PixelConversionModifiers.Premultiply, PixelConversionModifiers.None)]
internal void Remove(
PixelConversionModifiers baselineModifiers,
PixelConversionModifiers toRemove,
PixelConversionModifiers expected)
{
PixelConversionModifiers result = baselineModifiers.Remove(toRemove);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(PixelConversionModifiers.Premultiply, false, PixelConversionModifiers.Premultiply)]
[InlineData(PixelConversionModifiers.Premultiply, true, PixelConversionModifiers.Premultiply | PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale)]
internal void ApplyCompanding(
PixelConversionModifiers baselineModifiers,
bool compand,
PixelConversionModifiers expected)
{
PixelConversionModifiers result = baselineModifiers.ApplyCompanding(compand);
Assert.Equal(expected, result);
}
}
}

1
tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs

@ -42,6 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
var bounds = new Rectangle(10, 10, size.Width / 2, size.Height / 2);
ctx.DetectEdges(bounds);
},
comparer: ValidatorComparer,
useReferenceOutputFrom: nameof(this.DetectEdges_InBox));
}

69
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResamplerTests.cs

@ -0,0 +1,69 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class ResamplerTests
{
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void BicubicWindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Bicubic;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void Lanczos3WindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Lanczos3;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-4, 0)]
[InlineData(-2, 0)]
[InlineData(0, 1)]
[InlineData(2, 0)]
[InlineData(4, 0)]
public static void Lanczos5WindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Lanczos5;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void TriangleWindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Triangle;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
}
}

35
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeHelperTests.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class ResizeHelperTests
{
[Theory]
[InlineData(20, 100, 1, 2)]
[InlineData(20, 100, 20*100*16, 2)]
[InlineData(20, 100, 40*100*16, 2)]
[InlineData(20, 100, 59*100*16, 2)]
[InlineData(20, 100, 60*100*16, 3)]
[InlineData(17, 63, 5*17*63*16, 5)]
[InlineData(17, 63, 5*17*63*16+1, 5)]
[InlineData(17, 63, 6*17*63*16-1, 5)]
[InlineData(33, 400, 1*1024*1024, 4)]
[InlineData(33, 400, 8*1024*1024, 39)]
[InlineData(50, 300, 1*1024*1024, 4)]
public void CalculateResizeWorkerHeightInWindowBands(
int windowDiameter,
int width,
int sizeLimitHintInBytes,
int expectedCount)
{
int actualCount = ResizeHelper.CalculateResizeWorkerHeightInWindowBands(windowDiameter, width, sizeLimitHintInBytes);
Assert.Equal(expectedCount, actualCount);
}
}
}

2
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs

@ -104,7 +104,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public static implicit operator ReferenceKernel(ResizeKernel orig)
{
return new ReferenceKernel(orig.Left, orig.Values.ToArray());
return new ReferenceKernel(orig.StartIndex, orig.Values.ToArray());
}
}
}

12
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

@ -36,6 +36,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ nameof(KnownResamplers.Bicubic), 40, 50 },
{ nameof(KnownResamplers.Bicubic), 500, 200 },
{ nameof(KnownResamplers.Bicubic), 200, 500 },
{ nameof(KnownResamplers.Bicubic), 3032, 400 },
{ nameof(KnownResamplers.Bicubic), 10, 25 },
@ -93,7 +94,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
GenerateImageResizeData();
[Theory(Skip = "Only for debugging and development")]
[Theory(
Skip = "Only for debugging and development"
)]
[MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize)
{
@ -130,7 +133,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG
this.Output.WriteLine(kernelMap.Info);
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
#endif
@ -146,8 +152,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
referenceKernel.Length == kernel.Length,
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}");
Assert.True(
referenceKernel.Left == kernel.Left,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}");
referenceKernel.Left == kernel.StartIndex,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}");
float[] expectedValues = referenceKernel.Values;
Span<float> actualValues = kernel.Values;

480
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -2,133 +2,190 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Memory;
using SixLabors.Primitives;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class ResizeTests : FileTestBase
public class ResizeTests
{
public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial };
private const PixelTypes CommonNonDefaultPixelTypes =
PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector;
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
private const PixelTypes DefaultPixelType = PixelTypes.Rgba32;
public static readonly string[] AllResamplerNames = TestUtils.GetAllResamplerNames();
public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial };
public static readonly string[] SmokeTestResamplerNames =
{
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box),
nameof(KnownResamplers.Lanczos5),
};
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)]
[WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 0.3f, null, null)]
[WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 1.8f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 0.5f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 1f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 50, 50, DefaultPixelType, 8f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 201, 199, DefaultPixelType, null, 100, 99)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 301, 1180, DefaultPixelType, null, 300, 480)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 49, 80, DefaultPixelType, null, 301, 100)]
public void Resize_WorksWithAllResamplers<TPixel>(
TestImageProvider<TPixel> provider,
string samplerName,
float? ratio,
int? specificDestWidth,
int? specificDestHeight)
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
[Theory(
Skip = "Debug only, enable manually"
)]
[WithTestPatternImages(4000, 4000, PixelTypes.Rgba32, 300, 1024)]
[WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 1024)]
[WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 128)]
public void LargeImage<TPixel>(TestImageProvider<TPixel> provider, int destSize, int workingBufferSizeHintInKilobytes)
where TPixel : struct, IPixel<TPixel>
{
IResampler sampler = TestUtils.GetResampler(samplerName);
if (!TestEnvironment.Is64BitProcess)
{
return;
}
// NeirestNeighbourResampler is producing slightly different results With classic .NET framework on 32bit
// most likely because of differences in numeric behavior.
// The difference is well visible when comparing output for
// Resize_WorksWithAllResamplers_TestPattern301x1180_NearestNeighbor-300x480.png
// TODO: Should we investigate this?
bool allowHigherInaccuracy = !TestEnvironment.Is64BitProcess
&& string.IsNullOrEmpty(TestEnvironment.NetCoreVersion)
&& sampler is NearestNeighborResampler;
provider.Configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInKilobytes * 1024;
var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f);
using (var image = provider.GetImage())
{
image.Mutate(x => x.Resize(destSize, destSize));
image.DebugSave(provider, appendPixelTypeToFileName: false);
}
}
[Theory]
[WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)]
[WithBasicTestPatternImages(2, 256, PixelTypes.Rgba32, 1, 1, 1, 8)]
[WithBasicTestPatternImages(2, 32, PixelTypes.Rgba32, 1, 1, 1, 2)]
public void Resize_BasicSmall<TPixel>(TestImageProvider<TPixel> provider, int wN, int wD, int hN, int hD)
where TPixel : struct, IPixel<TPixel>
{
// Basic test case, very helpful for debugging
// [WithBasicTestPatternImages(15, 12, PixelTypes.Rgba32, 2, 3, 1, 2)] means:
// resizing: (15, 12) -> (10, 6)
// kernel dimensions: (3, 4)
provider.RunValidatingProcessorTest(
ctx =>
{
SizeF newSize;
string destSizeInfo;
if (ratio.HasValue)
{
newSize = ctx.GetCurrentSize() * ratio.Value;
destSizeInfo = ratio.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
else
{
if (!specificDestWidth.HasValue || !specificDestHeight.HasValue)
{
throw new InvalidOperationException(
"invalid dimensional input for Resize_WorksWithAllResamplers!");
}
using (Image<TPixel> image = provider.GetImage())
{
var destSize = new Size(image.Width * wN / wD, image.Height * hN / hD);
image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false));
FormattableString outputInfo = $"({wN}÷{wD},{hN}÷{hD})";
image.DebugSave(provider, outputInfo, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(provider, outputInfo, appendPixelTypeToFileName: false);
}
}
newSize = new SizeF(specificDestWidth.Value, specificDestHeight.Value);
destSizeInfo = $"{newSize.Width}x{newSize.Height}";
}
private static readonly int SizeOfVector4 = Unsafe.SizeOf<Vector4>();
FormattableString testOutputDetails = $"{samplerName}-{destSizeInfo}";
ctx.Apply(
img => img.DebugSave(
provider,
$"{testOutputDetails}-ORIGINAL",
appendPixelTypeToFileName: false));
ctx.Resize((Size)newSize, sampler, false);
return testOutputDetails;
},
comparer,
appendPixelTypeToFileName: false);
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32, 50)]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32, 60)]
[WithTestPatternImages(100, 400, PixelTypes.Rgba32, 110)]
[WithTestPatternImages(79, 97, PixelTypes.Rgba32, 73)]
[WithTestPatternImages(79, 97, PixelTypes.Rgba32, 5)]
[WithTestPatternImages(47, 193, PixelTypes.Rgba32, 73)]
[WithTestPatternImages(23, 211, PixelTypes.Rgba32, 31)]
public void WorkingBufferSizeHintInBytes_IsAppliedCorrectly<TPixel>(
TestImageProvider<TPixel> provider,
int workingBufferLimitInRows)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image0 = provider.GetImage())
{
Size destSize = image0.Size() / 4;
Configuration configuration = Configuration.CreateDefaultInstance();
int workingBufferSizeHintInBytes = workingBufferLimitInRows * destSize.Width * SizeOfVector4;
TestMemoryAllocator allocator = new TestMemoryAllocator();
configuration.MemoryAllocator = allocator;
configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInBytes;
var verticalKernelMap = ResizeKernelMap.Calculate(
KnownResamplers.Bicubic,
destSize.Height,
image0.Height,
Configuration.Default.MemoryAllocator);
int minimumWorkerAllocationInBytes = verticalKernelMap.MaxDiameter * 2 * destSize.Width * SizeOfVector4;
verticalKernelMap.Dispose();
using (Image<TPixel> image = image0.Clone(configuration))
{
image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false));
image.DebugSave(
provider,
testOutputDetails: workingBufferLimitInRows,
appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.001f),
provider,
testOutputDetails: workingBufferLimitInRows,
appendPixelTypeToFileName: false);
Assert.NotEmpty(allocator.AllocationLog);
int maxAllocationSize = allocator.AllocationLog.Where(
e => e.ElementType == typeof(Vector4)).Max(e => e.LengthInBytes);
Assert.True(maxAllocationSize <= Math.Max(workingBufferSizeHintInBytes, minimumWorkerAllocationInBytes));
}
}
}
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 8)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, -1)]
public void Resize_WorksWithAllParallelismLevels<TPixel>(TestImageProvider<TPixel> provider, int maxDegreeOfParallelism)
[WithTestPatternImages(100, 100, DefaultPixelType)]
public void Resize_Compand<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
provider.Configuration.MaxDegreeOfParallelism =
maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount;
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(image.Size() / 2, true));
FormattableString details = $"MDP{maxDegreeOfParallelism}";
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, false)]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, true)]
public void Resize_DoesNotBleedAlphaPixels<TPixel>(TestImageProvider<TPixel> provider, bool compand)
where TPixel : struct, IPixel<TPixel>
{
string details = compand ? "Compand" : "";
provider.RunValidatingProcessorTest(
x => x.Resize(x.GetCurrentSize() / 2),
x => x.Resize(x.GetCurrentSize() / 2, compand),
details,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
[WithTestPatternImages(100, 100, DefaultPixelType)]
public void Resize_Compand<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Gif.Giphy, DefaultPixelType)]
public void Resize_IsAppliedToAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(image.Size() / 2, true));
image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
// Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :(
image.DebugSave(provider, extension: "gif");
}
}
@ -152,41 +209,112 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height))
{
Assert.ThrowsAny<Exception>(
() =>
{
image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true));
});
() => { image1.Mutate(x => x.Resize(image0.Width / 2, image0.Height / 2, true)); });
}
}
}
[Theory]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, false)]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, true)]
public void Resize_DoesNotBleedAlphaPixels<TPixel>(TestImageProvider<TPixel> provider, bool compand)
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 8)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, -1)]
public void Resize_WorksWithAllParallelismLevels<TPixel>(
TestImageProvider<TPixel> provider,
int maxDegreeOfParallelism)
where TPixel : struct, IPixel<TPixel>
{
string details = compand ? "Compand" : "";
provider.Configuration.MaxDegreeOfParallelism =
maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount;
FormattableString details = $"MDP{maxDegreeOfParallelism}";
provider.RunValidatingProcessorTest(
x => x.Resize(x.GetCurrentSize() / 2, compand),
x => x.Resize(x.GetCurrentSize() / 2),
details,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
[WithFile(TestImages.Gif.Giphy, DefaultPixelType)]
public void Resize_IsAppliedToAllFrames<TPixel>(TestImageProvider<TPixel> provider)
[WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)]
[WithFileCollection(
nameof(CommonTestImages),
nameof(SmokeTestResamplerNames),
DefaultPixelType,
0.3f,
null,
null)]
[WithFileCollection(
nameof(CommonTestImages),
nameof(SmokeTestResamplerNames),
DefaultPixelType,
1.8f,
null,
null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 0.5f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 1f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 50, 50, DefaultPixelType, 8f, null, null)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 201, 199, DefaultPixelType, null, 100, 99)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 301, 1180, DefaultPixelType, null, 300, 480)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 49, 80, DefaultPixelType, null, 301, 100)]
public void Resize_WorksWithAllResamplers<TPixel>(
TestImageProvider<TPixel> provider,
string samplerName,
float? ratio,
int? specificDestWidth,
int? specificDestHeight)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic));
IResampler sampler = TestUtils.GetResampler(samplerName);
// Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :(
image.DebugSave(provider, extension: Extensions.Gif);
}
// NeirestNeighbourResampler is producing slightly different results With classic .NET framework on 32bit
// most likely because of differences in numeric behavior.
// The difference is well visible when comparing output for
// Resize_WorksWithAllResamplers_TestPattern301x1180_NearestNeighbor-300x480.png
// TODO: Should we investigate this?
bool allowHigherInaccuracy = !TestEnvironment.Is64BitProcess
&& string.IsNullOrEmpty(TestEnvironment.NetCoreVersion)
&& sampler is NearestNeighborResampler;
var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f);
// Let's make the working buffer size non-default:
provider.Configuration.WorkingBufferSizeHintInBytes = 16 * 1024 * SizeOfVector4;
provider.RunValidatingProcessorTest(
ctx =>
{
SizeF newSize;
string destSizeInfo;
if (ratio.HasValue)
{
newSize = ctx.GetCurrentSize() * ratio.Value;
destSizeInfo = ratio.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
else
{
if (!specificDestWidth.HasValue || !specificDestHeight.HasValue)
{
throw new InvalidOperationException(
"invalid dimensional input for Resize_WorksWithAllResamplers!");
}
newSize = new SizeF(specificDestWidth.Value, specificDestHeight.Value);
destSizeInfo = $"{newSize.Width}x{newSize.Height}";
}
FormattableString testOutputDetails = $"{samplerName}-{destSizeInfo}";
ctx.Apply(
img => img.DebugSave(
provider,
$"{testOutputDetails}-ORIGINAL",
appendPixelTypeToFileName: false));
ctx.Resize((Size)newSize, sampler, false);
return testOutputDetails;
},
comparer,
appendPixelTypeToFileName: false);
}
[Theory]
@ -196,10 +324,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
var sourceRectangle = new Rectangle(image.Width / 8, image.Height / 8, image.Width / 4, image.Height / 4);
var sourceRectangle = new Rectangle(
image.Width / 8,
image.Height / 8,
image.Width / 4,
image.Height / 4);
var destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.Mutate(x => x.Resize(image.Width, image.Height, KnownResamplers.Bicubic, sourceRectangle, destRectangle, false));
image.Mutate(
x => x.Resize(
image.Width,
image.Height,
KnownResamplers.Bicubic,
sourceRectangle,
destRectangle,
false));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -208,26 +347,39 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWidthAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeHeightAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(image.Width / 3, 0, false));
image.Mutate(x => x.Resize(0, image.Height / 3, false));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[WithTestPatternImages(10, 100, DefaultPixelType)]
public void ResizeHeightCannotKeepAspectKeepsOnePixel<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(0, 5));
Assert.Equal(1, image.Width);
Assert.Equal(5, image.Height);
}
}
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeHeightAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWidthAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(0, image.Height / 3, false));
image.Mutate(x => x.Resize(image.Width / 3, 0, false));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -247,30 +399,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
}
[Theory]
[WithTestPatternImages(10, 100, DefaultPixelType)]
public void ResizeHeightCannotKeepAspectKeepsOnePixel<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Resize(0, 5));
Assert.Equal(1, image.Width);
Assert.Equal(5, image.Height);
}
}
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithCropWidthMode<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWithBoxPadMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(image.Width / 2, image.Height)
};
{
Size = new Size(image.Width + 200, image.Height + 200), Mode = ResizeMode.BoxPad
};
image.Mutate(x => x.Resize(options));
@ -286,10 +425,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(image.Width, image.Height / 2)
};
var options = new ResizeOptions { Size = new Size(image.Width, image.Height / 2) };
image.Mutate(x => x.Resize(options));
@ -300,16 +436,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithPadMode<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWithCropWidthMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Pad
};
var options = new ResizeOptions { Size = new Size(image.Width / 2, image.Height) };
image.Mutate(x => x.Resize(options));
@ -320,16 +452,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithBoxPadMode<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWithMaxMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(image.Width + 200, image.Height + 200),
Mode = ResizeMode.BoxPad
};
var options = new ResizeOptions { Size = new Size(300, 300), Mode = ResizeMode.Max };
image.Mutate(x => x.Resize(options));
@ -340,16 +468,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithMaxMode<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWithMinMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(300, 300),
Mode = ResizeMode.Max
};
{
Size = new Size(
(int)Math.Round(image.Width * .75F),
(int)Math.Round(image.Height * .95F)),
Mode = ResizeMode.Min
};
image.Mutate(x => x.Resize(options));
@ -360,16 +490,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithMinMode<TPixel>(TestImageProvider<TPixel> provider)
public void ResizeWithPadMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size((int)Math.Round(image.Width * .75F), (int)Math.Round(image.Height * .95F)),
Mode = ResizeMode.Min
};
{
Size = new Size(image.Width + 200, image.Height), Mode = ResizeMode.Pad
};
image.Mutate(x => x.Resize(options));
@ -386,10 +515,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image<TPixel> image = provider.GetImage())
{
var options = new ResizeOptions
{
Size = new Size(image.Width / 2, image.Height),
Mode = ResizeMode.Stretch
};
{
Size = new Size(image.Width / 2, image.Height), Mode = ResizeMode.Stretch
};
image.Mutate(x => x.Resize(options));
@ -397,61 +525,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void BicubicWindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Bicubic;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void TriangleWindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Triangle;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-2, 0)]
[InlineData(-1, 0)]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(2, 0)]
public static void Lanczos3WindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Lanczos3;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
[Theory]
[InlineData(-4, 0)]
[InlineData(-2, 0)]
[InlineData(0, 1)]
[InlineData(2, 0)]
[InlineData(4, 0)]
public static void Lanczos5WindowOscillatesCorrectly(float x, float expected)
{
IResampler sampler = KnownResamplers.Lanczos5;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
}
}
}

37
tests/ImageSharp.Tests/TestUtilities/Attributes/WithBasicTestPatternImagesAttribute.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Reflection;
namespace SixLabors.ImageSharp.Tests
{
public class WithBasicTestPatternImagesAttribute : ImageDataAttributeBase
{
public WithBasicTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: this(null, width, height, pixelTypes, additionalParameters)
{
}
public WithBasicTestPatternImagesAttribute(string memberData, int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(memberData, pixelTypes, additionalParameters)
{
this.Width = width;
this.Height = height;
}
/// <summary>
/// Gets the width
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height
/// </summary>
public int Height { get; }
protected override string GetFactoryMethodName(MethodInfo testMethod) => "BasicTestPattern";
protected override object[] GetFactoryMethodArgs(MethodInfo testMethod, Type factoryType) => new object[] { this.Width, this.Height };
}
}

65
tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests
{
public abstract partial class TestImageProvider<TPixel>
{
private class BasicTestPatternProvider : BlankProvider
{
public BasicTestPatternProvider(int width, int height)
: base(width, height)
{
}
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public BasicTestPatternProvider()
{
}
public override string SourceFileOrDescription => TestUtils.AsInvariantString($"BasicTestPattern{this.Width}x{this.Height}");
public override Image<TPixel> GetImage()
{
var result = new Image<TPixel>(this.Configuration, this.Width, this.Height);
TPixel topLeftColor = NamedColors<TPixel>.Red;
TPixel topRightColor = NamedColors<TPixel>.Green;
TPixel bottomLeftColor = NamedColors<TPixel>.Blue;
// Transparent purple:
TPixel bottomRightColor = default;
bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f));
int midY = this.Height / 2;
int midX = this.Width / 2;
for (int y = 0; y < midY; y++)
{
Span<TPixel> row = result.GetPixelRowSpan(y);
row.Slice(0, midX).Fill(topLeftColor);
row.Slice(midX, this.Width-midX).Fill(topRightColor);
}
for (int y = midY; y < this.Height; y++)
{
Span<TPixel> row = result.GetPixelRowSpan(y);
row.Slice(0, midX).Fill(bottomLeftColor);
row.Slice(midX, this.Width-midX).Fill(bottomRightColor);
}
return result;
}
}
}
}

5
tests/ImageSharp.Tests/TestUtilities/ImageProviders/BlankProvider.cs

@ -20,6 +20,9 @@ namespace SixLabors.ImageSharp.Tests
this.Height = height;
}
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public BlankProvider()
{
this.Width = 100;
@ -32,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests
protected int Width { get; private set; }
public override Image<TPixel> GetImage() => new Image<TPixel>(this.Width, this.Height);
public override Image<TPixel> GetImage() => new Image<TPixel>(this.Configuration, this.Width, this.Height);
public override void Deserialize(IXunitSerializationInfo info)

2
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -152,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests
Image<TPixel> cachedImage = cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone();
return cachedImage.Clone(this.Configuration);
}
public override void Deserialize(IXunitSerializationInfo info)

3
tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs

@ -35,6 +35,9 @@ namespace SixLabors.ImageSharp.Tests
this.a = a;
}
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public SolidProvider()
: base()
{

8
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

@ -44,6 +44,12 @@ namespace SixLabors.ImageSharp.Tests
public string MethodName { get; private set; }
public string OutputSubfolderName { get; private set; }
public static TestImageProvider<TPixel> BasicTestPattern(int width,
int height,
MethodInfo testMethod = null,
PixelTypes pixelTypeOverride = PixelTypes.Undefined)
=> new BasicTestPatternProvider(width, height).Init(testMethod, pixelTypeOverride);
public static TestImageProvider<TPixel> TestPattern(
int width,
int height,
@ -100,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests
/// </summary>
public Image<TPixel> GetImage(Action<IImageProcessingContext<TPixel>> operationsToApply)
{
Image<TPixel> img = GetImage();
Image<TPixel> img = this.GetImage();
img.Mutate(operationsToApply);
return img;
}

8
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Net.Mime;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
@ -25,8 +26,10 @@ namespace SixLabors.ImageSharp.Tests
{
}
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public TestPatternProvider()
: base()
{
}
@ -42,9 +45,8 @@ namespace SixLabors.ImageSharp.Tests
DrawTestPattern(image);
TestImages.Add(this.SourceFileOrDescription, image);
}
return TestImages[this.SourceFileOrDescription].Clone(this.Configuration);
}
return TestImages[this.SourceFileOrDescription].Clone();
}
/// <summary>

13
tests/ImageSharp.Tests/TestUtilities/TestDataGenerator.cs

@ -23,14 +23,19 @@ namespace SixLabors.ImageSharp.Tests
{
float[] values = new float[length];
for (int i = 0; i < length; i++)
{
values[i] = GetRandomFloat(rnd, minVal, maxVal);
}
RandomFill(rnd, values, minVal, maxVal);
return values;
}
public static void RandomFill(this Random rnd, Span<float> destination, float minVal, float maxVal)
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = GetRandomFloat(rnd, minVal, maxVal);
}
}
/// <summary>
/// Creates an <see cref="Vector4[]"/> of the given length consisting of random values between the two ranges.
/// </summary>

39
tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.Memory;
@ -8,6 +10,8 @@ namespace SixLabors.ImageSharp.Tests.Memory
{
internal class TestMemoryAllocator : MemoryAllocator
{
private List<AllocationRequest> allocationLog = new List<AllocationRequest>();
public TestMemoryAllocator(byte dirtyValue = 42)
{
this.DirtyValue = dirtyValue;
@ -18,10 +22,11 @@ namespace SixLabors.ImageSharp.Tests.Memory
/// </summary>
public byte DirtyValue { get; }
public IList<AllocationRequest> AllocationLog => this.allocationLog;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
{
T[] array = this.AllocateArray<T>(length, options);
return new BasicArrayBuffer<T>(array, length);
}
@ -34,6 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Memory
private T[] AllocateArray<T>(int length, AllocationOptions options)
where T : struct
{
this.allocationLog.Add(AllocationRequest.Create<T>(options, length));
var array = new T[length + 42];
if (options == AllocationOptions.None)
@ -44,6 +50,35 @@ namespace SixLabors.ImageSharp.Tests.Memory
return array;
}
public struct AllocationRequest
{
private AllocationRequest(Type elementType, AllocationOptions allocationOptions, int length, int lengthInBytes)
{
this.ElementType = elementType;
this.AllocationOptions = allocationOptions;
this.Length = length;
this.LengthInBytes = lengthInBytes;
if (elementType == typeof(Vector4))
{
}
}
public static AllocationRequest Create<T>(AllocationOptions allocationOptions, int length)
{
Type type = typeof(T);
int elementSize = Marshal.SizeOf(type);
return new AllocationRequest(type, allocationOptions, length, length * elementSize);
}
public Type ElementType { get; }
public AllocationOptions AllocationOptions { get; }
public int Length { get; }
public int LengthInBytes { get; }
}
/// <summary>
/// Wraps an array as an <see cref="IManagedByteBuffer"/> instance.

452
tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs

@ -4,112 +4,135 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
using Xunit.Abstractions;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests
{
public class TestImageProviderTests
{
public static readonly TheoryData<object> BasicData = new TheoryData<object>()
{
TestImageProvider<Rgba32>.Blank(10, 20),
TestImageProvider<HalfVector4>.Blank(10, 20),
};
public static readonly TheoryData<object> FileData = new TheoryData<object>()
{
TestImageProvider<Rgba32>.File(TestImages.Bmp.Car),
TestImageProvider<HalfVector4>.File(
TestImages.Bmp.F)
};
public static string[] AllBmpFiles = { TestImages.Bmp.F, TestImages.Bmp.Bit8 };
public TestImageProviderTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
public void NoOutputSubfolderIsPresentByDefault<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> => Assert.Empty(provider.Utility.OutputSubfolderName);
/// <summary>
/// Need to us <see cref="GenericFactory{TPixel}"/> to create instance of <see cref="Image"/> when pixelType is StandardImageClass
/// </summary>
/// <typeparam name="TPixel"></typeparam>
/// <param name="factory"></param>
/// <returns></returns>
public static Image<TPixel> CreateTestImage<TPixel>()
where TPixel : struct, IPixel<TPixel> =>
new Image<TPixel>(3, 3);
[Theory]
[WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")]
public void Use_WithEmptyImageAttribute<TPixel>(TestImageProvider<TPixel> provider, string message)
[MemberData(nameof(BasicData))]
public void Blank_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
Assert.Equal(42, img.Width);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
Assert.True(img.Width * img.Height > 0);
}
[Theory]
[WithBlankImages(42, 666, PixelTypes.All, "hello")]
public void Use_WithBlankImagesAttribute_WithAllPixelTypes<TPixel>(
TestImageProvider<TPixel> provider,
string message)
[MemberData(nameof(FileData))]
public void File_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
this.Output.WriteLine("SRC: " + provider.Utility.SourceFileOrDescription);
this.Output.WriteLine("OUT: " + provider.Utility.GetTestOutputFileName());
Image<TPixel> img = provider.GetImage();
Assert.Equal(42, img.Width);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
Assert.True(img.Width * img.Height > 0);
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32, PixelTypes.Rgba32)]
[WithBlankImages(1, 1, PixelTypes.Alpha8, PixelTypes.Alpha8)]
[WithBlankImages(1, 1, PixelTypes.Argb32, PixelTypes.Argb32)]
public void PixelType_PropertyValueIsCorrect<TPixel>(TestImageProvider<TPixel> provider, PixelTypes expected)
where TPixel : struct, IPixel<TPixel> => Assert.Equal(expected, provider.PixelType);
[Theory]
[WithFile(TestImages.Bmp.Car, PixelTypes.All, 88)]
[WithFile(TestImages.Bmp.F, PixelTypes.All, 88)]
public void Use_WithFileAttribute<TPixel>(TestImageProvider<TPixel> provider, int yo)
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
public void GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache<TPixel>(
TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
{
// We don't cache with the 32 bit build.
return;
}
Assert.NotNull(provider.Utility.SourceFileOrDescription);
Image<TPixel> img = provider.GetImage();
Assert.True(img.Width * img.Height > 0);
Assert.Equal(88, yo);
TestDecoder.DoTestThreadSafe(
() =>
{
string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache);
string fn = provider.Utility.GetTestOutputFileName("jpg");
this.Output.WriteLine(fn);
}
var decoder = new TestDecoder();
decoder.InitCaller(testName);
private class TestDecoder : IImageDecoder
{
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
invocationCounts[this.callerName]++;
return new Image<TPixel>(42, 42);
}
provider.GetImage(decoder);
Assert.Equal(1, TestDecoder.GetInvocationCount(testName));
// Couldn't make xUnit happy without this hackery:
provider.GetImage(decoder);
Assert.Equal(1, TestDecoder.GetInvocationCount(testName));
});
}
private static readonly ConcurrentDictionary<string, int> invocationCounts = new ConcurrentDictionary<string, int>();
[Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
public void GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual<TPixel>(
TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Assert.NotNull(provider.Utility.SourceFileOrDescription);
private string callerName = null;
TestDecoderWithParameters.DoTestThreadSafe(
() =>
{
string testName = nameof(this
.GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual);
internal void InitCaller(string name)
{
this.callerName = name;
invocationCounts[name] = 0;
}
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 };
decoder1.InitCaller(testName);
internal static int GetInvocationCount(string callerName) => invocationCounts[callerName];
var decoder2 = new TestDecoderWithParameters() { Param1 = "LoL", Param2 = 42 };
decoder2.InitCaller(testName);
private static readonly object Monitor = new object();
provider.GetImage(decoder1);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
public static void DoTestThreadSafe(Action action)
{
lock (Monitor)
{
action();
}
}
provider.GetImage(decoder2);
Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName));
});
}
[Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
public void GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache<TPixel>(TestImageProvider<TPixel> provider)
public void GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual<TPixel>(
TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
@ -120,121 +143,122 @@ namespace SixLabors.ImageSharp.Tests
Assert.NotNull(provider.Utility.SourceFileOrDescription);
TestDecoder.DoTestThreadSafe(() =>
{
string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache);
TestDecoderWithParameters.DoTestThreadSafe(
() =>
{
string testName = nameof(this
.GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual);
var decoder = new TestDecoder();
decoder.InitCaller(testName);
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
decoder1.InitCaller(testName);
provider.GetImage(decoder);
Assert.Equal(1, TestDecoder.GetInvocationCount(testName));
var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
decoder2.InitCaller(testName);
provider.GetImage(decoder);
Assert.Equal(1, TestDecoder.GetInvocationCount(testName));
});
}
private class TestDecoderWithParameters : IImageDecoder
{
public string Param1 { get; set; }
public int Param2 { get; set; }
provider.GetImage(decoder1);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
invocationCounts[this.callerName]++;
return new Image<TPixel>(42, 42);
}
provider.GetImage(decoder2);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
});
}
private static readonly ConcurrentDictionary<string, int> invocationCounts = new ConcurrentDictionary<string, int>();
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
public void NoOutputSubfolderIsPresentByDefault<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> =>
Assert.Empty(provider.Utility.OutputSubfolderName);
private string callerName = null;
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32, PixelTypes.Rgba32)]
[WithBlankImages(1, 1, PixelTypes.Alpha8, PixelTypes.Alpha8)]
[WithBlankImages(1, 1, PixelTypes.Argb32, PixelTypes.Argb32)]
public void PixelType_PropertyValueIsCorrect<TPixel>(TestImageProvider<TPixel> provider, PixelTypes expected)
where TPixel : struct, IPixel<TPixel> =>
Assert.Equal(expected, provider.PixelType);
internal void InitCaller(string name)
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void SaveTestOutputFileMultiFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
this.callerName = name;
invocationCounts[name] = 0;
}
internal static int GetInvocationCount(string callerName) => invocationCounts[callerName];
private static readonly object Monitor = new object();
string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image);
public static void DoTestThreadSafe(Action action)
{
lock (Monitor)
Assert.True(files.Length > 2);
foreach (string path in files)
{
action();
this.Output.WriteLine(path);
Assert.True(File.Exists(path));
}
}
}
[Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
public void GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual<TPixel>(TestImageProvider<TPixel> provider)
[WithBasicTestPatternImages(50, 100, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(49, 17, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(20, 10, PixelTypes.Rgba32)]
public void Use_WithBasicTestPatternImages<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
using (Image<TPixel> img = provider.GetImage())
{
// We don't cache with the 32 bit build.
return;
img.DebugSave(provider);
}
}
Assert.NotNull(provider.Utility.SourceFileOrDescription);
TestDecoderWithParameters.DoTestThreadSafe(() =>
{
string testName =
nameof(this.GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual);
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
decoder1.InitCaller(testName);
[Theory]
[WithBlankImages(42, 666, PixelTypes.All, "hello")]
public void Use_WithBlankImagesAttribute_WithAllPixelTypes<TPixel>(
TestImageProvider<TPixel> provider,
string message)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
decoder2.InitCaller(testName);
Assert.Equal(42, img.Width);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
}
provider.GetImage(decoder1);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
[Theory]
[WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")]
public void Use_WithEmptyImageAttribute<TPixel>(TestImageProvider<TPixel> provider, string message)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
provider.GetImage(decoder2);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
});
Assert.Equal(42, img.Width);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
}
[Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
public void GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Bmp.Car, PixelTypes.All, 123)]
[WithFile(TestImages.Bmp.F, PixelTypes.All, 123)]
public void Use_WithFileAttribute<TPixel>(TestImageProvider<TPixel> provider, int yo)
where TPixel : struct, IPixel<TPixel>
{
Assert.NotNull(provider.Utility.SourceFileOrDescription);
TestDecoderWithParameters.DoTestThreadSafe(() =>
using (Image<TPixel> img = provider.GetImage())
{
string testName =
nameof(this.GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual);
Assert.True(img.Width * img.Height > 0);
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 };
decoder1.InitCaller(testName);
Assert.Equal(123, yo);
var decoder2 = new TestDecoderWithParameters() { Param1 = "LoL", Param2 = 42 };
decoder2.InitCaller(testName);
provider.GetImage(decoder1);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
provider.GetImage(decoder2);
Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName));
});
string fn = provider.Utility.GetTestOutputFileName("jpg");
this.Output.WriteLine(fn);
}
}
public static string[] AllBmpFiles =
{
TestImages.Bmp.F,
TestImages.Bmp.Bit8
};
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)]
public void Use_WithFileAttribute_CustomConfig<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
EnsureCustomConfigurationIsApplied(provider);
}
[Theory]
[WithFileCollection(nameof(AllBmpFiles), PixelTypes.Rgba32 | PixelTypes.Argb32)]
@ -249,20 +273,15 @@ namespace SixLabors.ImageSharp.Tests
}
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void SaveTestOutputFileMultiFrame<TPixel>(TestImageProvider<TPixel> provider)
[WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)]
public void Use_WithMemberFactoryAttribute<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
Image<TPixel> img = provider.GetImage();
Assert.Equal(3, img.Width);
if (provider.PixelType == PixelTypes.Rgba32)
{
string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image);
Assert.True(files.Length > 2);
foreach (string path in files)
{
this.Output.WriteLine(path);
Assert.True(File.Exists(path));
}
Assert.IsType<Image<Rgba32>>(img);
}
}
@ -291,75 +310,112 @@ namespace SixLabors.ImageSharp.Tests
}
}
/// <summary>
/// Need to us <see cref="GenericFactory{TPixel}"/> to create instance of <see cref="Image"/> when pixelType is StandardImageClass
/// </summary>
/// <typeparam name="TPixel"></typeparam>
/// <param name="factory"></param>
/// <returns></returns>
public static Image<TPixel> CreateTestImage<TPixel>()
where TPixel : struct, IPixel<TPixel> => new Image<TPixel>(3, 3);
[Theory]
[WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)]
public void Use_WithMemberFactoryAttribute<TPixel>(TestImageProvider<TPixel> provider)
[WithTestPatternImages(49, 20, PixelTypes.Rgba32)]
public void Use_WithTestPatternImages<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
Assert.Equal(3, img.Width);
if (provider.PixelType == PixelTypes.Rgba32)
using (Image<TPixel> img = provider.GetImage())
{
Assert.IsType<Image<Rgba32>>(img);
img.DebugSave(provider);
}
}
[Theory]
[WithTestPatternImages(49,20, PixelTypes.Rgba32)]
public void Use_WithTestPatternImages<TPixel>(TestImageProvider<TPixel> provider)
[WithTestPatternImages(20, 20, PixelTypes.Rgba32)]
public void Use_WithTestPatternImages_CustomConfiguration<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
EnsureCustomConfigurationIsApplied(provider);
}
private static void EnsureCustomConfigurationIsApplied<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (var image1 = provider.GetImage())
{
img.DebugSave(provider);
var customConfiguration = Configuration.CreateDefaultInstance();
provider.Configuration = customConfiguration;
using (var image2 = provider.GetImage())
using (var image3 = provider.GetImage())
{
Assert.Same(customConfiguration, image2.GetConfiguration());
Assert.Same(customConfiguration, image3.GetConfiguration());
}
}
}
public static readonly TheoryData<object> BasicData = new TheoryData<object>()
private class TestDecoder : IImageDecoder
{
TestImageProvider<Rgba32>.Blank(10, 20),
TestImageProvider<HalfVector4>.Blank(
10,
20),
};
// Couldn't make xUnit happy without this hackery:
[Theory]
[MemberData(nameof(BasicData))]
public void Blank_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
private static readonly ConcurrentDictionary<string, int> invocationCounts =
new ConcurrentDictionary<string, int>();
Assert.True(img.Width * img.Height > 0);
private static readonly object Monitor = new object();
private string callerName = null;
public static void DoTestThreadSafe(Action action)
{
lock (Monitor)
{
action();
}
}
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
invocationCounts[this.callerName]++;
return new Image<TPixel>(42, 42);
}
internal static int GetInvocationCount(string callerName) => invocationCounts[callerName];
internal void InitCaller(string name)
{
this.callerName = name;
invocationCounts[name] = 0;
}
}
public static readonly TheoryData<object> FileData = new TheoryData<object>()
private class TestDecoderWithParameters : IImageDecoder
{
TestImageProvider<Rgba32>.File(TestImages.Bmp.Car),
TestImageProvider<HalfVector4>.File(TestImages.Bmp.F)
};
private static readonly ConcurrentDictionary<string, int> invocationCounts =
new ConcurrentDictionary<string, int>();
[Theory]
[MemberData(nameof(FileData))]
public void File_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
this.Output.WriteLine("SRC: " + provider.Utility.SourceFileOrDescription);
this.Output.WriteLine("OUT: " + provider.Utility.GetTestOutputFileName());
private static readonly object Monitor = new object();
Image<TPixel> img = provider.GetImage();
private string callerName = null;
Assert.True(img.Width * img.Height > 0);
public string Param1 { get; set; }
public int Param2 { get; set; }
public static void DoTestThreadSafe(Action action)
{
lock (Monitor)
{
action();
}
}
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
invocationCounts[this.callerName]++;
return new Image<TPixel>(42, 42);
}
internal static int GetInvocationCount(string callerName) => invocationCounts[callerName];
internal void InitCaller(string name)
{
this.callerName = name;
invocationCounts[name] = 0;
}
}
}
}
}

2
tests/Images/External

@ -1 +1 @@
Subproject commit 802725dec2a6b1ca02f9e2f9a4c3f625583d0696
Subproject commit 8693e2fd4577a9ac1a749da8db564095b5a05389
Loading…
Cancel
Save