Browse Source

Merge pull request #386 from SixLabors/js/affine-transforms

Add Interpolated Affine Transforms
af/merge-core
James Jackson-South 8 years ago
committed by GitHub
parent
commit
0cf2315b94
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      NuGet.config
  2. 6
      src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
  3. 21
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  4. 11
      src/ImageSharp/Image/ImageFrame{TPixel}.cs
  5. 2
      src/ImageSharp/ImageSharp.csproj
  6. 2
      src/ImageSharp/Memory/Buffer{T}.cs
  7. 6
      src/ImageSharp/PixelFormats/Rgb24.cs
  8. 2
      src/ImageSharp/PixelFormats/Rgba32.cs
  9. 245
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  10. 18
      src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs
  11. 49
      src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs
  12. 43
      src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs
  13. 139
      src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs
  14. 50
      src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs
  15. 240
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  16. 199
      src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs
  17. 2
      src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs
  18. 37
      src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
  19. 229
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  20. 80
      src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs
  21. 53
      src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs
  22. 150
      src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs
  23. 2
      src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs
  24. 96
      src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs
  25. 2
      src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs
  26. 8
      src/ImageSharp/Processing/Transforms/Resize.cs
  27. 29
      src/ImageSharp/Processing/Transforms/Rotate.cs
  28. 26
      src/ImageSharp/Processing/Transforms/Skew.cs
  29. 122
      src/ImageSharp/Processing/Transforms/Transform.cs
  30. 68
      src/ImageSharp/Processing/Transforms/TransformHelpers.cs
  31. 20
      tests/ImageSharp.Tests/Drawing/DrawImageTest.cs
  32. 3
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  33. 6
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs
  34. 37
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  35. 18
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  36. 58
      tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
  37. 258
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs
  38. 9
      tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs
  39. 31
      tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs
  40. 21
      tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs
  41. 80
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs
  42. 2
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs

1
NuGet.config

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="myget.org sixlabors" value="https://www.myget.org/F/sixlabors/api/v3/index.json" />
<add key="myget.org dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="nuget.org" value="https://www.nuget.org/api/v2/" />

6
src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

@ -36,9 +36,9 @@
<ProjectReference Include="..\ImageSharp\ImageSharp.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0004" />
<PackageReference Include="SixLabors.Shapes.Text" Version="1.0.0-beta0003" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0003" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-ci0005" />
<PackageReference Include="SixLabors.Shapes.Text" Version="1.0.0-ci0005" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-ci0005" />
<AdditionalFiles Include="..\..\stylecop.json" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
<PrivateAssets>All</PrivateAssets>

21
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -139,27 +139,6 @@ namespace SixLabors.ImageSharp
return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
}
/// <summary>
/// Gets the bounding <see cref="Rectangle"/> from the given matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
var leftTop = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
var rightTop = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
var leftBottom = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
var rightBottom = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
Vector2[] allCorners = { leftTop, rightTop, leftBottom, rightBottom };
float extentX = allCorners.Select(v => v.X).Max() - allCorners.Select(v => v.X).Min();
float extentY = allCorners.Select(v => v.Y).Max() - allCorners.Select(v => v.Y).Min();
return new Rectangle(0, 0, (int)extentX, (int)extentY);
}
/// <summary>
/// Finds the bounding rectangle based on the first instance of any color component other
/// than the given one.

11
src/ImageSharp/Image/ImageFrame{TPixel}.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
{
@ -53,6 +54,16 @@ namespace SixLabors.ImageSharp
this.MetaData = metaData;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>
/// <param name="size">The <see cref="Size"/> of the frame.</param>
/// <param name="metaData">The meta data.</param>
internal ImageFrame(Size size, ImageFrameMetaData metaData)
: this(size.Width, size.Height, metaData)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>

2
src/ImageSharp/ImageSharp.csproj

@ -34,7 +34,7 @@
<Compile Include="..\Shared\*.cs" Exclude="bin\**;obj\**;**\*.xproj;packages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0004" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-ci0005" />
<AdditionalFiles Include="..\..\stylecop.json" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
<PrivateAssets>All</PrivateAssets>

2
src/ImageSharp/Memory/Buffer{T}.cs

@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Memory
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Buffer<T> CreateClean(int count)
{
Buffer<T> buffer = new Buffer<T>(count);
var buffer = new Buffer<T>(count);
buffer.Clear();
return buffer;
}

6
src/ImageSharp/PixelFormats/Rgb24.cs

@ -126,5 +126,11 @@ namespace SixLabors.ImageSharp.PixelFormats
dest.B = this.B;
dest.A = 255;
}
/// <inheritdoc/>
public override string ToString()
{
return $"({this.R},{this.G},{this.B})";
}
}
}

2
src/ImageSharp/PixelFormats/Rgba32.cs

@ -364,7 +364,7 @@ namespace SixLabors.ImageSharp
/// <returns>A string representation of the packed vector.</returns>
public override string ToString()
{
return this.ToVector4().ToString();
return $"({this.R},{this.G},{this.B},{this.A})";
}
/// <inheritdoc/>

245
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -0,0 +1,245 @@
// 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.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Provides the base methods to perform affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AffineTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private Size targetDimensions;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
public AffineTransformProcessor(Matrix3x2 matrix)
: this(matrix, KnownResamplers.Bicubic)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler)
: this(matrix, sampler, Size.Empty)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
: base(sampler)
{
// Tansforms are inverted else the output is the opposite of the expected.
Matrix3x2.Invert(matrix, out matrix);
this.TransformMatrix = matrix;
this.targetDimensions = targetDimensions;
}
/// <summary>
/// Gets the matrix used to supply the affine transform
/// </summary>
public Matrix3x2 TransformMatrix { get; }
/// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
if (this.targetDimensions == Size.Empty)
{
// TODO: CreateDestination() should not modify the processors state! (kinda CQRS)
this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix);
}
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(this.targetDimensions, x.MetaData.Clone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
}
/// <inheritdoc/>
protected override void OnApply(
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Rectangle sourceRectangle,
Configuration configuration)
{
int height = this.targetDimensions.Height;
int width = this.targetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
if (this.Sampler is NearestNeighborResampler)
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
});
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
using (var yBuffer = new Buffer2D<float>(yLength, height))
using (var xBuffer = new Buffer2D<float>(xLength, height))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
Span<float> ySpan = yBuffer.GetRowSpan(y);
Span<float> xSpan = xBuffer.GetRowSpan(y);
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = ySpan[yy];
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = xSpan[xx];
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W);
sum += mupltiplied * xWeight * yWeight;
}
}
ref TPixel dest = ref destRow[x];
// Reverse the premultiplication
dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W));
}
});
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix3x2"/>.
/// </returns>
protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
return this.TransformMatrix;
}
/// <summary>
/// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
/// </summary>
/// <param name="sourceDimensions">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Rectangle"/></returns>
protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix)
{
return sourceDimensions;
}
}
}

18
src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs

