diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs
index 8720140e1..0c5b05180 100644
--- a/src/ImageSharp/Common/Helpers/ImageMaths.cs
+++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs
@@ -5,6 +5,7 @@ using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
@@ -100,7 +101,6 @@ namespace SixLabors.ImageSharp
///
/// Determine the Least Common Multiple (LCM) of two numbers.
- /// TODO: This method might be useful for building a more compact
///
public static int LeastCommonMultiple(int a, int b)
{
diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs
new file mode 100644
index 000000000..5347efcc0
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/TolerantMath.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp
+{
+ ///
+ /// Implements basic math operations using tolerant comparison
+ /// whenever an equality check is needed.
+ ///
+ internal readonly struct TolerantMath
+ {
+ private readonly double epsilon;
+
+ private readonly double negEpsilon;
+
+ public TolerantMath(double epsilon)
+ {
+ DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon));
+
+ this.epsilon = epsilon;
+ this.negEpsilon = -epsilon;
+ }
+
+ public static TolerantMath Default { get; } = new TolerantMath(1e-8);
+
+ ///
+ /// == 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon;
+
+ ///
+ /// > 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsPositive(double a) => a > this.epsilon;
+
+ ///
+ /// < 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsNegative(double a) => a < this.negEpsilon;
+
+ ///
+ /// ==
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool AreEqual(double a, double b) => this.IsZero(a - b);
+
+ ///
+ /// >
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsGreater(double a, double b) => a > b + this.epsilon;
+
+ ///
+ /// <
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsLess(double a, double b) => a < b - this.epsilon;
+
+ ///
+ /// >=
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon;
+
+ ///
+ /// <=
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon;
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public double Ceiling(double a)
+ {
+ double rem = Math.IEEERemainder(a, 1);
+ if (this.IsZero(rem))
+ {
+ return Math.Round(a);
+ }
+
+ return Math.Ceiling(a);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public double Floor(double a)
+ {
+ double rem = Math.IEEERemainder(a, 1);
+ if (this.IsZero(rem))
+ {
+ return Math.Round(a);
+ }
+
+ return Math.Floor(a);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/ImageSharp.csproj.DotSettings b/src/ImageSharp/ImageSharp.csproj.DotSettings
index cd75f91b7..a7337240a 100644
--- a/src/ImageSharp/ImageSharp.csproj.DotSettings
+++ b/src/ImageSharp/ImageSharp.csproj.DotSettings
@@ -5,4 +5,6 @@
True
True
True
- True
\ No newline at end of file
+ True
+ True
+ True
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs
deleted file mode 100644
index 277be53ff..000000000
--- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the Apache License, Version 2.0.
-
-using System;
-using System.Runtime.CompilerServices;
-
-using SixLabors.ImageSharp.Memory;
-using SixLabors.Memory;
-
-namespace SixLabors.ImageSharp.Processing.Processors.Transforms
-{
- ///
- /// Holds the values in an optimized contigous memory region.
- ///
- internal class KernelMap : IDisposable
- {
- private readonly Buffer2D data;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The to use for allocations.
- /// The size of the destination window
- /// The radius of the kernel
- public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius)
- {
- int width = (int)Math.Ceiling(kernelRadius * 2);
- this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean);
- this.Kernels = new ResizeKernel[destinationSize];
- }
-
- ///
- /// Gets the calculated values.
- ///
- public ResizeKernel[] Kernels { get; }
-
- ///
- /// Disposes instance releasing it's backing buffer.
- ///
- public void Dispose()
- {
- this.data.Dispose();
- }
-
- ///
- /// Computes the weights to apply at each pixel when resizing.
- ///
- /// The
- /// The destination size
- /// The source size
- /// The to use for buffer allocations
- /// The
- public static KernelMap Calculate(
- IResampler sampler,
- int destinationSize,
- int sourceSize,
- MemoryAllocator memoryAllocator)
- {
- float ratio = (float)sourceSize / destinationSize;
- float scale = ratio;
-
- if (scale < 1F)
- {
- scale = 1F;
- }
-
- float radius = MathF.Ceiling(scale * sampler.Radius);
- var result = new KernelMap(memoryAllocator, destinationSize, radius);
-
- for (int i = 0; i < destinationSize; i++)
- {
- float center = ((i + .5F) * ratio) - .5F;
-
- // Keep inside bounds.
- int left = (int)MathF.Ceiling(center - radius);
- if (left < 0)
- {
- left = 0;
- }
-
- int right = (int)MathF.Floor(center + radius);
- if (right > sourceSize - 1)
- {
- right = sourceSize - 1;
- }
-
- float sum = 0;
-
- ResizeKernel ws = result.CreateKernel(i, left, right);
- result.Kernels[i] = ws;
-
- ref float weightsBaseRef = ref ws.GetStartReference();
-
- for (int j = left; j <= right; j++)
- {
- float weight = sampler.GetValue((j - center) / scale);
- sum += weight;
-
- // weights[j - left] = weight:
- Unsafe.Add(ref weightsBaseRef, j - left) = weight;
- }
-
- // Normalize, best to do it here rather than in the pixel loop later on.
- if (sum > 0)
- {
- for (int w = 0; w < ws.Length; w++)
- {
- // weights[w] = weights[w] / sum:
- ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w);
- wRef /= sum;
- }
- }
- }
-
- return result;
- }
-
- ///
- /// Slices a weights value at the given positions.
- ///
- /// The index in destination buffer
- /// The local left index value
- /// The local right index value
- /// The weights
- private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx)
- {
- return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1);
- }
- }
-}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs
similarity index 100%
rename from src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs
diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
similarity index 51%
rename from src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
index cc3c20453..f349634ac 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
@@ -2,82 +2,62 @@
// 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.Memory;
-
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
///
- /// Points to a collection of of weights allocated in .
+ /// Points to a collection of of weights allocated in .
///
- internal struct ResizeKernel
+ internal readonly unsafe struct ResizeKernel
{
- ///
- /// The local left index position
- ///
- public int Left;
-
- ///
- /// The length of the weights window
- ///
- public int Length;
-
- ///
- /// The buffer containing the weights values.
- ///
- private readonly Memory buffer;
+ private readonly float* bufferPtr;
///
/// Initializes a new instance of the struct.
///
- /// The destination index in the buffer
- /// The local left index
- /// The span
- /// The length of the window
[MethodImpl(InliningOptions.ShortMethod)]
- internal ResizeKernel(int index, int left, Buffer2D buffer, int length)
+ internal ResizeKernel(int left, float* bufferPtr, int length)
{
- int flatStartIndex = index * buffer.Width;
this.Left = left;
- this.buffer = buffer.MemorySource.Memory.Slice(flatStartIndex, length);
+ this.bufferPtr = bufferPtr;
this.Length = length;
}
///
- /// Gets a reference to the first item of the window.
+ /// Gets the left index for the destination row
///
- /// The reference to the first item of the window
- [MethodImpl(InliningOptions.ShortMethod)]
- public ref float GetStartReference()
- {
- Span span = this.buffer.Span;
- return ref span[0];
- }
+ public int Left { get; }
///
- /// Gets the span representing the portion of the that this window covers
+ /// Gets the the length of the kernel
///
- /// The
- [MethodImpl(InliningOptions.ShortMethod)]
- public Span GetSpan() => this.buffer.Span;
+ public int Length { get; }
+
+ ///
+ /// Gets the span representing the portion of the that this window covers
+ ///
+ /// The
+ ///
+ public Span Values
+ {
+ [MethodImpl(InliningOptions.ShortMethod)]
+ get => new Span(this.bufferPtr, this.Length);
+ }
///
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance.
///
/// The input span of vectors
- /// The source row position.
/// The weighted sum
[MethodImpl(InliningOptions.ShortMethod)]
- public Vector4 Convolve(Span rowSpan, int sourceX)
+ public Vector4 Convolve(Span rowSpan)
{
- ref float horizontalValues = ref this.GetStartReference();
+ ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr);
int left = this.Left;
- ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left + sourceX);
+ ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left);
// Destination color components
Vector4 result = Vector4.Zero;
@@ -91,5 +71,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return result;
}
+
+ ///
+ /// Copy the contents of altering
+ /// to the value .
+ ///
+ internal ResizeKernel AlterLeftValue(int left)
+ {
+ return new ResizeKernel(left, this.bufferPtr, this.Length);
+ }
+
+ internal void Fill(Span values)
+ {
+ DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!");
+
+ for (int i = 0; i < this.Length; i++)
+ {
+ this.Values[i] = (float)values[i];
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
new file mode 100644
index 000000000..4b81aaa64
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Transforms
+{
+ ///
+ /// Contains
+ ///
+ internal partial class ResizeKernelMap
+ {
+ ///
+ /// Memory-optimized where repeating rows are stored only once.
+ ///
+ private sealed class PeriodicKernelMap : ResizeKernelMap
+ {
+ private readonly int period;
+
+ private readonly int cornerInterval;
+
+ public PeriodicKernelMap(
+ MemoryAllocator memoryAllocator,
+ IResampler sampler,
+ int sourceLength,
+ int destinationLength,
+ double ratio,
+ double scale,
+ int radius,
+ int period,
+ int cornerInterval)
+ : base(
+ memoryAllocator,
+ sampler,
+ sourceLength,
+ destinationLength,
+ (cornerInterval * 2) + period,
+ ratio,
+ scale,
+ radius)
+ {
+ this.cornerInterval = cornerInterval;
+ this.period = period;
+ }
+
+ internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}";
+
+ protected override void Initialize()
+ {
+ // Build top corner data + one period of the mosaic data:
+ int startOfFirstRepeatedMosaic = this.cornerInterval + this.period;
+
+ for (int i = 0; i < startOfFirstRepeatedMosaic; i++)
+ {
+ ResizeKernel kernel = this.BuildKernel(i, i);
+ this.kernels[i] = kernel;
+ }
+
+ // Copy the mosaics:
+ int bottomStartDest = this.DestinationLength - this.cornerInterval;
+ for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++)
+ {
+ double center = ((i + .5) * this.ratio) - .5;
+ int left = (int)TolerantMath.Ceiling(center - this.radius);
+ ResizeKernel kernel = this.kernels[i - this.period];
+ this.kernels[i] = kernel.AlterLeftValue(left);
+ }
+
+ // Build bottom corner data:
+ int bottomStartData = this.cornerInterval + this.period;
+ for (int i = 0; i < this.cornerInterval; i++)
+ {
+ ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i);
+ this.kernels[bottomStartDest + i] = kernel;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
new file mode 100644
index 000000000..2ab574df2
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
@@ -0,0 +1,247 @@
+// 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 SixLabors.ImageSharp.Memory;
+using SixLabors.Memory;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Transforms
+{
+ ///
+ /// Provides values from an optimized,
+ /// contiguous memory region.
+ ///
+ internal partial class ResizeKernelMap : IDisposable
+ {
+ private static readonly TolerantMath TolerantMath = TolerantMath.Default;
+
+ private readonly IResampler sampler;
+
+ private readonly int sourceLength;
+
+ private readonly double ratio;
+
+ private readonly double scale;
+
+ private readonly int radius;
+
+ private readonly MemoryHandle pinHandle;
+
+ private readonly Buffer2D data;
+
+ private readonly ResizeKernel[] kernels;
+
+ // To avoid both GC allocations, and MemoryAllocator ceremony:
+ private readonly double[] tempValues;
+
+ private ResizeKernelMap(
+ MemoryAllocator memoryAllocator,
+ IResampler sampler,
+ int sourceLength,
+ int destinationLength,
+ int bufferHeight,
+ double ratio,
+ double scale,
+ int radius)
+ {
+ this.sampler = sampler;
+ this.ratio = ratio;
+ this.scale = scale;
+ this.radius = radius;
+ this.sourceLength = sourceLength;
+ this.DestinationLength = destinationLength;
+ int maxWidth = (radius * 2) + 1;
+ this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean);
+ this.pinHandle = this.data.Memory.Pin();
+ this.kernels = new ResizeKernel[destinationLength];
+ this.tempValues = new double[maxWidth];
+ }
+
+ ///
+ /// Gets the length of the destination row/column
+ ///
+ public int DestinationLength { get; }
+
+ ///
+ /// Gets a string of information to help debugging
+ ///
+ internal virtual string Info =>
+ $"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}";
+
+ ///
+ /// Disposes instance releasing it's backing buffer.
+ ///
+ public void Dispose()
+ {
+ this.pinHandle.Dispose();
+ this.data.Dispose();
+ }
+
+ ///
+ /// Returns a for an index value between 0 and DestinationSize - 1.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx];
+
+ ///
+ /// Computes the weights to apply at each pixel when resizing.
+ ///
+ /// The
+ /// The destination size
+ /// The source size
+ /// The to use for buffer allocations
+ /// The
+ public static ResizeKernelMap Calculate(
+ IResampler sampler,
+ int destinationSize,
+ int sourceSize,
+ MemoryAllocator memoryAllocator)
+ {
+ double ratio = (double)sourceSize / destinationSize;
+ double scale = ratio;
+
+ if (scale < 1)
+ {
+ scale = 1;
+ }
+
+ int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius);
+
+ // 'ratio' is a rational number.
+ // Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
+ // This value is determining the length of the periods in repeating kernel map rows.
+ int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize;
+
+ // the center position at i == 0:
+ double center0 = (ratio - 1) * 0.5;
+ double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio;
+
+ // The number of rows building a "stairway" at the top and the bottom of the kernel map
+ // corresponding to the corners of the image.
+ // If we do not normalize the kernel values, these rows also fit the periodic logic,
+ // however, it's just simpler to calculate them separately.
+ int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal);
+
+ // If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1
+ // instead of Ceiling:
+ if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval))
+ {
+ cornerInterval++;
+ }
+
+ // If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization.
+ // If we don't have at least 2 periods, we go with the basic implementation:
+ bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize;
+
+ ResizeKernelMap result = hasAtLeast2Periods
+ ? new PeriodicKernelMap(
+ memoryAllocator,
+ sampler,
+ sourceSize,
+ destinationSize,
+ ratio,
+ scale,
+ radius,
+ period,
+ cornerInterval)
+ : new ResizeKernelMap(
+ memoryAllocator,
+ sampler,
+ sourceSize,
+ destinationSize,
+ destinationSize,
+ ratio,
+ scale,
+ radius);
+
+ result.Initialize();
+
+ return result;
+ }
+
+ protected virtual void Initialize()
+ {
+ for (int i = 0; i < this.DestinationLength; i++)
+ {
+ ResizeKernel kernel = this.BuildKernel(i, i);
+ this.kernels[i] = kernel;
+ }
+ }
+
+ ///
+ /// Builds a for the row (in )
+ /// referencing the data at row within ,
+ /// so the data reusable by other data rows.
+ ///
+ private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex)
+ {
+ double center = ((destRowIndex + .5) * this.ratio) - .5;
+
+ // Keep inside bounds.
+ int left = (int)TolerantMath.Ceiling(center - this.radius);
+ if (left < 0)
+ {
+ left = 0;
+ }
+
+ int right = (int)TolerantMath.Floor(center + this.radius);
+ if (right > this.sourceLength - 1)
+ {
+ right = this.sourceLength - 1;
+ }
+
+ ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
+
+ Span kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length);
+ double sum = 0;
+
+ for (int j = left; j <= right; j++)
+ {
+ double value = this.sampler.GetValue((float)((j - center) / this.scale));
+ sum += value;
+
+ kernelValues[j - left] = value;
+ }
+
+ // Normalize, best to do it here rather than in the pixel loop later on.
+ if (sum > 0)
+ {
+ for (int j = 0; j < kernel.Length; j++)
+ {
+ // weights[w] = weights[w] / sum:
+ ref double kRef = ref kernelValues[j];
+ kRef /= sum;
+ }
+ }
+
+ kernel.Fill(kernelValues);
+
+ return kernel;
+ }
+
+ ///
+ /// Returns a referencing values of
+ /// at row .
+ ///
+ private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right)
+ {
+ int length = right - left + 1;
+
+ if (length > this.data.Width)
+ {
+ throw new InvalidOperationException(
+ $"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width");
+ }
+
+ Span rowSpan = this.data.GetRowSpan(dataRowIndex);
+
+ ref float rowReference = ref MemoryMarshal.GetReference(rowSpan);
+ float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference);
+ return new ResizeKernel(left, rowPtr, length);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
similarity index 95%
rename from src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
index 4d4ed06ce..189e21de7 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
@@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
where TPixel : struct, IPixel
{
// The following fields are not immutable but are optionally created on demand.
- private KernelMap horizontalKernelMap;
- private KernelMap verticalKernelMap;
+ private ResizeKernelMap horizontalKernelMap;
+ private ResizeKernelMap verticalKernelMap;
///
/// Initializes a new instance of the class.
@@ -165,13 +165,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
// Since all image frame dimensions have to be the same we can calculate this for all frames.
MemoryAllocator memoryAllocator = source.GetMemoryAllocator();
- this.horizontalKernelMap = KernelMap.Calculate(
+ this.horizontalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Width,
sourceRectangle.Width,
memoryAllocator);
- this.verticalKernelMap = KernelMap.Calculate(
+ this.verticalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Height,
sourceRectangle.Height,
@@ -254,8 +254,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
for (int y = rows.Min; y < rows.Max; y++)
{
- Span sourceRow = source.GetPixelRowSpan(y);
- Span tempRowSpan = tempRowBuffer.Span;
+ Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX);
+ Span tempRowSpan = tempRowBuffer.Span.Slice(sourceX);
PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan);
Vector4Utils.Premultiply(tempRowSpan);
@@ -269,9 +269,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int x = minX; x < maxX; x++)
{
- ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX];
+ ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX);
Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) =
- window.Convolve(tempRowSpan, sourceX);
+ kernel.Convolve(tempRowSpan);
}
}
});
@@ -289,16 +289,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int y = rows.Min; y < rows.Max; y++)
{
// Ensure offsets are normalized for cropping and padding.
- ResizeKernel window = this.verticalKernelMap.Kernels[y - startY];
+ ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY);
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan);
for (int x = 0; x < width; x++)
{
- Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x);
+ Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY);
// Destination color components
- Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn, sourceY);
+ Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn);
}
Vector4Utils.UnPremultiply(tempRowSpan);
diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs
index c0bb25a1b..02d4f80c5 100644
--- a/tests/ImageSharp.Sandbox46/Program.cs
+++ b/tests/ImageSharp.Sandbox46/Program.cs
@@ -63,8 +63,8 @@ namespace SixLabors.ImageSharp.Sandbox46
private static void RunDecodeJpegProfilingTests()
{
Console.WriteLine("RunDecodeJpegProfilingTests...");
- var benchmarks = new JpegBenchmarks(new ConsoleOutput());
- foreach (object[] data in JpegBenchmarks.DecodeJpegData)
+ var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput());
+ foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData)
{
string fileName = (string)data[0];
benchmarks.DecodeJpeg(fileName);
diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
index 75ef611a5..018fabd98 100644
--- a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
+++ b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
@@ -2,11 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers
{
- using Xunit;
-
public class ImageMathsTests
{
[Theory]
diff --git a/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
new file mode 100644
index 000000000..6c7a1f275
--- /dev/null
+++ b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+
+using Xunit;
+// ReSharper disable InconsistentNaming
+
+namespace SixLabors.ImageSharp.Tests.Helpers
+{
+ public class TolerantMathTests
+ {
+ private readonly TolerantMath tolerantMath = new TolerantMath(0.1);
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(0.01)]
+ [InlineData(-0.05)]
+ public void IsZero_WhenTrue(double a)
+ {
+ Assert.True(this.tolerantMath.IsZero(a));
+ }
+
+ [Theory]
+ [InlineData(0.11)]
+ [InlineData(-0.101)]
+ [InlineData(42)]
+ public void IsZero_WhenFalse(double a)
+ {
+ Assert.False(this.tolerantMath.IsZero(a));
+ }
+
+ [Theory]
+ [InlineData(0.11)]
+ [InlineData(100)]
+ public void IsPositive_WhenTrue(double a)
+ {
+ Assert.True(this.tolerantMath.IsPositive(a));
+ }
+
+ [Theory]
+ [InlineData(0.09)]
+ [InlineData(-0.1)]
+ [InlineData(-1000)]
+ public void IsPositive_WhenFalse(double a)
+ {
+ Assert.False(this.tolerantMath.IsPositive(a));
+ }
+
+ [Theory]
+ [InlineData(-0.11)]
+ [InlineData(-100)]
+ public void IsNegative_WhenTrue(double a)
+ {
+ Assert.True(this.tolerantMath.IsNegative(a));
+ }
+
+ [Theory]
+ [InlineData(-0.09)]
+ [InlineData(0.1)]
+ [InlineData(1000)]
+ public void IsNegative_WhenFalse(double a)
+ {
+ Assert.False(this.tolerantMath.IsNegative(a));
+ }
+
+ [Theory]
+ [InlineData(4.2, 4.2)]
+ [InlineData(4.2, 4.25)]
+ [InlineData(-Math.PI, -Math.PI + 0.05)]
+ [InlineData(999999.2, 999999.25)]
+ public void AreEqual_WhenTrue(double a, double b)
+ {
+ Assert.True(this.tolerantMath.AreEqual(a, b));
+ }
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(-1000000, -1000000.2)]
+ public void AreEqual_WhenFalse(double a, double b)
+ {
+ Assert.False(this.tolerantMath.AreEqual(a, b));
+ }
+
+ [Theory]
+ [InlineData(2, 1.8)]
+ [InlineData(-20, -20.2)]
+ [InlineData(0.1, -0.1)]
+ [InlineData(100, 10)]
+ public void IsGreater_IsLess_WhenTrue(double a, double b)
+ {
+ Assert.True(this.tolerantMath.IsGreater(a, b));
+ Assert.True(this.tolerantMath.IsLess(b, a));
+ }
+
+ [Theory]
+ [InlineData(2, 1.95)]
+ [InlineData(-20, -20.02)]
+ [InlineData(0.01, -0.01)]
+ [InlineData(999999, 999999.09)]
+ public void IsGreater_IsLess_WhenFalse(double a, double b)
+ {
+ Assert.False(this.tolerantMath.IsGreater(a, b));
+ Assert.False(this.tolerantMath.IsLess(b, a));
+ }
+
+ [Theory]
+ [InlineData(3, 2)]
+ [InlineData(3, 2.99)]
+ [InlineData(2.99, 3)]
+ [InlineData(-5, -6)]
+ [InlineData(-5, -5.05)]
+ [InlineData(-5.05, -5)]
+ public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b)
+ {
+ Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b));
+ Assert.True(this.tolerantMath.IsLessOrEqual(b, a));
+ }
+
+ [Theory]
+ [InlineData(2, 3)]
+ [InlineData(2.89, 3)]
+ [InlineData(-3, -2.89)]
+ public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b)
+ {
+ Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b));
+ Assert.False(this.tolerantMath.IsLessOrEqual(b, a));
+ }
+
+ [Theory]
+ [InlineData(3.5, 4.0)]
+ [InlineData(3.89, 4.0)]
+ [InlineData(4.09, 4.0)]
+ [InlineData(4.11, 5.0)]
+ [InlineData(0.11, 1)]
+ [InlineData(0.05, 0)]
+ [InlineData(-0.5, 0)]
+ [InlineData(-0.95, -1)]
+ [InlineData(-1.05, -1)]
+ [InlineData(-1.5, -1)]
+ public void Ceiling(double value, double expected)
+ {
+ double actual = this.tolerantMath.Ceiling(value);
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(1, 1)]
+ [InlineData(0.99, 1)]
+ [InlineData(0.5, 0)]
+ [InlineData(0.01, 0)]
+ [InlineData(-0.09, 0)]
+ [InlineData(-0.11, -1)]
+ [InlineData(-100.11, -101)]
+ [InlineData(-100.09, -100)]
+ public void Floor(double value, double expected)
+ {
+ double plz1 = Math.IEEERemainder(1.1, 1);
+ double plz2 = Math.IEEERemainder(0.9, 1);
+
+ double plz3 = Math.IEEERemainder(-1.1, 1);
+ double plz4 = Math.IEEERemainder(-0.9, 1);
+
+ double actual = this.tolerantMath.Floor(value);
+ Assert.Equal(expected, actual);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
deleted file mode 100644
index 1b4b3cf6a..000000000
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
-using SixLabors.ImageSharp.Processing.Processors.Transforms;
-using SixLabors.Primitives;
-
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
-{
- public class KernelMapTests
- {
- private ITestOutputHelper Output { get; }
-
- public KernelMapTests(ITestOutputHelper output)
- {
- this.Output = output;
- }
-
- [Theory(Skip = "TODO: Add asserionts")]
- [InlineData(500, 200, nameof(KnownResamplers.Bicubic))]
- [InlineData(50, 40, nameof(KnownResamplers.Bicubic))]
- [InlineData(40, 30, nameof(KnownResamplers.Bicubic))]
- [InlineData(500, 200, nameof(KnownResamplers.Lanczos8))]
- [InlineData(100, 80, nameof(KnownResamplers.Lanczos8))]
- [InlineData(100, 10, nameof(KnownResamplers.Lanczos8))]
- [InlineData(10, 100, nameof(KnownResamplers.Lanczos8))]
- public void PrintKernelMap(int srcSize, int destSize, string resamplerName)
- {
- var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null);
-
- var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
-
- var bld = new StringBuilder();
-
- foreach (ResizeKernel window in kernelMap.Kernels)
- {
- Span span = window.GetSpan();
- for (int i = 0; i < window.Length; i++)
- {
- float value = span[i];
- bld.Append($"{value,7:F4}");
- bld.Append("| ");
- }
-
- bld.AppendLine();
- }
-
- string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap));
- string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD";
-
- File.WriteAllText(fileName, bld.ToString());
-
- this.Output.WriteLine(bld.ToString());
- }
- }
-}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
new file mode 100644
index 000000000..7d842c4e1
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Collections.Generic;
+using System.Linq;
+
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
+
+namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
+{
+ public partial class ResizeKernelMapTests
+ {
+ ///
+ /// Simplified reference implementation for functionality.
+ ///
+ internal class ReferenceKernelMap
+ {
+ private readonly ReferenceKernel[] kernels;
+
+ public ReferenceKernelMap(ReferenceKernel[] kernels)
+ {
+ this.kernels = kernels;
+ }
+
+ public int DestinationSize => this.kernels.Length;
+
+ public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex];
+
+ public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true)
+ {
+ double ratio = (double)sourceSize / destinationSize;
+ double scale = ratio;
+
+ if (scale < 1F)
+ {
+ scale = 1F;
+ }
+
+ TolerantMath tolerantMath = TolerantMath.Default;
+
+ double radius = tolerantMath.Ceiling(scale * sampler.Radius);
+
+ var result = new List();
+
+ for (int i = 0; i < destinationSize; i++)
+ {
+ double center = ((i + .5) * ratio) - .5;
+
+ // Keep inside bounds.
+ int left = (int)tolerantMath.Ceiling(center - radius);
+ if (left < 0)
+ {
+ left = 0;
+ }
+
+ int right = (int)tolerantMath.Floor(center + radius);
+ if (right > sourceSize - 1)
+ {
+ right = sourceSize - 1;
+ }
+
+ double sum = 0;
+
+ double[] values = new double[right - left + 1];
+
+ for (int j = left; j <= right; j++)
+ {
+ double weight = sampler.GetValue((float)((j - center) / scale));
+ sum += weight;
+
+ values[j - left] = weight;
+ }
+
+ if (sum > 0 && normalize)
+ {
+ for (int w = 0; w < values.Length; w++)
+ {
+ values[w] /= sum;
+ }
+ }
+
+ float[] floatVals = values.Select(v => (float)v).ToArray();
+
+ result.Add(new ReferenceKernel(left, floatVals));
+ }
+
+ return new ReferenceKernelMap(result.ToArray());
+ }
+ }
+
+ internal struct ReferenceKernel
+ {
+ public ReferenceKernel(int left, float[] values)
+ {
+ this.Left = left;
+ this.Values = values;
+ }
+
+ public int Left { get; }
+
+ public float[] Values { get; }
+
+ public int Length => this.Values.Length;
+
+ public static implicit operator ReferenceKernel(ResizeKernel orig)
+ {
+ return new ReferenceKernel(orig.Left, orig.Values.ToArray());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
new file mode 100644
index 000000000..08b294913
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
@@ -0,0 +1,241 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
+
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
+{
+ public partial class ResizeKernelMapTests
+ {
+ private ITestOutputHelper Output { get; }
+
+ public ResizeKernelMapTests(ITestOutputHelper output)
+ {
+ this.Output = output;
+ }
+
+ ///
+ /// resamplerName, srcSize, destSize
+ ///
+ public static readonly TheoryData KernelMapData = new TheoryData
+ {
+ { nameof(KnownResamplers.Bicubic), 15, 10 },
+ { nameof(KnownResamplers.Bicubic), 10, 15 },
+ { nameof(KnownResamplers.Bicubic), 20, 20 },
+ { nameof(KnownResamplers.Bicubic), 50, 40 },
+ { nameof(KnownResamplers.Bicubic), 40, 50 },
+ { nameof(KnownResamplers.Bicubic), 500, 200 },
+ { nameof(KnownResamplers.Bicubic), 200, 500 },
+
+ { nameof(KnownResamplers.Bicubic), 10, 25 },
+
+ { nameof(KnownResamplers.Lanczos3), 16, 12 },
+ { nameof(KnownResamplers.Lanczos3), 12, 16 },
+ { nameof(KnownResamplers.Lanczos3), 12, 9 },
+ { nameof(KnownResamplers.Lanczos3), 9, 12 },
+ { nameof(KnownResamplers.Lanczos3), 6, 8 },
+ { nameof(KnownResamplers.Lanczos3), 8, 6 },
+ { nameof(KnownResamplers.Lanczos3), 20, 12 },
+
+ { nameof(KnownResamplers.Lanczos3), 5, 25 },
+ { nameof(KnownResamplers.Lanczos3), 5, 50 },
+
+ { nameof(KnownResamplers.Lanczos3), 25, 5 },
+ { nameof(KnownResamplers.Lanczos3), 50, 5 },
+ { nameof(KnownResamplers.Lanczos3), 49, 5 },
+ { nameof(KnownResamplers.Lanczos3), 31, 5 },
+
+ { nameof(KnownResamplers.Lanczos8), 500, 200 },
+ { nameof(KnownResamplers.Lanczos8), 100, 10 },
+ { nameof(KnownResamplers.Lanczos8), 100, 80 },
+ { nameof(KnownResamplers.Lanczos8), 10, 100 },
+
+ // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
+ { nameof(KnownResamplers.Box), 378, 149 },
+ { nameof(KnownResamplers.Box), 349, 174 },
+
+ // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
+ { nameof(KnownResamplers.Box), 201, 100 },
+ { nameof(KnownResamplers.Box), 199, 99 },
+ { nameof(KnownResamplers.Box), 10, 299 },
+ { nameof(KnownResamplers.Box), 299, 10 },
+ { nameof(KnownResamplers.Box), 301, 300 },
+ { nameof(KnownResamplers.Box), 1180, 480 },
+
+ { nameof(KnownResamplers.Lanczos2), 3264, 3032 },
+
+ { nameof(KnownResamplers.Bicubic), 1280, 2240 },
+ { nameof(KnownResamplers.Bicubic), 1920, 1680 },
+ { nameof(KnownResamplers.Bicubic), 3072, 2240 },
+
+ { nameof(KnownResamplers.Welch), 300, 2008 },
+
+ // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
+ { nameof(KnownResamplers.Bicubic), 10, 50 },
+ { nameof(KnownResamplers.Bicubic), 49, 301 },
+ { nameof(KnownResamplers.Bicubic), 301, 49 },
+ { nameof(KnownResamplers.Bicubic), 1680, 1200 },
+ { nameof(KnownResamplers.Box), 13, 299 },
+ { nameof(KnownResamplers.Lanczos5), 3032, 600 },
+ };
+
+ public static TheoryData GeneratedImageResizeData =
+ GenerateImageResizeData();
+
+
+ [Theory(Skip = "Only for debugging and development")]
+ [MemberData(nameof(KernelMapData))]
+ public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize)
+ {
+ IResampler resampler = TestUtils.GetResampler(resamplerName);
+
+ var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false);
+
+ this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
+ }
+
+ [Theory]
+ [MemberData(nameof(KernelMapData))]
+ public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
+ {
+ this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
+ }
+
+ // Comprehensive but expensive tests, for ResizeKernelMap.
+ // Enabling them can kill you, but sometimes you have to wear the burden!
+ // AppVeyor will never follow you to these shadows of Mordor.
+#if false
+ [Theory]
+ [MemberData(nameof(GeneratedImageResizeData))]
+ public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize)
+ {
+ this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
+ }
+#endif
+
+ private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
+ {
+ IResampler resampler = TestUtils.GetResampler(resamplerName);
+
+ var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
+ var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
+
+#if DEBUG
+ this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
+ this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
+#endif
+ var comparer = new ApproximateFloatComparer(1e-6f);
+
+ for (int i = 0; i < kernelMap.DestinationLength; i++)
+ {
+ ResizeKernel kernel = kernelMap.GetKernel(i);
+
+ ReferenceKernel referenceKernel = referenceMap.GetKernel(i);
+
+ Assert.True(
+ referenceKernel.Length == kernel.Length,
+ $"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}");
+ Assert.True(
+ referenceKernel.Left == kernel.Left,
+ $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}");
+ float[] expectedValues = referenceKernel.Values;
+ Span actualValues = kernel.Values;
+
+ Assert.Equal(expectedValues.Length, actualValues.Length);
+
+
+
+ for (int x = 0; x < expectedValues.Length; x++)
+ {
+ Assert.True(
+ comparer.Equals(expectedValues[x], actualValues[x]),
+ $"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})");
+ }
+ }
+ }
+
+ private static string PrintKernelMap(ResizeKernelMap kernelMap) =>
+ PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i));
+
+ private static string PrintKernelMap(ReferenceKernelMap kernelMap) =>
+ PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i));
+
+ private static string PrintKernelMap(
+ TKernelMap kernelMap,
+ Func getDestinationSize,
+ Func getKernel)
+ {
+ var bld = new StringBuilder();
+
+ if (kernelMap is ResizeKernelMap actualMap)
+ {
+ bld.AppendLine(actualMap.Info);
+ }
+
+ int destinationSize = getDestinationSize(kernelMap);
+
+ for (int i = 0; i < destinationSize; i++)
+ {
+ ReferenceKernel kernel = getKernel(kernelMap, i);
+ bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || ");
+ Span span = kernel.Values;
+
+ for (int j = 0; j < kernel.Length; j++)
+ {
+ float value = span[j];
+ bld.Append($"{value,8:F5}");
+ bld.Append(" | ");
+ }
+
+ bld.AppendLine();
+ }
+
+ return bld.ToString();
+ }
+
+
+ private static TheoryData GenerateImageResizeData()
+ {
+ var result = new TheoryData();
+
+ string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static)
+ .Select(p => p.Name)
+ .Where(name => name != nameof(KnownResamplers.NearestNeighbor))
+ .ToArray();
+
+ int[] dimensionVals =
+ {
+ // Arbitrary, small dimensions:
+ 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301,
+
+ // Typical image sizes:
+ 640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560,
+ 1920, 3032, 2008, 3072, 2304, 3264, 2448
+ };
+
+ IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals
+ .SelectMany(s => dimensionVals.Select(d => (s, d)))
+ .OrderBy(x => x.s + x.d);
+
+ foreach (string resampler in resamplerNames)
+ {
+ foreach ((int s, int d) x in source2Dest)
+ {
+ result.Add(resampler, x.s, x.d);
+ }
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
index bec64e4d3..b4aff53e8 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
@@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
+// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
@@ -20,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
- public static readonly TheoryData AllReSamplers =
+ public static readonly TheoryData AllResamplers =
new TheoryData
{
{ "Bicubic", KnownResamplers.Bicubic },
@@ -40,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
};
[Theory]
- [WithTestPatternImages(nameof(AllReSamplers), 100, 100, DefaultPixelType, 0.5f)]
- [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.5f)]
- [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.3f)]
+ [WithTestPatternImages(nameof(AllResamplers), 100, 100, DefaultPixelType, 0.5f)]
+ [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.5f)]
+ [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.3f)]
public void Resize_WorksWithAllResamplers(TestImageProvider provider, string name, IResampler sampler, float ratio)
where TPixel : struct, IPixel
{
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
index d1d2ea077..29c51543f 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
@@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
foreach (string resamplerName in ResamplerNames)
{
- IResampler sampler = GetResampler(resamplerName);
+ IResampler sampler = TestUtils.GetResampler(resamplerName);
using (Image image = provider.GetImage())
{
image.Mutate(i => i.Skew(x, y, sampler));
@@ -68,17 +68,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
}
}
-
- private static IResampler GetResampler(string name)
- {
- PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
-
- if (property is null)
- {
- throw new Exception($"No resampler named '{name}");
- }
-
- return (IResampler)property.GetValue(null);
- }
}
}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
similarity index 96%
rename from tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs
rename to tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
index e06d2da91..65989556d 100644
--- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs
+++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
@@ -15,9 +15,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
- public class JpegBenchmarks : MeasureFixture
+ public class JpegProfilingBenchmarks : MeasureFixture
{
- public JpegBenchmarks(ITestOutputHelper output)
+ public JpegProfilingBenchmarks(ITestOutputHelper output)
: base(output)
{
}
diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs
similarity index 89%
rename from tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs
rename to tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs
index 306072276..95fe4e48f 100644
--- a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs
+++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs
@@ -10,9 +10,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
- public class LoadResizeSaveBenchmarks : MeasureFixture
+ public class LoadResizeSaveProfilingBenchmarks : MeasureFixture
{
- public LoadResizeSaveBenchmarks(ITestOutputHelper output)
+ public LoadResizeSaveProfilingBenchmarks(ITestOutputHelper output)
: base(output)
{
}
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs
similarity index 82%
rename from tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs
rename to tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs
index e24458d38..8b9355938 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs
+++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs
@@ -7,17 +7,10 @@ using SixLabors.ImageSharp.Processing;
using Xunit;
using Xunit.Abstractions;
-namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
+namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
public class ResizeProfilingBenchmarks : MeasureFixture
{
- public const string SkipText =
-#if false
- null;
-#else
- "Benchmark, enable manually!";
-#endif
-
private readonly Configuration configuration = Configuration.CreateDefaultInstance();
public ResizeProfilingBenchmarks(ITestOutputHelper output)
@@ -28,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public int ExecutionCount { get; set; } = 50;
- [Theory(Skip = SkipText)]
+ [Theory(Skip = ProfilingSetup.SkipProfilingTests)]
[InlineData(100, 100)]
[InlineData(2000, 2000)]
public void ResizeBicubic(int width, int height)
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
index d7755ff7a..5ea0bccf0 100644
--- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
@@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
@@ -284,5 +285,17 @@ namespace SixLabors.ImageSharp.Tests
}
public static string AsInvariantString(this FormattableString formattable) => System.FormattableString.Invariant(formattable);
+
+ public static IResampler GetResampler(string name)
+ {
+ PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
+
+ if (property is null)
+ {
+ throw new Exception($"No resampler named '{name}");
+ }
+
+ return (IResampler)property.GetValue(null);
+ }
}
}
\ No newline at end of file
diff --git a/tests/Images/External b/tests/Images/External
index e7e0f1311..5b18d8c95 160000
--- a/tests/Images/External
+++ b/tests/Images/External
@@ -1 +1 @@
-Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4
+Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128