Browse Source

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

Limit ResizeProcessor memory consumption
af/merge-core
Anton Firsov 7 years ago
committed by GitHub
parent
commit
a3e773d671
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> /// </summary>
internal IFileSystem FileSystem { get; set; } = new LocalFileSystem(); 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> /// <summary>
/// Gets or sets the image operations provider factory. /// Gets or sets the image operations provider factory.
/// </summary> /// </summary>
@ -118,9 +127,9 @@ namespace SixLabors.ImageSharp
} }
/// <summary> /// <summary>
/// Creates a shallow copy of the <see cref="Configuration"/> /// Creates a shallow copy of the <see cref="Configuration"/>.
/// </summary> /// </summary>
/// <returns>A new configuration instance</returns> /// <returns>A new configuration instance.</returns>
public Configuration Clone() public Configuration Clone()
{ {
return new Configuration return new Configuration
@ -130,18 +139,19 @@ namespace SixLabors.ImageSharp
MemoryAllocator = this.MemoryAllocator, MemoryAllocator = this.MemoryAllocator,
ImageOperationsProvider = this.ImageOperationsProvider, ImageOperationsProvider = this.ImageOperationsProvider,
ReadOrigin = this.ReadOrigin, ReadOrigin = this.ReadOrigin,
FileSystem = this.FileSystem FileSystem = this.FileSystem,
WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes,
}; };
} }
/// <summary> /// <summary>
/// Creates the default instance with the following <see cref="IConfigurationModule"/>s preregistered: /// Creates the default instance with the following <see cref="IConfigurationModule"/>s preregistered:
/// <para><see cref="PngConfigurationModule"/></para> /// <see cref="PngConfigurationModule"/>
/// <para><see cref="JpegConfigurationModule"/></para> /// <see cref="JpegConfigurationModule"/>
/// <para><see cref="GifConfigurationModule"/></para> /// <see cref="GifConfigurationModule"/>
/// <para><see cref="BmpConfigurationModule"/></para> /// <see cref="BmpConfigurationModule"/>.
/// </summary> /// </summary>
/// <returns>The default configuration of <see cref="Configuration"/></returns> /// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance() internal static Configuration CreateDefaultInstance()
{ {
return new Configuration( return new Configuration(

160
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -14,55 +17,135 @@ namespace SixLabors.ImageSharp.Memory
internal static class Buffer2DExtensions internal static class Buffer2DExtensions
{ {
/// <summary> /// <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> /// </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 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> /// <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> /// </summary>
/// <param name="buffer">The buffer</param> /// <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> /// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam> /// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns> /// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 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> /// <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> /// </summary>
/// <param name="buffer">The buffer</param> /// <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> /// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam> /// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns> /// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 where T : struct
{ {
return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width); return buffer.GetSpan().Slice((y * buffer.Width) + x, buffer.Width - x);
} }
/// <summary> /// <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> /// </summary>
/// <param name="buffer">The buffer</param> /// <param name="buffer">The buffer</param>
/// <param name="y">The y (row) coordinate</param> /// <param name="y">The y (row) coordinate</param>
/// <typeparam name="T">The element type</typeparam> /// <typeparam name="T">The element type</typeparam>
/// <returns>The <see cref="Span{T}"/></returns> /// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 where T : struct
{ {
return buffer.MemorySource.Memory.Slice(y * buffer.Width, buffer.Width); return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width);
} }
/// <summary> /// <summary>
@ -78,49 +161,28 @@ namespace SixLabors.ImageSharp.Memory
} }
/// <summary> /// <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> /// </summary>
/// <typeparam name="T">The element type</typeparam> internal static Span<T> GetSpan<T>(this Buffer2D<T> buffer)
/// <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 where T : struct
{ {
return new Rectangle(0, 0, buffer.Width, buffer.Height); return buffer.MemorySource.GetSpan();
} }
/// <summary> [Conditional("DEBUG")]
/// Return a <see cref="BufferArea{T}"/> to the subarea represented by 'rectangle' private static void CheckColumnRegionsDoNotOverlap<T>(
/// </summary> Buffer2D<T> buffer,
/// <typeparam name="T">The element type</typeparam> int sourceIndex,
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param> int destIndex,
/// <param name="rectangle">The rectangle subarea</param> int columnCount)
/// <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)
where T : struct 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. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Memory namespace SixLabors.ImageSharp.Memory
@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Memory
/// <summary> /// <summary>
/// Represents an interval of rows in a <see cref="Rectangle"/> and/or <see cref="Buffer2D{T}"/> /// Represents an interval of rows in a <see cref="Rectangle"/> and/or <see cref="Buffer2D{T}"/>
/// </summary> /// </summary>
internal readonly struct RowInterval internal readonly struct RowInterval : IEquatable<RowInterval>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RowInterval"/> struct. /// Initializes a new instance of the <see cref="RowInterval"/> struct.
@ -36,7 +38,33 @@ namespace SixLabors.ImageSharp.Memory
/// </summary> /// </summary>
public int Height => this.Max - this.Min; 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 /> /// <inheritdoc />
public override string ToString() => $"RowInterval [{this.Min}->{this.Max}]"; 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 namespace SixLabors.ImageSharp.PixelFormats
{ {
/// <summary>
/// Extension and utility methods for <see cref="PixelConversionModifiers"/>.
/// </summary>
internal static class PixelConversionModifiersExtensions internal static class PixelConversionModifiersExtensions
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -16,5 +19,20 @@ namespace SixLabors.ImageSharp.PixelFormats
this PixelConversionModifiers modifiers, this PixelConversionModifiers modifiers,
PixelConversionModifiers removeThis) => PixelConversionModifiers removeThis) =>
modifiers & ~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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Linq; using System.Linq;
using System.Numerics;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
/// <summary> /// <summary>
/// Provides methods to help calculate the target rectangle when resizing using the /// Provides methods to help calculate the target rectangle when resizing using the
@ -13,6 +15,16 @@ namespace SixLabors.ImageSharp.Processing
/// </summary> /// </summary>
internal static class ResizeHelper 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> /// <summary>
/// Calculates the target location and bounds to perform the resize operation against. /// Calculates the target location and bounds to perform the resize operation against.
/// </summary> /// </summary>
@ -21,9 +33,13 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="width">The target width</param> /// <param name="width">The target width</param>
/// <param name="height">The target height</param> /// <param name="height">The target height</param>
/// <returns> /// <returns>
/// The <see cref="ValueTuple{Size,Rectangle}"/>. /// The tuple representing the location and the bounds
/// </returns> /// </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) 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) if (width <= 0 || height <= 0)
{ {
@ -147,152 +246,15 @@ namespace SixLabors.ImageSharp.Processing
destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight); destinationWidth = (int)MathF.Ceiling(sourceWidth * percentHeight);
} }
return (new Size(width, height), new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight)); 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);
} }
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 destinationWidth = width;
int destinationHeight = height; int destinationHeight = height;
@ -320,7 +282,11 @@ namespace SixLabors.ImageSharp.Processing
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); 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 sourceWidth = source.Width;
int sourceHeight = source.Height; int sourceHeight = source.Height;
@ -372,5 +338,78 @@ namespace SixLabors.ImageSharp.Processing
// Replace the size to match the rectangle. // Replace the size to match the rectangle.
return (new Size(width, height), new Rectangle(0, 0, destinationWidth, destinationHeight)); 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. /// Initializes a new instance of the <see cref="ResizeKernel"/> struct.
/// </summary> /// </summary>
[MethodImpl(InliningOptions.ShortMethod)] [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.bufferPtr = bufferPtr;
this.Length = length; this.Length = length;
} }
/// <summary> /// <summary>
/// Gets the left index for the destination row /// Gets the start index for the destination row.
/// </summary> /// </summary>
public int Left { get; } public int StartIndex { get; }
/// <summary> /// <summary>
/// Gets the the length of the kernel /// Gets the the length of the kernel.
/// </summary> /// </summary>
public int Length { get; } public int Length { get; }
/// <summary> /// <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> /// </summary>
/// <value>The <see cref="Span{T}"/> /// <value>The <see cref="Span{T}"/>.
/// </value> /// </value>
public Span<float> Values public Span<float> Values
{ {
@ -54,10 +54,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>The weighted sum</returns> /// <returns>The weighted sum</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public Vector4 Convolve(Span<Vector4> rowSpan) 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); 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 // Destination color components
Vector4 result = Vector4.Zero; Vector4 result = Vector4.Zero;
@ -65,7 +69,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int i = 0; i < this.Length; i++) for (int i = 0; i < this.Length; i++)
{ {
float weight = Unsafe.Add(ref horizontalValues, 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; result += v * weight;
} }
@ -73,7 +79,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
/// <summary> /// <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"/>. /// to the value <paramref name="left"/>.
/// </summary> /// </summary>
internal ResizeKernel AlterLeftValue(int left) 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.radius = radius;
this.sourceLength = sourceLength; this.sourceLength = sourceLength;
this.DestinationLength = destinationLength; this.DestinationLength = destinationLength;
int maxWidth = (radius * 2) + 1; this.MaxDiameter = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean); this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin(); this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength]; this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[maxWidth]; this.tempValues = new double[this.MaxDiameter];
} }
/// <summary> /// <summary>
@ -66,6 +66,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary> /// </summary>
public int DestinationLength { get; } public int DestinationLength { get; }
/// <summary>
/// Gets the maximum diameter of the kernels.
/// </summary>
public int MaxDiameter { get; }
/// <summary> /// <summary>
/// Gets a string of information to help debugging /// Gets a string of information to help debugging
/// </summary> /// </summary>

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

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

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;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Globalization;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -22,15 +23,21 @@ namespace SixLabors.ImageSharp.Benchmarks
private Bitmap sourceBitmap; private Bitmap sourceBitmap;
[Params(3032)] [Params("3032-400")]
public int SourceSize { get; set; } 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] [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.sourceImage = new Image<TPixel>(this.Configuration, this.SourceSize, this.SourceSize);
this.sourceBitmap = new Bitmap(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); 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 // 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 // Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC
// .NET Core SDK=2.1.602 // .NET Core SDK=2.2.202
// [Host] : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT // [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 // 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 | // 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 | 118.71 ms | 4.884 ms | 0.2677 ms | 1.00 | - | - | - | 2048 B | // 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 | 94.55 ms | 16.160 ms | 0.8858 ms | 0.80 | - | - | - | 16384 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 | 118.38 ms | 2.814 ms | 0.1542 ms | 1.00 | - | - | - | 96 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 | 90.28 ms | 4.679 ms | 0.2565 ms | 0.76 | - | - | - | 15712 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> public class Resize_Bicubic_Bgra32 : ResizeBenchmarkBase<Bgra32>

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

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

6
tests/ImageSharp.Sandbox46/Program.cs

@ -33,11 +33,11 @@ namespace SixLabors.ImageSharp.Sandbox46
/// </param> /// </param>
public static void Main(string[] args) public static void Main(string[] args)
{ {
RunJpegColorProfilingTests(); // RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests(); // RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest(); // RunToVector4ProfilingTest();
// RunResizeProfilingTest(); RunResizeProfilingTest();
Console.ReadLine(); Console.ReadLine();
} }
@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Sandbox46
private static void RunResizeProfilingTest() private static void RunResizeProfilingTest()
{ {
var test = new ResizeProfilingBenchmarks(new ConsoleOutput()); var test = new ResizeProfilingBenchmarks(new ConsoleOutput());
test.ResizeBicubic(2000, 2000); test.ResizeBicubic(4000, 4000);
} }
private static void RunToVector4ProfilingTest() 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. /// Test that the default configuration is not null.
/// </summary> /// </summary>
[Fact] [Fact]
public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null); public void TestDefaultConfigurationIsNotNull() => Assert.True(this.DefaultConfiguration != null);
/// <summary> /// <summary>
/// Test that the default configuration read origin options is set to begin. /// Test that the default configuration read origin options is set to begin.
/// </summary> /// </summary>
[Fact] [Fact]
public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current); public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(this.DefaultConfiguration.ReadOrigin == ReadOrigin.Current);
/// <summary> /// <summary>
/// Test that the default configuration parallel options max degrees of parallelism matches the /// Test that the default configuration parallel options max degrees of parallelism matches the
@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact] [Fact]
public void TestDefaultConfigurationMaxDegreeOfParallelism() public void TestDefaultConfigurationMaxDegreeOfParallelism()
{ {
Assert.True(Configuration.Default.MaxDegreeOfParallelism == Environment.ProcessorCount); Assert.True(this.DefaultConfiguration.MaxDegreeOfParallelism == Environment.ProcessorCount);
var cfg = new Configuration(); var cfg = new Configuration();
Assert.True(cfg.MaxDegreeOfParallelism == Environment.ProcessorCount); Assert.True(cfg.MaxDegreeOfParallelism == Environment.ProcessorCount);
@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests
public void ConfigurationCannotAddDuplicates() public void ConfigurationCannotAddDuplicates()
{ {
const int count = 4; const int count = 4;
Configuration config = Configuration.Default; Configuration config = this.DefaultConfiguration;
Assert.Equal(count, config.ImageFormats.Count()); Assert.Equal(count, config.ImageFormats.Count());
@ -105,9 +105,16 @@ namespace SixLabors.ImageSharp.Tests
[Fact] [Fact]
public void DefaultConfigurationHasCorrectFormatCount() public void DefaultConfigurationHasCorrectFormatCount()
{ {
Configuration config = Configuration.Default; Configuration config = Configuration.CreateDefaultInstance();
Assert.Equal(4, config.ImageFormats.Count()); 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)); 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()); 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); var bounds = new Rectangle(10, 10, size.Width / 2, size.Height / 2);
ctx.DetectEdges(bounds); ctx.DetectEdges(bounds);
}, },
comparer: ValidatorComparer,
useReferenceOutputFrom: nameof(this.DetectEdges_InBox)); 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) 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), 40, 50 },
{ nameof(KnownResamplers.Bicubic), 500, 200 }, { nameof(KnownResamplers.Bicubic), 500, 200 },
{ nameof(KnownResamplers.Bicubic), 200, 500 }, { nameof(KnownResamplers.Bicubic), 200, 500 },
{ nameof(KnownResamplers.Bicubic), 3032, 400 },
{ nameof(KnownResamplers.Bicubic), 10, 25 }, { nameof(KnownResamplers.Bicubic), 10, 25 },
@ -93,7 +94,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
GenerateImageResizeData(); GenerateImageResizeData();
[Theory(Skip = "Only for debugging and development")] [Theory(
Skip = "Only for debugging and development"
)]
[MemberData(nameof(KernelMapData))] [MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) 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 referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG #if DEBUG
this.Output.WriteLine(kernelMap.Info);
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
#endif #endif
@ -146,8 +152,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
referenceKernel.Length == kernel.Length, referenceKernel.Length == kernel.Length,
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}"); $"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}");
Assert.True( Assert.True(
referenceKernel.Left == kernel.Left, referenceKernel.Left == kernel.StartIndex,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}"); $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}");
float[] expectedValues = referenceKernel.Values; float[] expectedValues = referenceKernel.Values;
Span<float> actualValues = kernel.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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms 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[] AllResamplerNames = TestUtils.GetAllResamplerNames();
public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial };
public static readonly string[] SmokeTestResamplerNames = public static readonly string[] SmokeTestResamplerNames =
{ {
nameof(KnownResamplers.NearestNeighbor), nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box), nameof(KnownResamplers.Box),
nameof(KnownResamplers.Lanczos5), nameof(KnownResamplers.Lanczos5),
}; };
[Theory] private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
[WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)]
[WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 0.3f, null, null)] [Theory(
[WithFileCollection(nameof(CommonTestImages), nameof(SmokeTestResamplerNames), DefaultPixelType, 1.8f, null, null)] Skip = "Debug only, enable manually"
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 0.5f, null, null)] )]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 100, 100, DefaultPixelType, 1f, null, null)] [WithTestPatternImages(4000, 4000, PixelTypes.Rgba32, 300, 1024)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 50, 50, DefaultPixelType, 8f, null, null)] [WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 1024)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 201, 199, DefaultPixelType, null, 100, 99)] [WithTestPatternImages(3032, 3032, PixelTypes.Rgba32, 400, 128)]
[WithTestPatternImages(nameof(SmokeTestResamplerNames), 301, 1180, DefaultPixelType, null, 300, 480)] public void LargeImage<TPixel>(TestImageProvider<TPixel> provider, int destSize, int workingBufferSizeHintInKilobytes)
[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> 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 provider.Configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInKilobytes * 1024;
// 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); 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( using (Image<TPixel> image = provider.GetImage())
ctx => {
{ var destSize = new Size(image.Width * wN / wD, image.Height * hN / hD);
image.Mutate(x => x.Resize(destSize, KnownResamplers.Bicubic, false));
SizeF newSize; FormattableString outputInfo = $"({wN}÷{wD},{hN}÷{hD})";
string destSizeInfo; image.DebugSave(provider, outputInfo, appendPixelTypeToFileName: false);
if (ratio.HasValue) image.CompareToReferenceOutput(provider, outputInfo, appendPixelTypeToFileName: false);
{ }
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); private static readonly int SizeOfVector4 = Unsafe.SizeOf<Vector4>();
destSizeInfo = $"{newSize.Width}x{newSize.Height}";
}
FormattableString testOutputDetails = $"{samplerName}-{destSizeInfo}"; [Theory]
ctx.Apply( [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 50)]
img => img.DebugSave( [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 60)]
provider, [WithTestPatternImages(100, 400, PixelTypes.Rgba32, 110)]
$"{testOutputDetails}-ORIGINAL", [WithTestPatternImages(79, 97, PixelTypes.Rgba32, 73)]
appendPixelTypeToFileName: false)); [WithTestPatternImages(79, 97, PixelTypes.Rgba32, 5)]
ctx.Resize((Size)newSize, sampler, false); [WithTestPatternImages(47, 193, PixelTypes.Rgba32, 73)]
return testOutputDetails; [WithTestPatternImages(23, 211, PixelTypes.Rgba32, 31)]
}, public void WorkingBufferSizeHintInBytes_IsAppliedCorrectly<TPixel>(
comparer, TestImageProvider<TPixel> provider,
appendPixelTypeToFileName: false); 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] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)] [WithTestPatternImages(100, 100, DefaultPixelType)]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)] public void Resize_Compand<TPixel>(TestImageProvider<TPixel> provider)
[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> where TPixel : struct, IPixel<TPixel>
{ {
provider.Configuration.MaxDegreeOfParallelism = using (Image<TPixel> image = provider.GetImage())
maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount; {
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( provider.RunValidatingProcessorTest(
x => x.Resize(x.GetCurrentSize() / 2), x => x.Resize(x.GetCurrentSize() / 2, compand),
details, details,
appendPixelTypeToFileName: false, appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false); appendSourceFileOrDescription: false);
} }
[Theory] [Theory]
[WithTestPatternImages(100, 100, DefaultPixelType)] [WithFile(TestImages.Gif.Giphy, DefaultPixelType)]
public void Resize_Compand<TPixel>(TestImageProvider<TPixel> provider) public void Resize_IsAppliedToAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) 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); // Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :(
image.CompareToReferenceOutput(ValidatorComparer, provider); 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)) using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height))
{ {
Assert.ThrowsAny<Exception>( 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] [Theory]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, false)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)]
[WithFile(TestImages.Png.Kaboom, DefaultPixelType, true)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)]
public void Resize_DoesNotBleedAlphaPixels<TPixel>(TestImageProvider<TPixel> provider, bool compand) [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> where TPixel : struct, IPixel<TPixel>
{ {
string details = compand ? "Compand" : ""; provider.Configuration.MaxDegreeOfParallelism =
maxDegreeOfParallelism > 0 ? maxDegreeOfParallelism : Environment.ProcessorCount;
FormattableString details = $"MDP{maxDegreeOfParallelism}";
provider.RunValidatingProcessorTest( provider.RunValidatingProcessorTest(
x => x.Resize(x.GetCurrentSize() / 2, compand), x => x.Resize(x.GetCurrentSize() / 2),
details, details,
appendPixelTypeToFileName: false, appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false); appendSourceFileOrDescription: false);
} }
[Theory] [Theory]
[WithFile(TestImages.Gif.Giphy, DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplerNames), DefaultPixelType, 0.5f, null, null)]
public void Resize_IsAppliedToAllFrames<TPixel>(TestImageProvider<TPixel> provider) [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> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) IResampler sampler = TestUtils.GetResampler(samplerName);
{
image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.Bicubic));
// Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :( // NeirestNeighbourResampler is producing slightly different results With classic .NET framework on 32bit
image.DebugSave(provider, extension: Extensions.Gif); // 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] [Theory]
@ -196,10 +324,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
using (Image<TPixel> image = provider.GetImage()) 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); 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.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider); image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -208,26 +347,39 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWidthAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider) public void ResizeHeightAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) 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.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, 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] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeHeightAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWidthAndKeepAspect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) 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.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, 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] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithCropWidthMode<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWithBoxPadMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions 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)); image.Mutate(x => x.Resize(options));
@ -286,10 +425,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions { Size = new Size(image.Width, image.Height / 2) };
{
Size = new Size(image.Width, image.Height / 2)
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -300,16 +436,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithPadMode<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWithCropWidthMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions { Size = new Size(image.Width / 2, image.Height) };
{
Size = new Size(image.Width + 200, image.Height),
Mode = ResizeMode.Pad
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -320,16 +452,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithBoxPadMode<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWithMaxMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions { Size = new Size(300, 300), Mode = ResizeMode.Max };
{
Size = new Size(image.Width + 200, image.Height + 200),
Mode = ResizeMode.BoxPad
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -340,16 +468,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithMaxMode<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWithMinMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions
{ {
Size = new Size(300, 300), Size = new Size(
Mode = ResizeMode.Max (int)Math.Round(image.Width * .75F),
}; (int)Math.Round(image.Height * .95F)),
Mode = ResizeMode.Min
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -360,16 +490,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), DefaultPixelType)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType)]
public void ResizeWithMinMode<TPixel>(TestImageProvider<TPixel> provider) public void ResizeWithPadMode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions
{ {
Size = new Size((int)Math.Round(image.Width * .75F), (int)Math.Round(image.Height * .95F)), Size = new Size(image.Width + 200, image.Height), Mode = ResizeMode.Pad
Mode = ResizeMode.Min };
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -386,10 +515,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
var options = new ResizeOptions var options = new ResizeOptions
{ {
Size = new Size(image.Width / 2, image.Height), Size = new Size(image.Width / 2, image.Height), Mode = ResizeMode.Stretch
Mode = ResizeMode.Stretch };
};
image.Mutate(x => x.Resize(options)); image.Mutate(x => x.Resize(options));
@ -397,61 +525,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
image.CompareToReferenceOutput(ValidatorComparer, provider); 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; this.Height = height;
} }
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public BlankProvider() public BlankProvider()
{ {
this.Width = 100; this.Width = 100;
@ -32,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests
protected int Width { get; private set; } 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) 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)); Image<TPixel> cachedImage = cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone(); return cachedImage.Clone(this.Configuration);
} }
public override void Deserialize(IXunitSerializationInfo info) 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; this.a = a;
} }
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public SolidProvider() public SolidProvider()
: base() : 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 MethodName { get; private set; }
public string OutputSubfolderName { 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( public static TestImageProvider<TPixel> TestPattern(
int width, int width,
int height, int height,
@ -100,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests
/// </summary> /// </summary>
public Image<TPixel> GetImage(Action<IImageProcessingContext<TPixel>> operationsToApply) public Image<TPixel> GetImage(Action<IImageProcessingContext<TPixel>> operationsToApply)
{ {
Image<TPixel> img = GetImage(); Image<TPixel> img = this.GetImage();
img.Mutate(operationsToApply); img.Mutate(operationsToApply);
return img; return img;
} }

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

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

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

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

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

@ -1,5 +1,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.Memory; using SixLabors.Memory;
@ -8,6 +10,8 @@ namespace SixLabors.ImageSharp.Tests.Memory
{ {
internal class TestMemoryAllocator : MemoryAllocator internal class TestMemoryAllocator : MemoryAllocator
{ {
private List<AllocationRequest> allocationLog = new List<AllocationRequest>();
public TestMemoryAllocator(byte dirtyValue = 42) public TestMemoryAllocator(byte dirtyValue = 42)
{ {
this.DirtyValue = dirtyValue; this.DirtyValue = dirtyValue;
@ -18,10 +22,11 @@ namespace SixLabors.ImageSharp.Tests.Memory
/// </summary> /// </summary>
public byte DirtyValue { get; } public byte DirtyValue { get; }
public IList<AllocationRequest> AllocationLog => this.allocationLog;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{ {
T[] array = this.AllocateArray<T>(length, options); T[] array = this.AllocateArray<T>(length, options);
return new BasicArrayBuffer<T>(array, length); return new BasicArrayBuffer<T>(array, length);
} }
@ -34,6 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Memory
private T[] AllocateArray<T>(int length, AllocationOptions options) private T[] AllocateArray<T>(int length, AllocationOptions options)
where T : struct where T : struct
{ {
this.allocationLog.Add(AllocationRequest.Create<T>(options, length));
var array = new T[length + 42]; var array = new T[length + 42];
if (options == AllocationOptions.None) if (options == AllocationOptions.None)
@ -44,6 +50,35 @@ namespace SixLabors.ImageSharp.Tests.Memory
return array; 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> /// <summary>
/// Wraps an array as an <see cref="IManagedByteBuffer"/> instance. /// 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;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests namespace SixLabors.ImageSharp.Tests
{ {
public class TestImageProviderTests 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; public TestImageProviderTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
[Theory] /// <summary>
[WithBlankImages(1, 1, PixelTypes.Rgba32)] /// Need to us <see cref="GenericFactory{TPixel}"/> to create instance of <see cref="Image"/> when pixelType is StandardImageClass
public void NoOutputSubfolderIsPresentByDefault<TPixel>(TestImageProvider<TPixel> provider) /// </summary>
where TPixel : struct, IPixel<TPixel> => Assert.Empty(provider.Utility.OutputSubfolderName); /// <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] [Theory]
[WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")] [MemberData(nameof(BasicData))]
public void Use_WithEmptyImageAttribute<TPixel>(TestImageProvider<TPixel> provider, string message) public void Blank_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
Image<TPixel> img = provider.GetImage(); Image<TPixel> img = provider.GetImage();
Assert.Equal(42, img.Width); Assert.True(img.Width * img.Height > 0);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
} }
[Theory] [Theory]
[WithBlankImages(42, 666, PixelTypes.All, "hello")] [MemberData(nameof(FileData))]
public void Use_WithBlankImagesAttribute_WithAllPixelTypes<TPixel>( public void File_MemberData<TPixel>(TestImageProvider<TPixel> provider)
TestImageProvider<TPixel> provider,
string message)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
this.Output.WriteLine("SRC: " + provider.Utility.SourceFileOrDescription);
this.Output.WriteLine("OUT: " + provider.Utility.GetTestOutputFileName());
Image<TPixel> img = provider.GetImage(); Image<TPixel> img = provider.GetImage();
Assert.Equal(42, img.Width); Assert.True(img.Width * img.Height > 0);
Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
} }
[Theory] [Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32, PixelTypes.Rgba32)] [WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)]
[WithBlankImages(1, 1, PixelTypes.Alpha8, PixelTypes.Alpha8)] public void GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache<TPixel>(
[WithBlankImages(1, 1, PixelTypes.Argb32, PixelTypes.Argb32)] TestImageProvider<TPixel> provider)
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)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
if (!TestEnvironment.Is64BitProcess)
{
// We don't cache with the 32 bit build.
return;
}
Assert.NotNull(provider.Utility.SourceFileOrDescription); 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"); var decoder = new TestDecoder();
this.Output.WriteLine(fn); decoder.InitCaller(testName);
}
private class TestDecoder : IImageDecoder provider.GetImage(decoder);
{ Assert.Equal(1, TestDecoder.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);
}
// 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) var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 };
{ decoder1.InitCaller(testName);
this.callerName = name;
invocationCounts[name] = 0;
}
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) provider.GetImage(decoder2);
{ Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName));
lock (Monitor) });
{
action();
}
}
} }
[Theory] [Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] [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> where TPixel : struct, IPixel<TPixel>
{ {
if (!TestEnvironment.Is64BitProcess) if (!TestEnvironment.Is64BitProcess)
@ -120,121 +143,122 @@ namespace SixLabors.ImageSharp.Tests
Assert.NotNull(provider.Utility.SourceFileOrDescription); Assert.NotNull(provider.Utility.SourceFileOrDescription);
TestDecoder.DoTestThreadSafe(() => TestDecoderWithParameters.DoTestThreadSafe(
{ () =>
string testName = nameof(this.GetImage_WithCustomParameterlessDecoder_ShouldUtilizeCache); {
string testName = nameof(this
.GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual);
var decoder = new TestDecoder(); var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
decoder.InitCaller(testName); decoder1.InitCaller(testName);
provider.GetImage(decoder); var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 };
Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); decoder2.InitCaller(testName);
provider.GetImage(decoder); provider.GetImage(decoder1);
Assert.Equal(1, TestDecoder.GetInvocationCount(testName)); Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
});
}
private class TestDecoderWithParameters : IImageDecoder
{
public string Param1 { get; set; }
public int Param2 { get; set; }
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream) provider.GetImage(decoder2);
where TPixel : struct, IPixel<TPixel> Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
{ });
invocationCounts[this.callerName]++; }
return new Image<TPixel>(42, 42);
}
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; string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image);
invocationCounts[name] = 0;
}
internal static int GetInvocationCount(string callerName) => invocationCounts[callerName];
private static readonly object Monitor = new object();
public static void DoTestThreadSafe(Action action) Assert.True(files.Length > 2);
{ foreach (string path in files)
lock (Monitor)
{ {
action(); this.Output.WriteLine(path);
Assert.True(File.Exists(path));
} }
} }
} }
[Theory] [Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] [WithBasicTestPatternImages(50, 100, PixelTypes.Rgba32)]
public void GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual<TPixel>(TestImageProvider<TPixel> provider) [WithBasicTestPatternImages(49, 17, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(20, 10, PixelTypes.Rgba32)]
public void Use_WithBasicTestPatternImages<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
if (!TestEnvironment.Is64BitProcess) using (Image<TPixel> img = provider.GetImage())
{ {
// We don't cache with the 32 bit build. img.DebugSave(provider);
return;
} }
}
Assert.NotNull(provider.Utility.SourceFileOrDescription); [Theory]
[WithBlankImages(42, 666, PixelTypes.All, "hello")]
TestDecoderWithParameters.DoTestThreadSafe(() => public void Use_WithBlankImagesAttribute_WithAllPixelTypes<TPixel>(
{ TestImageProvider<TPixel> provider,
string testName = string message)
nameof(this.GetImage_WithCustomParametricDecoder_ShouldUtilizeCache_WhenParametersAreEqual); where TPixel : struct, IPixel<TPixel>
{
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; Image<TPixel> img = provider.GetImage();
decoder1.InitCaller(testName);
var decoder2 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 666 }; Assert.Equal(42, img.Width);
decoder2.InitCaller(testName); Assert.Equal(666, img.Height);
Assert.Equal("hello", message);
}
provider.GetImage(decoder1); [Theory]
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); [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(42, img.Width);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName)); Assert.Equal(666, img.Height);
}); Assert.Equal("hello", message);
} }
[Theory] [Theory]
[WithFile(TestImages.Bmp.F, PixelTypes.Rgba32)] [WithFile(TestImages.Bmp.Car, PixelTypes.All, 123)]
public void GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual<TPixel>(TestImageProvider<TPixel> provider) [WithFile(TestImages.Bmp.F, PixelTypes.All, 123)]
public void Use_WithFileAttribute<TPixel>(TestImageProvider<TPixel> provider, int yo)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
Assert.NotNull(provider.Utility.SourceFileOrDescription); Assert.NotNull(provider.Utility.SourceFileOrDescription);
using (Image<TPixel> img = provider.GetImage())
TestDecoderWithParameters.DoTestThreadSafe(() =>
{ {
string testName = Assert.True(img.Width * img.Height > 0);
nameof(this.GetImage_WithCustomParametricDecoder_ShouldNotUtilizeCache_WhenParametersAreNotEqual);
var decoder1 = new TestDecoderWithParameters() { Param1 = "Lol", Param2 = 42 }; Assert.Equal(123, yo);
decoder1.InitCaller(testName);
var decoder2 = new TestDecoderWithParameters() { Param1 = "LoL", Param2 = 42 }; string fn = provider.Utility.GetTestOutputFileName("jpg");
decoder2.InitCaller(testName); this.Output.WriteLine(fn);
}
provider.GetImage(decoder1);
Assert.Equal(1, TestDecoderWithParameters.GetInvocationCount(testName));
provider.GetImage(decoder2);
Assert.Equal(2, TestDecoderWithParameters.GetInvocationCount(testName));
});
} }
[Theory]
public static string[] AllBmpFiles = [WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)]
{ public void Use_WithFileAttribute_CustomConfig<TPixel>(TestImageProvider<TPixel> provider)
TestImages.Bmp.F, where TPixel : struct, IPixel<TPixel>
TestImages.Bmp.Bit8 {
}; EnsureCustomConfigurationIsApplied(provider);
}
[Theory] [Theory]
[WithFileCollection(nameof(AllBmpFiles), PixelTypes.Rgba32 | PixelTypes.Argb32)] [WithFileCollection(nameof(AllBmpFiles), PixelTypes.Rgba32 | PixelTypes.Argb32)]
@ -249,20 +273,15 @@ namespace SixLabors.ImageSharp.Tests
} }
[Theory] [Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] [WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)]
public void SaveTestOutputFileMultiFrame<TPixel>(TestImageProvider<TPixel> provider) public void Use_WithMemberFactoryAttribute<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> 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.IsType<Image<Rgba32>>(img);
Assert.True(files.Length > 2);
foreach (string path in files)
{
this.Output.WriteLine(path);
Assert.True(File.Exists(path));
}
} }
} }
@ -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] [Theory]
[WithMemberFactory(nameof(CreateTestImage), PixelTypes.All)] [WithTestPatternImages(49, 20, PixelTypes.Rgba32)]
public void Use_WithMemberFactoryAttribute<TPixel>(TestImageProvider<TPixel> provider) public void Use_WithTestPatternImages<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
Image<TPixel> img = provider.GetImage(); using (Image<TPixel> img = provider.GetImage())
Assert.Equal(3, img.Width);
if (provider.PixelType == PixelTypes.Rgba32)
{ {
Assert.IsType<Image<Rgba32>>(img); img.DebugSave(provider);
} }
} }
[Theory] [Theory]
[WithTestPatternImages(49,20, PixelTypes.Rgba32)] [WithTestPatternImages(20, 20, PixelTypes.Rgba32)]
public void Use_WithTestPatternImages<TPixel>(TestImageProvider<TPixel> provider) public void Use_WithTestPatternImages_CustomConfiguration<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> 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), // Couldn't make xUnit happy without this hackery:
TestImageProvider<HalfVector4>.Blank(
10,
20),
};
[Theory] private static readonly ConcurrentDictionary<string, int> invocationCounts =
[MemberData(nameof(BasicData))] new ConcurrentDictionary<string, int>();
public void Blank_MemberData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Image<TPixel> img = provider.GetImage();
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), private static readonly ConcurrentDictionary<string, int> invocationCounts =
TestImageProvider<HalfVector4>.File(TestImages.Bmp.F) new ConcurrentDictionary<string, int>();
};
[Theory] private static readonly object Monitor = new object();
[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(); 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