@ -15,13 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
internal class AutoOrientProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoOrientProcessor{TPixel}"/> class.
/// </summary>
public AutoOrientProcessor()
{
}
/// <inheritdoc/>
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle)
{
Orientation orientation = GetExifOrientation(source);
@ -33,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
break;
case Orientation.BottomRight:
new RotateProcessor<TPixel>() { Angle = (int)RotateType.Rotate180, Expand = false }.Apply(source, sourceRectangle);
new RotateProcessor<TPixel>((int)RotateType.Rotate180).Apply(source, sourceRectangle);
break;
case Orientation.BottomLeft:
@ -41,21 +35,21 @@ namespace SixLabors.ImageSharp.Processing.Processors
break;
case Orientation.LeftTop:
new RotateProcessor<TPixel>() { Angle = (int)RotateType.Rotate90, Expand = false }.Apply(source, sourceRectangle);
new RotateProcessor<TPixel>((int)RotateType.Rotate90).Apply(source, sourceRectangle);
new FlipProcessor<TPixel>(FlipType.Horizontal).Apply(source, sourceRectangle);
break;
case Orientation.RightTop:
new RotateProcessor<TPixel>() { Angle = (int)RotateType.Rotate90, Expand = false }.Apply(source, sourceRectangle);
new RotateProcessor<TPixel>((int)RotateType.Rotate90).Apply(source, sourceRectangle);
break;
case Orientation.RightBottom:
new FlipProcessor<TPixel>(FlipType.Vertical).Apply(source, sourceRectangle);
new RotateProcessor<TPixel>() { Angle = (int)RotateType.Rotate270, Expand = false }.Apply(source, sourceRectangle);
new RotateProcessor<TPixel>((int)RotateType.Rotate270).Apply(source, sourceRectangle);
break;
case Orientation.LeftBottom:
new RotateProcessor<TPixel>() { Angle = (int)RotateType.Rotate270, Expand = false }.Apply(source, sourceRectangle);
new RotateProcessor<TPixel>((int)RotateType.Rotate270).Apply(source, sourceRectangle);
break;
case Orientation.Unknown:

49
src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredAffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler)
: base(matrix, sampler)
{
}
/// <inheritdoc/>
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F);
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter;
}
/// <inheritdoc/>
protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix)
{
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
if (!Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix))
{
// TODO: Shouldn't we throw an exception instead?
return sourceDimensions;
}
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix).Size;
}
}
}

43
src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of non-affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredProjectiveTransformProcessor<TPixel> : ProjectiveTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler)
: base(matrix, sampler)
{
}
/// <inheritdoc/>
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0);
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0);
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter;
}
/// <inheritdoc/>
protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix)
{
return Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 sizeMatrix)
? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix)
: sourceRectangle;
}
}
}

139
src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs

@ -0,0 +1,139 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// The base class for performing interpolated affine and non-affine transforms.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class InterpolatedTransformProcessorBase<TPixel> : CloningImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="InterpolatedTransformProcessorBase{TPixel}"/> class.
/// </summary>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected InterpolatedTransformProcessorBase(IResampler sampler)
{
this.Sampler = sampler;
}
/// <summary>
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Calculated the weights for the given point.
/// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered.
/// Additionally the weights are nomalized.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="scale">The transformed image scale relative to the source</param>
/// <param name="weights">The collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span<float> weights)
{
float sum = 0;
ref float weightsBaseRef = ref weights[0];
// Downsampling weights requires more edge sampling plus normalization of the weights
for (int x = 0, i = min; i <= max; i++, x++)
{
int index = i;
if (index < sourceMin)
{
index = sourceMin;
}
if (index > sourceMax)
{
index = sourceMax;
}
float weight = sampler.GetValue((index - point) / scale);
sum += weight;
Unsafe.Add(ref weightsBaseRef, x) = weight;
}
if (sum > 0)
{
for (int i = 0; i < weights.Length; i++)
{
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i);
wRef = wRef / sum;
}
}
}
/// <summary>
/// Calculated the weights for the given point.
/// </summary>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="weights">The collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span<float> weights)
{
ref float weightsBaseRef = ref weights[0];
for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++)
{
float weight = sampler.GetValue(i - point);
Unsafe.Add(ref weightsBaseRef, x) = weight;
}
}
/// <summary>
/// Calculates the sampling radius for the current sampler
/// </summary>
/// <param name="sourceSize">The source dimension size</param>
/// <param name="destinationSize">The destination dimension size</param>
/// <returns>The radius, and scaling factor</returns>
protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio);
}
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle)
{
ExifProfile profile = destination.MetaData.ExifProfile;
if (profile == null)
{
return;
}
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.SetValue(ExifTag.PixelXDimension, destination.Width);
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.SetValue(ExifTag.PixelYDimension, destination.Height);
}
}
}
}

50
src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs

@ -1,50 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Provides methods to transform an image using a <see cref="Matrix3x2"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class Matrix3x2Processor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets the rectangle designating the target canvas.
/// </summary>
protected Rectangle CanvasRectangle { get; private set; }
/// <summary>
/// Creates a new target canvas to contain the results of the matrix transform.
/// </summary>
/// <param name="sourceRectangle">The source rectangle.</param>
/// <param name="processMatrix">The processing matrix.</param>
protected void CreateNewCanvas(Rectangle sourceRectangle, Matrix3x2 processMatrix)
{
Matrix3x2 sizeMatrix;
this.CanvasRectangle = Matrix3x2.Invert(processMatrix, out sizeMatrix)
? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix)
: sourceRectangle;
}
/// <summary>
/// Gets a transform matrix adjusted to center upon the target image bounds.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="matrix">The transform matrix.</param>
/// <returns>
/// The <see cref="Matrix3x2"/>.
/// </returns>
protected Matrix3x2 GetCenteredMatrix(ImageFrame<TPixel> source, Matrix3x2 matrix)
{
var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.CanvasRectangle.Width * .5F, -this.CanvasRectangle.Height * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F);
return (translationToTargetCenter * matrix) * translateToSourceCenter;
}
}
}

240
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs

@ -0,0 +1,240 @@
// 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.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Provides the base methods to perform non-affine transforms on an image.
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
// TODO: We should use a Size instead! (See AffineTransformProcessor<T>)
private Rectangle targetRectangle;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix)
: this(matrix, KnownResamplers.Bicubic)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler)
: this(matrix, sampler, Rectangle.Empty)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle)
: base(sampler)
{
// Tansforms are inverted else the output is the opposite of the expected.
Matrix4x4.Invert(matrix, out matrix);
this.TransformMatrix = matrix;
this.targetRectangle = rectangle;
}
/// <summary>
/// Gets the matrix used to supply the non-affine transform
/// </summary>
public Matrix4x4 TransformMatrix { get; }
/// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
if (this.targetRectangle == Rectangle.Empty)
{
this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix);
}
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
}
/// <inheritdoc/>
protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{
int height = this.targetRectangle.Height;
int width = this.targetRectangle.Width;
Rectangle sourceBounds = source.Bounds();
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle);
if (this.Sampler is NearestNeighborResampler)
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Round(Vector2.Transform(new Vector2(x, y), matrix));
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
});
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
using (var yBuffer = new Buffer2D<float>(yLength, height))
using (var xBuffer = new Buffer2D<float>(xLength, height))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
Span<float> ySpan = yBuffer.GetRowSpan(y);
Span<float> xSpan = xBuffer.GetRowSpan(y);
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = ySpan[yy];
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = xSpan[xx];
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W);
sum += mupltiplied * xWeight * yWeight;
}
}
ref TPixel dest = ref destRow[x];
// Reverse the premultiplication
dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W));
}
});
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix4x4"/>.
/// </returns>
protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
return this.TransformMatrix;
}
/// <summary>
/// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
/// </summary>
/// <param name="sourceRectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Rectangle"/></returns>
protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix)
{
return sourceRectangle;
}
}
}

199
src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs

@ -1,199 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <content>
/// Conains the definition of <see cref="WeightsWindow"/> and <see cref="WeightsBuffer"/>.
/// </content>
internal abstract partial class ResamplingWeightedProcessor<TPixel>
{
/// <summary>
/// Points to a collection of weights allocated in <see cref="WeightsBuffer"/>.
/// </summary>
internal struct WeightsWindow
{
/// <summary>
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The index in the destination buffer
/// </summary>
private readonly int flatStartIndex;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Buffer<float> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsWindow"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal WeightsWindow(int index, int left, Buffer2D<float> buffer, int length)
{
this.flatStartIndex = (index * buffer.Width) + left;
this.Left = left;
this.buffer = buffer;
this.Length = length;
}
/// <summary>
/// Gets a reference to the first item of the window.
/// </summary>
/// <returns>The reference to the first item of the window</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref float GetStartReference()
{
return ref this.buffer[this.flatStartIndex];
}
/// <summary>
/// Gets the span representing the portion of the <see cref="WeightsBuffer"/> that this window covers
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<float> GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length);
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float weight = Unsafe.Add(ref horizontalValues, i);
Vector4 v = Unsafe.Add(ref vecPtr, i);
result += v * weight;
}
return result;
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float weight = Unsafe.Add(ref horizontalValues, i);
Vector4 v = Unsafe.Add(ref vecPtr, i);
result += v.Expand() * weight;
}
return result;
}
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> firstPassPixels, int x, int sourceY)
{
ref float verticalValues = ref this.GetStartReference();
int left = this.Left;
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float yw = Unsafe.Add(ref verticalValues, i);
int index = left + i + sourceY;
result += firstPassPixels[x, index] * yw;
}
return result;
}
}
/// <summary>
/// Holds the <see cref="WeightsWindow"/> values in an optimized contiguous memory region.
/// </summary>
internal class WeightsBuffer : IDisposable
{
private readonly Buffer2D<float> dataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsBuffer"/> class.
/// </summary>
/// <param name="sourceSize">The size of the source window</param>
/// <param name="destinationSize">The size of the destination window</param>
public WeightsBuffer(int sourceSize, int destinationSize)
{
this.dataBuffer = Buffer2D<float>.CreateClean(sourceSize, destinationSize);
this.Weights = new WeightsWindow[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Weights"/> values.
/// </summary>
public WeightsWindow[] Weights { get; }
/// <summary>
/// Disposes <see cref="WeightsBuffer"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.dataBuffer.Dispose();
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx)
{
return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1);
}
}
}
}

2
src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// Adapted from <see href="http://www.realtimerendering.com/resources/GraphicsGems/gemsiii/filter_rcg.c"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract partial class ResamplingWeightedProcessor<TPixel> : CloningImageProcessor<TPixel>
internal abstract class ResamplingWeightedProcessor<TPixel> : CloningImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>

37
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs

@ -8,6 +8,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -53,20 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
Configuration config = source.GetConfiguration();
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(this.Width, this.Height, x.MetaData.Clone()));
// We will always be creating the clone even for mutate because thats the way this base processor works
// ------------
// For resize we know we are going to populate every pixel with fresh data and we want a different target size so
// let's manually clone an empty set of images at the correct target and then have the base class process them in turn.
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select(x => new ImageFrame<TPixel>(this.Width, this.Height, x.MetaData.Clone())); // this will create places holders
var image = new Image<TPixel>(config, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added
return image;
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
}
/// <inheritdoc/>
protected override unsafe void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> cloned, Rectangle sourceRectangle, Configuration configuration)
protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> cloned, Rectangle sourceRectangle, Configuration configuration)
{
// Jump out, we'll deal with that later.
if (source.Width == cloned.Width && source.Height == cloned.Height && sourceRectangle == this.ResizeRectangle)
@ -194,5 +191,25 @@ namespace SixLabors.ImageSharp.Processing.Processors
});
}
}
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle)
{
ExifProfile profile = destination.MetaData.ExifProfile;
if (profile == null)
{
return;
}
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.SetValue(ExifTag.PixelXDimension, destination.Width);
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.SetValue(ExifTag.PixelYDimension, destination.Height);
}
}
}
}

229
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -17,87 +15,55 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// Provides methods that allow the rotating of images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class RotateProcessor<TPixel> : Matrix3x2Processor<TPixel>
internal class RotateProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The transform matrix to apply.
/// Initializes a new instance of the <see cref="RotateProcessor{TPixel}"/> class.
/// </summary>
private Matrix3x2 processMatrix;
/// <param name="degrees">The angle of rotation in degrees.</param>
public RotateProcessor(float degrees)
: this(degrees, KnownResamplers.Bicubic)
{
}
/// <summary>
/// Gets or sets the angle of processMatrix in degrees.
/// Initializes a new instance of the <see cref="RotateProcessor{TPixel}"/> class.
/// </summary>
public float Angle { get; set; }
/// <param name="degrees">The angle of rotation in degrees.</param>
/// <param name="sampler">The sampler to perform the rotating operation.</param>
public RotateProcessor(float degrees, IResampler sampler)
: base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler)
{
this.Degrees = degrees;
}
/// <summary>
/// Gets or sets a value indicating whether to expand the canvas to fit the rotated image.
/// Gets the angle of rotation in degrees.
/// </summary>
public bool Expand { get; set; } = true;
/// <inheritdoc/>
protected override void OnApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
if (this.OptimizedApply(source, configuration))
{
return;
}
int height = this.CanvasRectangle.Height;
int width = this.CanvasRectangle.Width;
Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix);
Rectangle sourceBounds = source.Bounds();
using (var targetPixels = new PixelAccessor<TPixel>(width, height))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = 0; x < width; x++)
{
var transformedPoint = Point.Rotate(new Point(x, y), matrix);
if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y))
{
targetRow[x] = source[transformedPoint.X, transformedPoint.Y];
}
}
});
source.SwapPixelsBuffers(targetPixels);
}
}
public float Degrees { get; }
/// <inheritdoc/>
protected override void BeforeApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{
if (MathF.Abs(this.Angle) < Constants.Epsilon || MathF.Abs(this.Angle - 90) < Constants.Epsilon || MathF.Abs(this.Angle - 180) < Constants.Epsilon || MathF.Abs(this.Angle - 270) < Constants.Epsilon)
if (this.OptimizedApply(source, destination, configuration))
{
return;
}
this.processMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0));
if (this.Expand)
{
this.CreateNewCanvas(sourceRectangle, this.processMatrix);
}
base.OnApply(source, destination, sourceRectangle, configuration);
}
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> source, Rectangle sourceRectangle)
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle)
{
ExifProfile profile = source.MetaData.ExifProfile;
ExifProfile profile = destination.MetaData.ExifProfile;
if (profile == null)
{
return;
}
if (MathF.Abs(this.Angle) < Constants.Epsilon)
if (MathF.Abs(WrapDegrees(this.Degrees)) < Constants.Epsilon)
{
// No need to do anything so return.
return;
@ -105,44 +71,62 @@ namespace SixLabors.ImageSharp.Processing.Processors
profile.RemoveValue(ExifTag.Orientation);
if (this.Expand && profile.GetValue(ExifTag.PixelXDimension) != null)
base.AfterImageApply(source, destination, sourceRectangle);
}
/// <summary>
/// Wraps a given angle in degrees so that it falls withing the 0-360 degree range
/// </summary>
/// <param name="degrees">The angle of rotation in degrees.</param>
/// <returns>The <see cref="float"/></returns>
private static float WrapDegrees(float degrees)
{
degrees = degrees % 360;
while (degrees < 0)
{
profile.SetValue(ExifTag.PixelXDimension, source.Width);
profile.SetValue(ExifTag.PixelYDimension, source.Height);
degrees += 360;
}
return degrees;
}
/// <summary>
/// Rotates the images with an optimized method when the angle is 90, 180 or 270 degrees.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
/// <returns>
/// The <see cref="bool" />
/// </returns>
private bool OptimizedApply(ImageFrame<TPixel> source, Configuration configuration)
private bool OptimizedApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
if (MathF.Abs(this.Angle) < Constants.Epsilon)
// Wrap the degrees to keep within 0-360 so we can apply optimizations when possible.
float degrees = WrapDegrees(this.Degrees);
if (MathF.Abs(degrees) < Constants.Epsilon)
{
// No need to do anything so return.
// The destination will be blank here so copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
return true;
}
if (MathF.Abs(this.Angle - 90) < Constants.Epsilon)
if (MathF.Abs(degrees - 90) < Constants.Epsilon)
{
this.Rotate90(source, configuration);
this.Rotate90(source, destination, configuration);
return true;
}
if (MathF.Abs(this.Angle - 180) < Constants.Epsilon)
if (MathF.Abs(degrees - 180) < Constants.Epsilon)
{
this.Rotate180(source, configuration);
this.Rotate180(source, destination, configuration);
return true;
}
if (MathF.Abs(this.Angle - 270) < Constants.Epsilon)
if (MathF.Abs(degrees - 270) < Constants.Epsilon)
{
this.Rotate270(source, configuration);
this.Rotate270(source, destination, configuration);
return true;
}
@ -153,95 +137,90 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// Rotates the image 270 degrees clockwise at the centre point.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
private void Rotate270(ImageFrame<TPixel> source, Configuration configuration)
private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
int width = source.Width;
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
using (var targetPixels = new PixelAccessor<TPixel>(height, width))
{
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
{
for (int x = 0; x < width; x++)
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
targetPixels[newX, newY] = sourcePixels[x, y];
}
});
}
source.SwapPixelsBuffers(targetPixels);
}
destination[newX, newY] = sourceRow[x];
}
}
});
}
/// <summary>
/// Rotates the image 180 degrees clockwise at the centre point.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
private void Rotate180(ImageFrame<TPixel> source, Configuration configuration)
private void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
int width = source.Width;
int height = source.Height;
using (var targetPixels = new PixelAccessor<TPixel>(width, height))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
{
targetRow[width - x - 1] = sourceRow[x];
}
});
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
source.SwapPixelsBuffers(targetPixels);
}
for (int x = 0; x < width; x++)
{
targetRow[width - x - 1] = sourceRow[x];
}
});
}
/// <summary>
/// Rotates the image 90 degrees clockwise at the centre point.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
private void Rotate90(ImageFrame<TPixel> source, Configuration configuration)
private void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
int width = source.Width;
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
using (var targetPixels = new PixelAccessor<TPixel>(height, width))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
if (destinationBounds.Contains(newX, x))
{
targetPixels[newX, x] = sourceRow[x];
destination[newX, x] = sourceRow[x];
}
});
source.SwapPixelsBuffers(targetPixels);
}
}
});
}
}
}

80
src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs

@ -1,12 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -16,70 +10,40 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// Provides methods that allow the skewing of images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class SkewProcessor<TPixel> : Matrix3x2Processor<TPixel>
internal class SkewProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The transform matrix to apply.
/// Initializes a new instance of the <see cref="SkewProcessor{TPixel}"/> class.
/// </summary>
private Matrix3x2 processMatrix;
/// <param name="degreesX">The angle in degrees to perform the skew along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the skew along the y-axis.</param>
public SkewProcessor(float degreesX, float degreesY)
: this(degreesX, degreesY, KnownResamplers.Bicubic)
{
}
/// <summary>
/// Gets or sets the angle of rotation along the x-axis in degrees.
/// Initializes a new instance of the <see cref="SkewProcessor{TPixel}"/> class.
/// </summary>
public float AngleX { get; set; }
/// <param name="degreesX">The angle in degrees to perform the skew along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the skew along the y-axis.</param>
/// <param name="sampler">The sampler to perform the skew operation.</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler)
: base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler)
{
this.DegreesX = degreesX;
this.DegreesY = degreesY;
}
/// <summary>
/// Gets or sets the angle of rotation along the y-axis in degrees.
/// Gets the angle of rotation along the x-axis in degrees.
/// </summary>
public float AngleY { get; set; }
public float DegreesX { get; }
/// <summary>
/// Gets or sets a value indicating whether to expand the canvas to fit the skewed image.
/// Gets the angle of rotation along the y-axis in degrees.
/// </summary>
public bool Expand { get; set; } = true;
/// <inheritdoc/>
protected override void OnApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
int height = this.CanvasRectangle.Height;
int width = this.CanvasRectangle.Width;
Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix);
Rectangle sourceBounds = source.Bounds();
using (var targetPixels = new PixelAccessor<TPixel>(width, height))
{
Parallel.For(
0,
height,
configuration.ParallelOptions,
y =>
{
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = 0; x < width; x++)
{
var transformedPoint = Point.Skew(new Point(x, y), matrix);
if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y))
{
targetRow[x] = source[transformedPoint.X, transformedPoint.Y];
}
}
});
source.SwapPixelsBuffers(targetPixels);
}
}
/// <inheritdoc/>
protected override void BeforeApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
this.processMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0));
if (this.Expand)
{
this.CreateNewCanvas(sourceRectangle, this.processMatrix);
}
}
public float DegreesY { get; }
}
}

53
src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Holds the <see cref="WeightsWindow"/> values in an optimized contigous memory region.
/// </summary>
internal class WeightsBuffer : IDisposable
{
private readonly Buffer2D<float> dataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsBuffer"/> class.
/// </summary>
/// <param name="sourceSize">The size of the source window</param>
/// <param name="destinationSize">The size of the destination window</param>
public WeightsBuffer(int sourceSize, int destinationSize)
{
this.dataBuffer = Buffer2D<float>.CreateClean(sourceSize, destinationSize);
this.Weights = new WeightsWindow[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Weights"/> values.
/// </summary>
public WeightsWindow[] Weights { get; }
/// <summary>
/// Disposes <see cref="WeightsBuffer"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.dataBuffer.Dispose();
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx)
{
return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1);
}
}
}

150
src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs

@ -0,0 +1,150 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="WeightsBuffer"/>.
/// </summary>
internal struct WeightsWindow
{
/// <summary>
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The index in the destination buffer
/// </summary>
private readonly int flatStartIndex;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Buffer<float> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsWindow"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal WeightsWindow(int index, int left, Buffer2D<float> buffer, int length)
{
this.flatStartIndex = (index * buffer.Width) + left;
this.Left = left;
this.buffer = buffer;
this.Length = length;
}
/// <summary>
/// Gets a reference to the first item of the window.
/// </summary>
/// <returns>The reference to the first item of the window</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref float GetStartReference()
{
return ref this.buffer[this.flatStartIndex];
}
/// <summary>
/// Gets the span representing the portion of the <see cref="WeightsBuffer"/> that this window covers
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<float> GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length);
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float weight = Unsafe.Add(ref horizontalValues, i);
Vector4 v = Unsafe.Add(ref vecPtr, i);
result += v * weight;
}
return result;
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float weight = Unsafe.Add(ref horizontalValues, i);
Vector4 v = Unsafe.Add(ref vecPtr, i);
result += v.Expand() * weight;
}
return result;
}
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> firstPassPixels, int x, int sourceY)
{
ref float verticalValues = ref this.GetStartReference();
int left = this.Left;
// Destination color components
Vector4 result = Vector4.Zero;
for (int i = 0; i < this.Length; i++)
{
float yw = Unsafe.Add(ref verticalValues, i);
int index = left + i + sourceY;
result += firstPassPixels[x, index] * yw;
}
return result;
}
}
}

2
src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs

@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets or sets the sampler to perform the resize operation.
/// </summary>
public IResampler Sampler { get; set; } = new BicubicResampler();
public IResampler Sampler { get; set; } = KnownResamplers.Bicubic;
/// <summary>
/// Gets or sets a value indicating whether to compress

96
src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs

@ -0,0 +1,96 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Contains reusable static instances of known resampling algorithms
/// </summary>
public static class KnownResamplers
{
/// <summary>
/// Gets the Bicubic sampler that implements the bicubic kernel algorithm W(x)
/// </summary>
public static IResampler Bicubic { get; } = new BicubicResampler();
/// <summary>
/// Gets the Box sampler that implements the box algorithm. Similar to nearest neighbor when upscaling.
/// When downscaling the pixels will average, merging pixels together.
/// </summary>
public static IResampler Box { get; } = new BoxResampler();
/// <summary>
/// Gets the Catmull-Rom sampler, a well known standard Cubic Filter often used as a interpolation function
/// </summary>
public static IResampler CatmullRom { get; } = new CatmullRomResampler();
/// <summary>
/// Gets the Hermite sampler. A type of smoothed triangular interpolation filter that rounds off strong edges while
/// preserving flat 'color levels' in the original image.
/// </summary>
public static IResampler Hermite { get; } = new HermiteResampler();
/// <summary>
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 2 pixels.
/// This algorithm provides sharpened results when compared to others when downsampling.
/// </summary>
public static IResampler Lanczos2 { get; } = new Lanczos2Resampler();
/// <summary>
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 3 pixels
/// This algorithm provides sharpened results when compared to others when downsampling.
/// </summary>
public static IResampler Lanczos3 { get; } = new Lanczos3Resampler();
/// <summary>
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 5 pixels
/// This algorithm provides sharpened results when compared to others when downsampling.
/// </summary>
public static IResampler Lanczos5 { get; } = new Lanczos5Resampler();
/// <summary>
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 8 pixels
/// This algorithm provides sharpened results when compared to others when downsampling.
/// </summary>
public static IResampler Lanczos8 { get; } = new Lanczos8Resampler();
/// <summary>
/// Gets the Mitchell-Netravali sampler. This seperable cubic algorithm yields a very good equilibrium between
/// detail preservation (sharpness) and smoothness.
/// </summary>
public static IResampler MitchellNetravali { get; } = new MitchellNetravaliResampler();
/// <summary>
/// Gets the Nearest-Neighbour sampler that implements the nearest neighbor algorithm. This uses a very fast, unscaled filter
/// which will select the closest pixel to the new pixels position.
/// </summary>
public static IResampler NearestNeighbor { get; } = new NearestNeighborResampler();
/// <summary>
/// Gets the Robidoux sampler. This algorithm developed by Nicolas Robidoux providing a very good equilibrium between
/// detail preservation (sharpness) and smoothness comprable to <see cref="MitchellNetravali"/>.
/// </summary>
public static IResampler Robidoux { get; } = new RobidouxResampler();
/// <summary>
/// Gets the Robidoux Sharp sampler. A sharpend form of the <see cref="Robidoux"/> sampler
/// </summary>
public static IResampler RobidouxSharp { get; } = new RobidouxSharpResampler();
/// <summary>
/// Gets the Spline sampler. A seperable cubic algorithm similar to <see cref="MitchellNetravali"/> but yielding smoother results.
/// </summary>
public static IResampler Spline { get; } = new SplineResampler();
/// <summary>
/// Gets the Triangle sampler, otherwise known as Bilinear. This interpolation algorithm can be used where perfect image transformation
/// with pixel matching is impossible, so that one can calculate and assign appropriate intensity values to pixels
/// </summary>
public static IResampler Triangle { get; } = new TriangleResampler();
/// <summary>
/// Gets the Welch sampler. A high speed algorthm that delivers very sharpened results.
/// </summary>
public static IResampler Welch { get; } = new WelchResampler();
}
}

2
src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs

@ -28,4 +28,4 @@ namespace SixLabors.ImageSharp.Processing
return 0F;
}
}
}
}

8
src/ImageSharp/Processing/Transforms/Resize.cs

@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp
public static IImageProcessingContext<TPixel> Resize<TPixel>(this IImageProcessingContext<TPixel> source, Size size)
where TPixel : struct, IPixel<TPixel>
{
return Resize(source, size.Width, size.Height, new BicubicResampler(), false);
return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, false);
}
/// <summary>
@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp
public static IImageProcessingContext<TPixel> Resize<TPixel>(this IImageProcessingContext<TPixel> source, Size size, bool compand)
where TPixel : struct, IPixel<TPixel>
{
return Resize(source, size.Width, size.Height, new BicubicResampler(), compand);
return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, compand);
}
/// <summary>
@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp
public static IImageProcessingContext<TPixel> Resize<TPixel>(this IImageProcessingContext<TPixel> source, int width, int height)
where TPixel : struct, IPixel<TPixel>
{
return Resize(source, width, height, new BicubicResampler(), false);
return Resize(source, width, height, KnownResamplers.Bicubic, false);
}
/// <summary>
@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp
public static IImageProcessingContext<TPixel> Resize<TPixel>(this IImageProcessingContext<TPixel> source, int width, int height, bool compand)
where TPixel : struct, IPixel<TPixel>
{
return Resize(source, width, height, new BicubicResampler(), compand);
return Resize(source, width, height, KnownResamplers.Bicubic, compand);
}
/// <summary>

29
src/ImageSharp/Processing/Transforms/Rotate.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors;
@ -14,39 +13,37 @@ namespace SixLabors.ImageSharp
public static partial class ImageExtensions
{
/// <summary>
/// Rotates an image by the given angle in degrees, expanding the image to fit the rotated result.
/// Rotates and flips an image by the given instructions.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to rotate.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <param name="rotateType">The <see cref="RotateType"/> to perform the rotation.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, float degrees)
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, RotateType rotateType)
where TPixel : struct, IPixel<TPixel>
{
return Rotate(source, degrees, true);
}
=> Rotate(source, (float)rotateType);
/// <summary>
/// Rotates and flips an image by the given instructions.
/// Rotates an image by the given angle in degrees.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to rotate.</param>
/// <param name="rotateType">The <see cref="RotateType"/> to perform the rotation.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, RotateType rotateType)
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, float degrees)
where TPixel : struct, IPixel<TPixel>
=> Rotate(source, (float)rotateType, false);
=> Rotate(source, degrees, KnownResamplers.Bicubic);
/// <summary>
/// Rotates an image by the given angle in degrees.
/// Rotates an image by the given angle in degrees using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to rotate.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <param name="expand">Whether to expand the image to fit the rotated result.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, float degrees, bool expand)
public static IImageProcessingContext<TPixel> Rotate<TPixel>(this IImageProcessingContext<TPixel> source, float degrees, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new RotateProcessor<TPixel> { Angle = degrees, Expand = expand });
=> source.ApplyProcessor(new RotateProcessor<TPixel>(degrees, sampler));
}
}
}

26
src/ImageSharp/Processing/Transforms/Skew.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors;
namespace SixLabors.ImageSharp
@ -13,30 +13,28 @@ namespace SixLabors.ImageSharp
public static partial class ImageExtensions
{
/// <summary>
/// Skews an image by the given angles in degrees, expanding the image to fit the skewed result.
/// Skews an image by the given angles in degrees.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to skew.</param>
/// <param name="degreesX">The angle in degrees to perform the rotation along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the rotation along the y-axis.</param>
/// <param name="degreesX">The angle in degrees to perform the skew along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the skew along the y-axis.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Skew<TPixel>(this IImageProcessingContext<TPixel> source, float degreesX, float degreesY)
where TPixel : struct, IPixel<TPixel>
{
return Skew(source, degreesX, degreesY, true);
}
=> Skew(source, degreesX, degreesY, KnownResamplers.Bicubic);
/// <summary>
/// Skews an image by the given angles in degrees.
/// Skews an image by the given angles in degrees using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to skew.</param>
/// <param name="degreesX">The angle in degrees to perform the rotation along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the rotation along the y-axis.</param>
/// <param name="expand">Whether to expand the image to fit the skewed result.</param>
/// <param name="degreesX">The angle in degrees to perform the skew along the x-axis.</param>
/// <param name="degreesY">The angle in degrees to perform the skew along the y-axis.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Skew<TPixel>(this IImageProcessingContext<TPixel> source, float degreesX, float degreesY, bool expand)
public static IImageProcessingContext<TPixel> Skew<TPixel>(this IImageProcessingContext<TPixel> source, float degreesX, float degreesY, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new SkewProcessor<TPixel> { AngleX = degreesX, AngleY = degreesY, Expand = expand });
=> source.ApplyProcessor(new SkewProcessor<TPixel>(degreesX, degreesY, sampler));
}
}
}

122
src/ImageSharp/Processing/Transforms/Transform.cs

@ -0,0 +1,122 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Extension methods for the <see cref="Image{TPixel}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Transforms an image by the given matrix.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.NearestNeighbor);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, sampler, Size.Empty);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm
/// and a rectangle defining the transform origin in the source image and the size of the result image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">
/// The rectangle defining the transform origin in the source image, and the size of the result image.
/// </param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
{
var t = Matrix3x2.CreateTranslation(-rectangle.Location);
Matrix3x2 combinedMatrix = t * matrix;
return source.ApplyProcessor(new AffineTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size));
}
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm,
/// cropping or extending the image according to <paramref name="destinationSize"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="destinationSize">The size of the destination image.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Size destinationSize)
where TPixel : struct, IPixel<TPixel>
{
return source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, destinationSize));
}
/// <summary>
/// Transforms an image by the given matrix.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.NearestNeighbor);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, sampler, Rectangle.Empty);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(matrix, sampler, rectangle));
}
}

68
src/ImageSharp/Processing/Transforms/TransformHelpers.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Contains helper methods for working with affine and non-affine transforms
/// </summary>
internal class TransformHelpers
{
/// <summary>
/// Returns the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors
float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
float sizeX = maxX - minX + .5F;
float sizeY = maxY - minY + .5F;
return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY));
}
}
}

20
tests/ImageSharp.Tests/Drawing/DrawImageTest.cs

@ -10,6 +10,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
using System;
using System.Numerics;
public class DrawImageTest : FileTestBase
{
@ -45,6 +46,25 @@ namespace SixLabors.ImageSharp.Tests
}
}
[Theory]
[WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Normal)]
public void ImageShouldDrawTransformedImage<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
using (Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F);
Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F));
blend.Mutate(x => x.Transform(rotate * scale));
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, mode, .75F, new Size(blend.Width, blend.Height), position));
image.DebugSave(provider, new[] { "Transformed" });
}
}
[Theory]
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32)]
public void ImageShouldHandleNegativeLocation(TestImageProvider<Rgba32> provider)

3
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -17,6 +17,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CoreCompat.System.Drawing" Version="1.0.0-beta006" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="Moq" Version="4.7.145" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.0" />

6
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs

@ -38,13 +38,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
// [Fact]
public void PrintWeightsData()
{
var proc = new ResizeProcessor<Rgba32>(new BicubicResampler(), 200, 200);
var proc = new ResizeProcessor<Rgba32>(KnownResamplers.Bicubic, 200, 200);
ResamplingWeightedProcessor<Rgba32>.WeightsBuffer weights = proc.PrecomputeWeights(200, 500);
WeightsBuffer weights = proc.PrecomputeWeights(200, 500);
var bld = new StringBuilder();
foreach (ResamplingWeightedProcessor<Rgba32>.WeightsWindow window in weights.Weights)
foreach (WeightsWindow window in weights.Weights)
{
Span<float> span = window.GetWindowSpan();
for (int i = 0; i < window.Length; i++)

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

@ -20,19 +20,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public static readonly TheoryData<string, IResampler> AllReSamplers =
new TheoryData<string, IResampler>
{
{ "Bicubic", new BicubicResampler() },
{ "Triangle", new TriangleResampler() },
{ "NearestNeighbor", new NearestNeighborResampler() },
{ "Box", new BoxResampler() },
{ "Lanczos3", new Lanczos3Resampler() },
{ "Lanczos5", new Lanczos5Resampler() },
{ "MitchellNetravali", new MitchellNetravaliResampler() },
{ "Lanczos8", new Lanczos8Resampler() },
{ "Hermite", new HermiteResampler() },
{ "Spline", new SplineResampler() },
{ "Robidoux", new RobidouxResampler() },
{ "RobidouxSharp", new RobidouxSharpResampler() },
{ "Welch", new WelchResampler() }
{ "Bicubic", KnownResamplers.Bicubic },
{ "Triangle", KnownResamplers.Triangle},
{ "NearestNeighbor", KnownResamplers.NearestNeighbor },
{ "Box", KnownResamplers.Box },
// { "Lanczos2", KnownResamplers.Lanczos2 }, TODO: Add expected file
{ "Lanczos3", KnownResamplers.Lanczos3 },
{ "Lanczos5", KnownResamplers.Lanczos5 },
{ "MitchellNetravali", KnownResamplers.MitchellNetravali },
{ "Lanczos8", KnownResamplers.Lanczos8 },
{ "Hermite", KnownResamplers.Hermite },
{ "Spline", KnownResamplers.Spline },
{ "Robidoux", KnownResamplers.Robidoux },
{ "RobidouxSharp", KnownResamplers.RobidouxSharp },
{ "Welch", KnownResamplers.Welch }
};
[Theory]
@ -105,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
var sourceRectangle = new Rectangle(image.Width / 8, image.Height / 8, image.Width / 4, image.Height / 4);
var destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.Mutate(x => x.Resize(image.Width, image.Height, new BicubicResampler(), sourceRectangle, destRectangle, false));
image.Mutate(x => x.Resize(image.Width, image.Height, KnownResamplers.Bicubic, sourceRectangle, destRectangle, false));
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
@ -286,7 +287,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[InlineData(2, 0)]
public static void BicubicWindowOscillatesCorrectly(float x, float expected)
{
var sampler = new BicubicResampler();
var sampler = KnownResamplers.Bicubic;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
@ -300,7 +301,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[InlineData(2, 0)]
public static void TriangleWindowOscillatesCorrectly(float x, float expected)
{
var sampler = new TriangleResampler();
var sampler = KnownResamplers.Triangle;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
@ -314,7 +315,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[InlineData(2, 0)]
public static void Lanczos3WindowOscillatesCorrectly(float x, float expected)
{
var sampler = new Lanczos3Resampler();
var sampler = KnownResamplers.Lanczos3;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);
@ -328,7 +329,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[InlineData(4, 0)]
public static void Lanczos5WindowOscillatesCorrectly(float x, float expected)
{
var sampler = new Lanczos5Resampler();
var sampler = KnownResamplers.Lanczos5;
float result = sampler.GetValue(x);
Assert.Equal(result, expected);

18
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -8,13 +8,15 @@ using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using System;
using System.Reflection;
public class RotateTests : FileTestBase
{
public static readonly TheoryData<float> RotateFloatValues
public static readonly TheoryData<float> RotateAngles
= new TheoryData<float>
{
170,
-170
50, -50, 170, -170
};
public static readonly TheoryData<RotateType> RotateEnumValues
@ -25,11 +27,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
RotateType.Rotate180,
RotateType.Rotate270
};
[Theory]
[WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateFloatValues), 50, 100, DefaultPixelType)]
public void Rotate<TPixel>(TestImageProvider<TPixel> provider, float value)
[WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
@ -38,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
image.DebugSave(provider, value);
}
}
[Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)]

58
tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs

@ -6,17 +6,43 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using System;
using System.Collections.Generic;
using System.Reflection;
using SixLabors.ImageSharp.Processing;
public class SkewTest : FileTestBase
{
public static readonly TheoryData<float, float> SkewValues
= new TheoryData<float, float>
= new TheoryData<float, float>
{
{ 20, 10 },
{ -20, -10 }
};
public static readonly List<string> ResamplerNames
= new List<string>
{
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box),
nameof(KnownResamplers.CatmullRom),
nameof(KnownResamplers.Hermite),
nameof(KnownResamplers.Lanczos2),
nameof(KnownResamplers.Lanczos3),
nameof(KnownResamplers.Lanczos5),
nameof(KnownResamplers.Lanczos8),
nameof(KnownResamplers.MitchellNetravali),
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Robidoux),
nameof(KnownResamplers.RobidouxSharp),
nameof(KnownResamplers.Spline),
nameof(KnownResamplers.Triangle),
nameof(KnownResamplers.Welch),
};
[Theory]
[WithFileCollection(nameof(DefaultFiles), nameof(SkewValues), DefaultPixelType)]
[WithTestPatternImages(nameof(SkewValues), 100, 50, DefaultPixelType)]
public void ImageShouldSkew<TPixel>(TestImageProvider<TPixel> provider, float x, float y)
where TPixel : struct, IPixel<TPixel>
{
@ -26,5 +52,33 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
image.DebugSave(provider, string.Join("_", x, y));
}
}
[Theory]
[WithTestPatternImages(nameof(SkewValues), 100, 50, DefaultPixelType)]
public void ImageShouldSkewWithSampler<TPixel>(TestImageProvider<TPixel> provider, float x, float y)
where TPixel : struct, IPixel<TPixel>
{
foreach (string resamplerName in ResamplerNames)
{
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(i => i.Skew(x, y, sampler));
image.DebugSave(provider, string.Join("_", x, y, resamplerName));
}
}
}
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property == null)
{
throw new Exception("Invalid property name!");
}
return (IResampler)property.GetValue(null);
}
}
}

258
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs

@ -0,0 +1,258 @@
using System;
using System.Numerics;
using System.Reflection;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
using Xunit;
using Xunit.Abstractions;
using SixLabors.ImageSharp.Helpers;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public class AffineTransformTests
{
private readonly ITestOutputHelper Output;
/// <summary>
/// angleDeg, sx, sy, tx, ty
/// </summary>
public static readonly TheoryData<float, float, float, float, float> TransformValues
= new TheoryData<float, float, float, float, float>
{
{ 0, 1, 1, 0, 0 },
{ 50, 1, 1, 0, 0 },
{ 0, 1, 1, 20, 10 },
{ 50, 1, 1, 20, 10 },
{ 0, 1, 1, -20, -10 },
{ 50, 1, 1, -20, -10 },
{ 50, 1.5f, 1.5f, 0, 0 },
{ 50, 1.1F, 1.3F, 30, -20 },
{ 0, 2f, 1f, 0, 0 },
{ 0, 1f, 2f, 0, 0 },
};
public static readonly TheoryData<string> ResamplerNames =
new TheoryData<string>
{
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Box),
nameof(KnownResamplers.CatmullRom),
nameof(KnownResamplers.Hermite),
nameof(KnownResamplers.Lanczos2),
nameof(KnownResamplers.Lanczos3),
nameof(KnownResamplers.Lanczos5),
nameof(KnownResamplers.Lanczos8),
nameof(KnownResamplers.MitchellNetravali),
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Robidoux),
nameof(KnownResamplers.RobidouxSharp),
nameof(KnownResamplers.Spline),
nameof(KnownResamplers.Triangle),
nameof(KnownResamplers.Welch),
};
public static readonly TheoryData<string> Transform_DoesNotCreateEdgeArtifacts_ResamplerNames =
new TheoryData<string>
{
nameof(KnownResamplers.NearestNeighbor),
nameof(KnownResamplers.Triangle),
nameof(KnownResamplers.Bicubic),
nameof(KnownResamplers.Lanczos8),
};
public AffineTransformTests(ITestOutputHelper output)
{
this.Output = output;
}
/// <summary>
/// The output of an "all white" image should be "all white" or transparent, regardless of the transformation and the resampler.
/// </summary>
[Theory]
[WithSolidFilledImages(nameof(Transform_DoesNotCreateEdgeArtifacts_ResamplerNames), 5, 5, 255, 255, 255, 255, PixelTypes.Rgba32)]
public void Transform_DoesNotCreateEdgeArtifacts<TPixel>(TestImageProvider<TPixel> provider, string resamplerName)
where TPixel : struct, IPixel<TPixel>
{
IResampler resampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F));
var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F);
Rectangle sourceRectangle = image.Bounds();
Matrix3x2 matrix = rotate * translate;
Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix);
image.Mutate(c => c.Transform(matrix, resampler, destRectangle));
image.DebugSave(provider, resamplerName);
VerifyAllPixelsAreWhiteOrTransparent(image);
}
}
[Theory]
[WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)]
public void Transform_RotateScaleTranslate<TPixel>(
TestImageProvider<TPixel> provider,
float angleDeg,
float sx, float sy,
float tx, float ty)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg);
var translate = Matrix3x2.CreateTranslation(tx, ty);
var scale = Matrix3x2.CreateScale(sx, sy);
Matrix3x2 m = rotate * scale * translate;
this.PrintMatrix(m);
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
string testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})";
image.DebugSave(provider, testOutputDetails);
image.CompareToReferenceOutput(provider, testOutputDetails);
}
}
[Theory]
[WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)]
public void Transform_RotateScale_ManuallyCentered<TPixel>(TestImageProvider<TPixel> provider, float angleDeg, float s)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image);
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
string testOutputDetails = $"R({angleDeg})_S({s})";
image.DebugSave(provider, testOutputDetails);
image.CompareToReferenceOutput(provider, testOutputDetails);
}
}
public static readonly TheoryData<int, int, int, int> Transform_IntoRectangle_Data =
new TheoryData<int, int, int, int>
{
{ 0, 0, 10, 10 },
{ 0, 0, 5, 10 },
{ 0, 0, 10, 5 },
{ 5, 0, 5, 10 },
{-5,-5, 20, 20 }
};
/// <summary>
/// Testing transforms using custom source rectangles:
/// https://github.com/SixLabors/ImageSharp/pull/386#issuecomment-357104963
/// </summary>
[Theory]
[WithTestPatternImages(96, 48, PixelTypes.Rgba32)]
public void Transform_FromSourceRectangle1<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(48, 0, 96, 36);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(2.0F, 1.5F);
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
}
[Theory]
[WithTestPatternImages(96, 48, PixelTypes.Rgba32)]
public void Transform_FromSourceRectangle2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(0, 24, 48, 48);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(1.0F, 2.0F);
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
}
[Theory]
[WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)]
public void Transform_WithSampler<TPixel>(TestImageProvider<TPixel> provider, string resamplerName)
where TPixel : struct, IPixel<TPixel>
{
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image);
image.Mutate(i =>
{
i.Transform(m, sampler);
});
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(provider, resamplerName);
}
}
private Matrix3x2 MakeManuallyCenteredMatrix<TPixel>(float angleDeg, float s, Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg);
Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height);
var translate = Matrix3x2.CreateTranslation(-toCenter);
var translateBack = Matrix3x2.CreateTranslation(toCenter);
var scale = Matrix3x2.CreateScale(s);
Matrix3x2 m = translate * rotate * scale * translateBack;
this.PrintMatrix(m);
return m;
}
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property == null)
{
throw new Exception("Invalid property name!");
}
return (IResampler)property.GetValue(null);
}
private static void VerifyAllPixelsAreWhiteOrTransparent<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
TPixel[] data = new TPixel[image.Width * image.Height];
image.Frames.RootFrame.SavePixelData(data);
var rgba = default(Rgba32);
var white = new Rgb24(255, 255, 255);
foreach (TPixel pixel in data)
{
pixel.ToRgba32(ref rgba);
if (rgba.A == 0) continue;
Assert.Equal(white, rgba.Rgb);
}
}
private void PrintMatrix(Matrix3x2 a)
{
string s = $"{a.M11:F10},{a.M12:F10},{a.M21:F10},{a.M22:F10},{a.M31:F10},{a.M32:F10}";
this.Output.WriteLine(s);
}
}
}

9
tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs

@ -25,14 +25,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
[InlineData(RotateType.Rotate90, FlipType.Vertical, 90)]
[InlineData(RotateType.Rotate180, FlipType.Vertical, 180)]
[InlineData(RotateType.Rotate270, FlipType.Vertical, 270)]
public void Rotate_degreesFloat_RotateProcessorWithAnglesSetAndExpandTrue(RotateType angle, FlipType flip, float expectedAngle)
public void Rotate_degreesFloat_RotateProcessorWithAnglesSetrue(RotateType angle, FlipType flip, float expectedAngle)
{
this.operations.RotateFlip(angle, flip);
var rotateProcessor = this.Verify<RotateProcessor<Rgba32>>(0);
var flipProcessor = this.Verify<FlipProcessor<Rgba32>>(1);
RotateProcessor<Rgba32> rotateProcessor = this.Verify<RotateProcessor<Rgba32>>(0);
FlipProcessor<Rgba32> flipProcessor = this.Verify<FlipProcessor<Rgba32>>(1);
Assert.Equal(expectedAngle, rotateProcessor.Angle);
Assert.False(rotateProcessor.Expand);
Assert.Equal(expectedAngle, rotateProcessor.Degrees);
Assert.Equal(flip, flipProcessor.FlipType);
}
}

31
tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors;
using Xunit;
@ -10,46 +9,28 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public class RotateTests : BaseImageOperationsExtensionTest
{
[Theory]
[InlineData(85.6f)]
[InlineData(21)]
public void Rotate_degreesFloat_RotateProcessorWithAnglesSetAndExpandTrue(float angle)
public void RotateDegreesFloatRotateProcessorWithAnglesSet(float angle)
{
this.operations.Rotate(angle);
var processor = this.Verify<RotateProcessor<Rgba32>>();
RotateProcessor<Rgba32> processor = this.Verify<RotateProcessor<Rgba32>>();
Assert.Equal(angle, processor.Angle);
Assert.True(processor.Expand);
Assert.Equal(angle, processor.Degrees);
}
[Theory]
[InlineData(RotateType.None, 0)]
[InlineData(RotateType.Rotate90, 90)]
[InlineData(RotateType.Rotate180, 180)]
[InlineData(RotateType.Rotate270, 270)]
public void Rotate_RotateType_RotateProcessorWithAnglesConvertedFromEnumAndExpandTrue(RotateType angle, float expectedangle)
public void RotateRotateTypeRotateProcessorWithAnglesConvertedFromEnum(RotateType angle, float expectedangle)
{
this.operations.Rotate(angle); // is this api needed ???
var processor = this.Verify<RotateProcessor<Rgba32>>();
Assert.Equal(expectedangle, processor.Angle);
Assert.False(processor.Expand);
}
[Theory]
[InlineData(85.6f, false)]
[InlineData(21, true)]
[InlineData(21, false)]
public void Rotate_degreesFloat_expand_RotateProcessorWithAnglesSetAndExpandSet(float angle, bool expand)
{
this.operations.Rotate(angle, expand);
var processor = this.Verify<RotateProcessor<Rgba32>>();
RotateProcessor<Rgba32> processor = this.Verify<RotateProcessor<Rgba32>>();
Assert.Equal(angle, processor.Angle);
Assert.Equal(expand, processor.Expand);
Assert.Equal(expectedangle, processor.Degrees);
}
}
}

21
tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using Xunit;
@ -10,25 +9,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public class SkewTest : BaseImageOperationsExtensionTest
{
[Fact]
public void Skew_x_y_CreateSkewProcessorWithAnglesSetAndExpandTrue()
public void SkewXYCreateSkewProcessorWithAnglesSet()
{
this.operations.Skew(10, 20);
var processor = this.Verify<SkewProcessor<Rgba32>>();
SkewProcessor<Rgba32> processor = this.Verify<SkewProcessor<Rgba32>>();
Assert.Equal(10, processor.AngleX);
Assert.Equal(20, processor.AngleY);
Assert.True(processor.Expand);
}
[Fact]
public void Skew_x_y_expand_CreateSkewProcessorWithAnglesSetAndExpandTrue()
{
this.operations.Skew(10, 20, false);
var processor = this.Verify<SkewProcessor<Rgba32>>();
Assert.Equal(10, processor.AngleX);
Assert.Equal(20, processor.AngleY);
Assert.False(processor.Expand);
Assert.Equal(10, processor.DegreesX);
Assert.Equal(20, processor.DegreesY);
}
}
}

80
tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs

@ -6,6 +6,8 @@ using System.Reflection;
namespace SixLabors.ImageSharp.Tests
{
using SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which produce an image of size width * height filled with the requested color.
/// One <see cref="TestImageProvider{TPixel}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
@ -56,14 +58,88 @@ namespace SixLabors.ImageSharp.Tests
byte a,
PixelTypes pixelTypes,
params object[] additionalParameters)
: base(width, height, pixelTypes, additionalParameters)
: this(null, width, height, r, g, b, a, pixelTypes, additionalParameters)
{
}
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which produce an image of size width * height filled with the requested color.
/// One <see cref="TestImageProvider{TPixel}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
/// <param name="memberData">The member data to apply to theories</param>
/// <param name="width">The width of the requested image</param>
/// <param name="height">The height of the requested image</param>
/// <param name="r">Red</param>
/// <param name="g">Green</param>
/// <param name="b">Blue</param>
/// /// <param name="a">Alpha</param>
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithSolidFilledImagesAttribute(
string memberData,
int width,
int height,
byte r,
byte g,
byte b,
byte a,
PixelTypes pixelTypes,
params object[] additionalParameters)
: base(memberData, width, height, pixelTypes, additionalParameters)
{
this.R = r;
this.G = g;
this.B = b;
this.A = a;
}
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which produce an image of size width * height filled with the requested color.
/// One <see cref="TestImageProvider{TPixel}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
/// <param name="width">The width of the requested image</param>
/// <param name="height">The height of the requested image</param>
/// <param name="colorName">The referenced color name (name of property in <see cref="NamedColors{TPixel}")/></param>
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithSolidFilledImagesAttribute(
int width,
int height,
string colorName,
PixelTypes pixelTypes,
params object[] additionalParameters)
: this(null, width, height, colorName, pixelTypes, additionalParameters)
{
}
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which produce an image of size width * height filled with the requested color.
/// One <see cref="TestImageProvider{TPixel}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
/// <param name="memberData">The member data to apply to theories</param>
/// <param name="width">The width of the requested image</param>
/// <param name="height">The height of the requested image</param>
/// <param name="colorName">The referenced color name (name of property in <see cref="NamedColors{TPixel}")/></param>
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithSolidFilledImagesAttribute(
string memberData,
int width,
int height,
string colorName,
PixelTypes pixelTypes,
params object[] additionalParameters)
: base(memberData, width, height, pixelTypes, additionalParameters)
{
Guard.NotNull(colorName, nameof(colorName));
var c = (Rgba32)typeof(Rgba32).GetTypeInfo().GetField(colorName).GetValue(null);
this.R = c.R;
this.G = c.G;
this.B = c.B;
this.A = c.A;
}
/// <summary>
/// Red
/// </summary>

2
tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: this(null, width, height, pixelTypes,additionalParameters)
: this(null, width, height, pixelTypes, additionalParameters)
{
}

Loading…
Cancel
Save