From e79f26bc7ec9cfca5bedb2309d7b7486c9da79ea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 5 Nov 2018 10:55:22 +0000 Subject: [PATCH 01/28] Add base becnhmark --- .../InterpolatedTransformProcessorBase.cs | 5 +-- .../ImageSharp.Benchmarks/Samplers/Rotate.cs | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Samplers/Rotate.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs index c1abb4a5e1..4737a4102c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int i = 0; i < length; i++) { ref float wRef = ref Unsafe.Add(ref weightsRef, i); - wRef = wRef / sum; + wRef /= sum; } } } @@ -90,8 +90,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) { - float weight = sampler.GetValue(i - point); - Unsafe.Add(ref weightsRef, x) = weight; + Unsafe.Add(ref weightsRef, x) = sampler.GetValue(i - point); } } diff --git a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs new file mode 100644 index 0000000000..c1456f9d77 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs @@ -0,0 +1,39 @@ +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Samplers +{ + [Config(typeof(Config.ShortClr))] + public class Rotate + { + [Benchmark] + public Size DoRotate() + { + using (var image = new Image(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) + { + image.Mutate(x => x.Rotate(37.5F)); + + return image.Size(); + } + } + } +} + +// Nov 4 2018 +//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 +//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores +//.NET Core SDK = 2.1.403 + +// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT +// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 +// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT + +//LaunchCount=1 TargetCount=3 WarmupCount=3 + +// #### BEFORE ####: +// Method | Runtime | Mean | Error | StdDev | Allocated | +//--------- |-------- |---------:|----------:|----------:|----------:| +// DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB | +// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | \ No newline at end of file From dfc926e5692cadfc5c5b0d3b59cdcea9225ba7fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 6 Nov 2018 17:27:21 +0000 Subject: [PATCH 02/28] Better Rotate --- .../Transforms/AffineTransformProcessor.cs | 204 ++++----------- .../Transforms/AffineTransformProcessorOld.cs | 239 ++++++++++++++++++ .../CenteredAffineTransformProcessor.cs | 2 +- .../Processors/Transforms/RotateProcessor.cs | 16 +- .../Transforms/TransformKernelMap.cs | 188 ++++++++++++++ .../Processors/Transforms/TransformUtils.cs | 121 +++++++++ .../Processing/TransformExtensions.cs | 6 +- 7 files changed, 611 insertions(+), 165 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index e12b91eab9..edddab1811 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -5,13 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -20,29 +16,42 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides the base methods to perform affine transforms on an image. /// /// The pixel format. - internal class AffineTransformProcessor : InterpolatedTransformProcessorBase + internal class AffineTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { + private readonly Rectangle transformedRectangle; + /// /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) + /// The source image size + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) { + Guard.NotNull(sampler, nameof(sampler)); + this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; + this.transformedRectangle = TransformUtils.GetTransformedRectangle( + new Rectangle(Point.Empty, sourceSize), + matrix); + + // We want to resize the canvas here taking into account any translations. + this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); } /// - /// Gets the matrix used to supply the affine transform + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + + /// + /// Gets the matrix used to supply the affine transform. /// public Matrix3x2 TransformMatrix { get; } /// - /// Gets the target dimensions to constrain the transformed image to + /// Gets the target dimensions to constrain the transformed image to. /// public Size TargetDimensions { get; } @@ -68,13 +77,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms 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); + var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. - Matrix3x2.Invert(matrix, out matrix); + Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 matrix); if (this.Sampler is NearestNeighborResampler) { @@ -82,158 +88,52 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms targetBounds, configuration, rows => + { + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span destRow = destination.GetPixelRowSpan(y); + Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) + for (int x = 0; x < width; x++) + { + var point = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(point.X, point.Y)) { - var point = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(point.X, point.Y)) - { - destRow[x] = source[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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler)) { - ParallelHelper.IterateRows( + ParallelHelper.IterateRowsWithTempBuffer( targetBounds, configuration, - rows => + (rows, vectorBuffer) => + { + Span vectorSpan = vectorBuffer.Span; + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); + Span targetRowSpan = destination.GetPixelRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, targetRowSpan, vectorSpan); + ref float ySpanRef = ref kernel.GetYStartReference(y); + ref float xSpanRef = ref kernel.GetXStartReference(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. - // Precalculating 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } + 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); + kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); } - }); + + PixelOperations.Instance.FromVector4(configuration, vectorSpan, targetRowSpan); + } + }); } } - - /// - /// Gets a transform matrix adjusted for final processing based upon the target image bounds. - /// - /// The source image bounds. - /// The destination image bounds. - /// - /// The . - /// - protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => this.TransformMatrix; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs new file mode 100644 index 0000000000..5891afd9a6 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs @@ -0,0 +1,239 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Provides the base methods to perform affine transforms on an image. + /// + /// The pixel format. + internal class AffineTransformProcessorOld : InterpolatedTransformProcessorBase + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + /// The target dimensions to constrain the transformed image to. + public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) + : base(sampler) + { + this.TransformMatrix = matrix; + this.TargetDimensions = targetDimensions; + } + + /// + /// Gets the matrix used to supply the affine transform + /// + public Matrix3x2 TransformMatrix { get; } + + /// + /// Gets the target dimensions to constrain the transformed image to + /// + public Size TargetDimensions { get; } + + /// + protected override Image CreateDestination(Image source, Rectangle sourceRectangle) + { + // We will always be creating the clone even for mutate because we may need to resize the canvas + IEnumerable> frames = + source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); + + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); + } + + /// + protected override void OnFrameApply( + ImageFrame source, + ImageFrame 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); + + // Convert from screen to world space. + Matrix3x2.Invert(matrix, out matrix); + + if (this.Sampler is NearestNeighborResampler) + { + ParallelHelper.IterateRows( + targetBounds, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span 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); + + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + + using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) + using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + { + ParallelHelper.IterateRows( + targetBounds, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(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. + // Precalculating 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, + ref ySpanRef, + yLength); + + CalculateWeightsDown( + left, + right, + minX, + maxX, + point.X, + sampler, + xScale, + ref xSpanRef, + xLength); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); + } + + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) + { + float yWeight = Unsafe.Add(ref ySpanRef, yy); + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = Unsafe.Add(ref xSpanRef, xx); + + // Values are first premultiplied to prevent darkening of edge pixels + var current = source[i, j].ToVector4(); + Vector4Utils.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + + // Reverse the premultiplication + Vector4Utils.UnPremultiply(ref sum); + dest.FromVector4(sum); + } + } + }); + } + } + + /// + /// Gets a transform matrix adjusted for final processing based upon the target image bounds. + /// + /// The source image bounds. + /// The destination image bounds. + /// + /// The . + /// + protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) + => this.TransformMatrix; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs index adaee17665..82614dc8c2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// A base class that provides methods to allow the automatic centering of affine transforms /// /// The pixel format. - internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessor + internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessorOld where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 2ad626755c..cbf82cc9b0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -2,12 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -16,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : CenteredAffineTransformProcessor + internal class RotateProcessor : AffineTransformProcessor where TPixel : struct, IPixel { /// @@ -36,10 +34,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the rotating operation. /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) - : base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler, sourceSize) - { - this.Degrees = degrees; - } + : base( + TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize), + sampler, + sourceSize) + => this.Degrees = degrees; /// /// Gets the angle of rotation in degrees. @@ -84,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The private static float WrapDegrees(float degrees) { - degrees = degrees % 360; + degrees %= 360; while (degrees < 0) { @@ -223,7 +222,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int newX = height - y - 1; for (int x = 0; x < width; x++) { - // TODO: Optimize this: if (destinationBounds.Contains(newX, x)) { destination[newX, x] = sourceRow[x]; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs new file mode 100644 index 0000000000..5acc7213cb --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +// TODO: It would be great if we could somehow optimize this to calculate the weights once. +// currently we cannot do that as we are calulating the weight of the transformed point dimension +// not the point in the original image. +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Contains the methods required to calculate kernel sampling weights on-the-fly. + /// + internal class TransformKernelMap : IDisposable + { + private readonly Buffer2D yBuffer; + private readonly Buffer2D xBuffer; + private readonly float yScale; + private readonly float xScale; + private readonly int yLength; + private readonly int xLength; + private readonly Vector2 extents; + private Vector4 maxSourceExtents; + private readonly IResampler sampler; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The source size. + /// The destination size. + /// The sampler. + public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) + { + this.sampler = sampler; + (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + + this.yScale = yRadiusScale.scale; + this.xScale = xRadiusScale.scale; + this.extents = new Vector2(xRadiusScale.radius, yRadiusScale.radius); + this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); + this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); + + // We use 2D buffers so that we can access the weight spans in parallel. + this.yBuffer = configuration.MemoryAllocator.Allocate2D(this.yLength, destination.Height); + this.xBuffer = configuration.MemoryAllocator.Allocate2D(this.xLength, destination.Height); + + int maxX = source.Width - 1; + int maxY = source.Height - 1; + this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY); + } + + /// + /// Gets a reference to the first item of the y window. + /// + /// The reference to the first item of the window. + [MethodImpl(InliningOptions.ShortMethod)] + public ref float GetYStartReference(int y) + => ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y)); + + /// + /// Gets a reference to the first item of the x window. + /// + /// The reference to the first item of the window. + [MethodImpl(InliningOptions.ShortMethod)] + public ref float GetXStartReference(int y) + => ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y)); + + public void Convolve( + Vector2 transformedPoint, + int column, + ref float ySpanRef, + ref float xSpanRef, + Buffer2D sourcePixels, + Span targetRow) + where TPixel : struct, IPixel + { + // Clamp sampling pixel radial extents to the source image edges + Vector2 minXY = transformedPoint - this.extents; + Vector2 maxXY = transformedPoint + this.extents; + + // minX, minY, maxX, maxY + var extents = new Vector4( + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F), + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F)); + + int left = (int)extents.X; + int top = (int)extents.Y; + int right = (int)extents.Z; + int bottom = (int)extents.W; + + extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); + + int minX = (int)extents.X; + int minY = (int)extents.Y; + int maxX = (int)extents.Z; + int maxY = (int)extents.W; + + if (minX == maxX || minY == maxY) + { + return; + } + + // TODO: Get Anton to use his superior brain on this one. + // It looks to me like we're calculating the same weights over and over again + // since min(X+Y) and max(X+Y) are the same distance apart. + this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef); + this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef); + + Vector4 sum = Vector4.Zero; + for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++) + { + float yWeight = Unsafe.Add(ref ySpanRef, kernelY); + + for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++) + { + float xWeight = Unsafe.Add(ref xSpanRef, kernelX); + + // Values are first premultiplied to prevent darkening of edge pixels. + var current = sourcePixels[x, y].ToVector4(); + Vector4Utils.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + // Reverse the premultiplication + Vector4Utils.UnPremultiply(ref sum); + targetRow[column] = sum; + } + + /// + /// Calculated the normalized weights for the given point. + /// + /// The minimum sampling offset + /// The maximum sampling offset + /// The length of the weights collection + /// The transformed point dimension + /// The reference to the collection of weights + [MethodImpl(InliningOptions.ShortMethod)] + private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef) + { + float sum = 0; + for (int x = 0, i = min; i <= max; i++, x++) + { + float weight = this.sampler.GetValue(i - point); + sum += weight; + Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point); + } + + // TODO: Do we need this? Check what happens when we scale an image down. + // if (sum > 0) + // { + // for (int i = 0; i < length; i++) + // { + // ref float wRef = ref Unsafe.Add(ref weightsRef, i); + // wRef /= sum; + // } + // } + } + + private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + { + float scale = (float)sourceSize / destinationSize; + + if (scale < 1F) + { + scale = 1F; + } + + return (MathF.Ceiling(scale * this.sampler.Radius), scale); + } + + public void Dispose() + { + this.yBuffer?.Dispose(); + this.xBuffer?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs new file mode 100644 index 0000000000..7d03502499 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -0,0 +1,121 @@ +// 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.Processing.Processors.Transforms +{ + /// + /// Contains utility methods for working with transforms. + /// + public static class TransformUtils + { + /// + /// Creates a centered rotation matrix using the given rotation in degrees and the source size. + /// + /// The amount of rotation, in degrees. + /// The source image size. + /// The . + public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + + /// + /// Gets the centered transform matrix based upon the source and destination rectangles. + /// + /// The source image bounds. + /// The transformation matrix. + /// The + public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) + { + Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + + // We invert the matrix to handle the transformation from screen to world space. + // This ensures scaling matrices are correct. + Matrix3x2.Invert(matrix, out Matrix3x2 inverted); + + var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + + // Translate back to world space. + Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); + + return centered; + } + + /// + /// Gets the centered transform matrix based upon the source and destination rectangles. + /// + /// The source image bounds. + /// The destination image bounds. + /// The transformation matrix. + /// The + public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) + { + // We invert the matrix to handle the transformation from screen to world space. + // This ensures scaling matrices are correct. + Matrix3x2.Invert(matrix, out Matrix3x2 inverted); + + var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + + // Translate back to world space. + Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); + + return centered; + } + + /// + /// Returns the rectangle bounds relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) + { + Rectangle transformed = GetTransformedRectangle(rectangle, matrix); + + // TODO: Check this. + return new Rectangle(0, 0, transformed.Width, transformed.Height); + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) + { + if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) + { + return rectangle; + } + + var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), 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 left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); + float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); + float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); + float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); + + return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); + } + } +} diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 0ec1e295d9..3f24969a3d 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, source.GetCurrentSize())); + => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, source.GetCurrentSize())); /// /// Transforms an image by the given matrix using the specified sampling algorithm @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing { var t = Matrix3x2.CreateTranslation(-rectangle.Location); Matrix3x2 combinedMatrix = t * matrix; - return source.ApplyProcessor(new AffineTransformProcessor(combinedMatrix, sampler, rectangle.Size)); + return source.ApplyProcessor(new AffineTransformProcessorOld(combinedMatrix, sampler, rectangle.Size)); } /// @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Processing IResampler sampler, Size destinationSize) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, destinationSize)); + => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, destinationSize)); /// /// Transforms an image by the given matrix. From 2afd0d572970a1dbbf48e0707ddab23ae86ec648 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 7 Nov 2018 00:00:41 +0000 Subject: [PATCH 03/28] Cleanup TransformKernel and handle negative transform dimensions --- .../Transforms/AffineTransformProcessor.cs | 6 ++++++ .../Processors/Transforms/TransformKernelMap.cs | 15 ++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index edddab1811..f63baa95c6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -38,6 +38,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // We want to resize the canvas here taking into account any translations. this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); + + // Handle a negative translation that exceeds the original with of the image. + if (this.TargetDimensions.Width <= 0 || this.TargetDimensions.Height <= 0) + { + this.TargetDimensions = sourceSize; + } } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs index 5acc7213cb..531edbc45b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -21,8 +21,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { private readonly Buffer2D yBuffer; private readonly Buffer2D xBuffer; - private readonly float yScale; - private readonly float xScale; private readonly int yLength; private readonly int xLength; private readonly Vector2 extents; @@ -39,12 +37,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) { this.sampler = sampler; - (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); - (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + float yRadius = this.GetSamplingRadius(source.Height, destination.Height); + float xRadius = this.GetSamplingRadius(source.Width, destination.Width); - this.yScale = yRadiusScale.scale; - this.xScale = xRadiusScale.scale; - this.extents = new Vector2(xRadiusScale.radius, yRadiusScale.radius); + this.extents = new Vector2(xRadius, yRadius); this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); @@ -167,7 +163,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // } } - private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + [MethodImpl(InliningOptions.ShortMethod)] + private float GetSamplingRadius(int sourceSize, int destinationSize) { float scale = (float)sourceSize / destinationSize; @@ -176,7 +173,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms scale = 1F; } - return (MathF.Ceiling(scale * this.sampler.Radius), scale); + return MathF.Ceiling(scale * this.sampler.Radius); } public void Dispose() From 8205216dfc07f4ea6ebafcbbbbc391c8426173e7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 14:20:52 +0000 Subject: [PATCH 04/28] Introduce AffineTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 177 +++++++++++++ .../Transforms/AffineTransformProcessor.cs | 33 ++- .../Transforms/AffineTransformProcessorOld.cs | 239 ------------------ .../CenteredAffineTransformProcessor.cs | 38 --- .../Processors/Transforms/RotateProcessor.cs | 2 +- .../Processors/Transforms/SkewProcessor.cs | 8 +- .../Processors/Transforms/TransformHelpers.cs | 20 -- .../Transforms/TransformKernelMap.cs | 56 ++-- .../Processors/Transforms/TransformUtils.cs | 78 ++++-- .../Processing/TransformExtensions.cs | 54 +--- .../ImageSharp.Benchmarks/Samplers/Rotate.cs | 10 +- tests/ImageSharp.Benchmarks/Samplers/Skew.cs | 45 ++++ .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 22 +- .../Transforms/AffineTransformTests.cs | 54 ++-- 14 files changed, 360 insertions(+), 476 deletions(-) create mode 100644 src/ImageSharp/Processing/AffineTransformBuilder.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs create mode 100644 tests/ImageSharp.Benchmarks/Samplers/Skew.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs new file mode 100644 index 0000000000..e5ce1450f7 --- /dev/null +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// A helper class for constructing instances for use in affine transforms. + /// + public class AffineTransformBuilder + { + private readonly List matrices = new List(); + private Rectangle rectangle; + + /// + /// Initializes a new instance of the class. + /// + /// The source image size. + public AffineTransformBuilder(Size sourceSize) => this.Size = sourceSize; + + /// + /// Initializes a new instance of the class. + /// + /// The source rectangle. + public AffineTransformBuilder(Rectangle sourceRectangle) + : this(sourceRectangle.Size) + => this.rectangle = sourceRectangle; + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) + { + this.matrices.Insert(0, TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + return this; + } + + /// + /// Gets the source image size. + /// + internal Size Size { get; } + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) + { + this.matrices.Add(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + return this; + } + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScaleMatrix(SizeF scales) + { + this.matrices.Insert(0, Matrix3x2Extensions.CreateScale(scales)); + return this; + } + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScaleMatrix(SizeF scales) + { + this.matrices.Add(Matrix3x2Extensions.CreateScale(scales)); + return this; + } + + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) + { + this.matrices.Insert(0, TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + return this; + } + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) + { + this.matrices.Add(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + return this; + } + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslationMatrix(PointF position) + { + this.matrices.Insert(0, Matrix3x2Extensions.CreateTranslation(position)); + return this; + } + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslationMatrix(PointF position) + { + this.matrices.Add(Matrix3x2Extensions.CreateTranslation(position)); + return this; + } + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// The . + public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) + { + this.matrices.Insert(0, matrix); + return this; + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// The . + public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) + { + this.matrices.Add(matrix); + return this; + } + + /// + /// Returns the combined matrix. + /// + /// The . + public Matrix3x2 BuildMatrix() + { + Matrix3x2 matrix = Matrix3x2.Identity; + + // Translate the origin matrix to cater for source rectangle offsets. + if (!this.rectangle.Equals(default)) + { + matrix *= Matrix3x2.CreateTranslation(-this.rectangle.Location); + } + + foreach (Matrix3x2 m in this.matrices) + { + matrix *= m; + } + + return matrix; + } + + /// + /// Removes all matrices from the builder. + /// + public void Clear() => this.matrices.Clear(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index f63baa95c6..fb42b83341 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -19,8 +19,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal class AffineTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { - private readonly Rectangle transformedRectangle; - /// /// Initializes a new instance of the class. /// @@ -32,18 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.transformedRectangle = TransformUtils.GetTransformedRectangle( - new Rectangle(Point.Empty, sourceSize), - matrix); - - // We want to resize the canvas here taking into account any translations. - this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); - - // Handle a negative translation that exceeds the original with of the image. - if (this.TargetDimensions.Width <= 0 || this.TargetDimensions.Height <= 0) - { - this.TargetDimensions = sourceSize; - } + this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); } /// @@ -79,10 +66,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Rectangle sourceRectangle, Configuration configuration) { + // Handle tranforms that result in output identical to the original. + if (this.TransformMatrix.Equals(Matrix3x2.Identity)) + { + // The cloned will be blank here copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + return; + } + int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. @@ -102,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { var point = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(point.X, point.Y)) + if (sourceRectangle.Contains(point.X, point.Y)) { destRow[x] = source[point.X, point.Y]; } @@ -113,7 +107,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return; } - using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler)) + var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler); + try { ParallelHelper.IterateRowsWithTempBuffer( targetBounds, @@ -140,6 +135,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } }); } + finally + { + kernel.Dispose(); + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs deleted file mode 100644 index 5891afd9a6..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs +++ /dev/null @@ -1,239 +0,0 @@ -// 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.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.ParallelUtils; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// Provides the base methods to perform affine transforms on an image. - /// - /// The pixel format. - internal class AffineTransformProcessorOld : InterpolatedTransformProcessorBase - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) - { - this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; - } - - /// - /// Gets the matrix used to supply the affine transform - /// - public Matrix3x2 TransformMatrix { get; } - - /// - /// Gets the target dimensions to constrain the transformed image to - /// - public Size TargetDimensions { get; } - - /// - protected override Image CreateDestination(Image source, Rectangle sourceRectangle) - { - // We will always be creating the clone even for mutate because we may need to resize the canvas - IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); - - // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); - } - - /// - protected override void OnFrameApply( - ImageFrame source, - ImageFrame 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); - - // Convert from screen to world space. - Matrix3x2.Invert(matrix, out matrix); - - if (this.Sampler is NearestNeighborResampler) - { - ParallelHelper.IterateRows( - targetBounds, - configuration, - rows => - { - for (int y = rows.Min; y < rows.Max; y++) - { - Span 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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) - { - ParallelHelper.IterateRows( - targetBounds, - configuration, - rows => - { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(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. - // Precalculating 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } - } - }); - } - } - - /// - /// Gets a transform matrix adjusted for final processing based upon the target image bounds. - /// - /// The source image bounds. - /// The destination image bounds. - /// - /// The . - /// - protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => this.TransformMatrix; - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs deleted file mode 100644 index 82614dc8c2..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ /dev/null @@ -1,38 +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.Transforms -{ - /// - /// A base class that provides methods to allow the automatic centering of affine transforms - /// - /// The pixel format. - internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessorOld - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The source image size - protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) - : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) - { - } - - /// - protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); - - private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) - { - var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index cbf82cc9b0..57cca4bf9e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : base( - TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize), + TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index a0cfa63794..4a006a9dfe 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -11,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : CenteredAffineTransformProcessor + internal class SkewProcessor : AffineTransformProcessor where TPixel : struct, IPixel { /// @@ -33,7 +32,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the skew operation. /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) - : base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler, sourceSize) + : base( + TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize), + sampler, + sourceSize) { this.DegreesX = degreesX; this.DegreesY = degreesY; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs index b22fa64cfd..2e85f6c2cc 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs @@ -102,26 +102,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return centered; } - /// - /// Returns the bounding rectangle relative to the source for the given transformation matrix. - /// - /// The source rectangle. - /// The transformation matrix. - /// - /// The . - /// - 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); - } - /// /// Returns the bounding rectangle relative to the source for the given transformation matrix. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs index 531edbc45b..573120888f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -9,20 +9,15 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; -// TODO: It would be great if we could somehow optimize this to calculate the weights once. -// currently we cannot do that as we are calulating the weight of the transformed point dimension -// not the point in the original image. namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Contains the methods required to calculate kernel sampling weights on-the-fly. + /// Contains the methods required to calculate transform kernel convolution. /// internal class TransformKernelMap : IDisposable { private readonly Buffer2D yBuffer; private readonly Buffer2D xBuffer; - private readonly int yLength; - private readonly int xLength; private readonly Vector2 extents; private Vector4 maxSourceExtents; private readonly IResampler sampler; @@ -41,12 +36,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float xRadius = this.GetSamplingRadius(source.Width, destination.Width); this.extents = new Vector2(xRadius, yRadius); - this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); - this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); + int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); + int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); - // We use 2D buffers so that we can access the weight spans in parallel. - this.yBuffer = configuration.MemoryAllocator.Allocate2D(this.yLength, destination.Height); - this.xBuffer = configuration.MemoryAllocator.Allocate2D(this.xLength, destination.Height); + // We use 2D buffers so that we can access the weight spans per row in parallel. + this.yBuffer = configuration.MemoryAllocator.Allocate2D(yLength, destination.Height); + this.xBuffer = configuration.MemoryAllocator.Allocate2D(xLength, destination.Height); int maxX = source.Width - 1; int maxY = source.Height - 1; @@ -82,42 +77,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Vector2 minXY = transformedPoint - this.extents; Vector2 maxXY = transformedPoint + this.extents; - // minX, minY, maxX, maxY + // left, top, right, bottom var extents = new Vector4( MathF.Ceiling(minXY.X - .5F), MathF.Ceiling(minXY.Y - .5F), MathF.Floor(maxXY.X + .5F), MathF.Floor(maxXY.Y + .5F)); + extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); + int left = (int)extents.X; int top = (int)extents.Y; int right = (int)extents.Z; int bottom = (int)extents.W; - extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); - - int minX = (int)extents.X; - int minY = (int)extents.Y; - int maxX = (int)extents.Z; - int maxY = (int)extents.W; - - if (minX == maxX || minY == maxY) + if (left == right || top == bottom) { return; } - // TODO: Get Anton to use his superior brain on this one. - // It looks to me like we're calculating the same weights over and over again - // since min(X+Y) and max(X+Y) are the same distance apart. - this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef); - this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef); + this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef); + this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef); Vector4 sum = Vector4.Zero; - for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++) + for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++) { float yWeight = Unsafe.Add(ref ySpanRef, kernelY); - for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++) + for (int kernelX = 0, x = left; x <= right; x++, kernelX++) { float xWeight = Unsafe.Add(ref xSpanRef, kernelX); @@ -138,29 +125,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The minimum sampling offset /// The maximum sampling offset - /// The length of the weights collection /// The transformed point dimension /// The reference to the collection of weights [MethodImpl(InliningOptions.ShortMethod)] - private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef) + private void CalculateWeights(int min, int max, float point, ref float weightsRef) { float sum = 0; for (int x = 0, i = min; i <= max; i++, x++) { float weight = this.sampler.GetValue(i - point); sum += weight; - Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point); + Unsafe.Add(ref weightsRef, x) = weight; } - - // TODO: Do we need this? Check what happens when we scale an image down. - // if (sum > 0) - // { - // for (int i = 0; i < length; i++) - // { - // ref float wRef = ref Unsafe.Add(ref weightsRef, i); - // wRef /= sum; - // } - // } } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 7d03502499..10cf49c34b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Contains utility methods for working with transforms. /// - public static class TransformUtils + internal static class TransformUtils { /// /// Creates a centered rotation matrix using the given rotation in degrees and the source size. @@ -18,43 +18,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The amount of rotation, in degrees. /// The source image size. /// The . - public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size) + public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size) => CreateCenteredTransformMatrix( new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); /// - /// Gets the centered transform matrix based upon the source and destination rectangles. + /// Creates a centered skew matrix from the give angles in degrees and the source size. /// - /// The source image bounds. - /// The transformation matrix. - /// The - public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) - { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); - - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); - - // Translate back to world space. - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - return centered; - } + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The source image size. + /// The . + public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); /// /// Gets the centered transform matrix based upon the source and destination rectangles. /// /// The source image bounds. - /// The destination image bounds. /// The transformation matrix. /// The - public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) + public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) { + Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. Matrix3x2.Invert(matrix, out Matrix3x2 inverted); @@ -79,8 +69,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) { Rectangle transformed = GetTransformedRectangle(rectangle, matrix); - - // TODO: Check this. return new Rectangle(0, 0, transformed.Width, transformed.Height); } @@ -107,6 +95,44 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return GetBoundingRectangle(tl, tr, bl, br); } + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The source size. + /// The transformation matrix. + /// + /// The . + /// + public static Size GetTransformedSize(Size size, Matrix3x2 matrix) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) + { + return size; + } + + Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + + // We want to resize the canvas here taking into account any translations. + int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); + int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); + + // If location in either direction is translated to a negative value equal to or exceeding the + // dimensions in eith direction we need to reassign the dimension. + if (height <= 0) + { + height = rectangle.Height; + } + + if (width <= 0) + { + width = rectangle.Width; + } + + return new Size(width, height); + } + private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) { // Find the minimum and maximum "corners" based on the given vectors diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 3f24969a3d..4cbd1b041f 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -18,65 +18,23 @@ namespace SixLabors.ImageSharp.Processing /// /// The pixel format. /// The image to transform. - /// The transformation matrix. + /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix) + public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder) where TPixel : struct, IPixel - => Transform(source, matrix, KnownResamplers.Bicubic); + => Transform(source, builder, KnownResamplers.Bicubic); /// /// Transforms an image by the given matrix using the specified sampling algorithm. /// /// The pixel format. /// The image to transform. - /// The transformation matrix. - /// The to perform the resampling. - /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, source.GetCurrentSize())); - - /// - /// 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. - /// - /// The pixel format. - /// The image to transform. - /// The transformation matrix. - /// The to perform the resampling. - /// - /// The rectangle defining the transform origin in the source image, and the size of the result image. - /// - /// The - public static IImageProcessingContext Transform( - this IImageProcessingContext source, - Matrix3x2 matrix, - IResampler sampler, - Rectangle rectangle) - where TPixel : struct, IPixel - { - var t = Matrix3x2.CreateTranslation(-rectangle.Location); - Matrix3x2 combinedMatrix = t * matrix; - return source.ApplyProcessor(new AffineTransformProcessorOld(combinedMatrix, sampler, rectangle.Size)); - } - - /// - /// Transforms an image by the given matrix using the specified sampling algorithm, - /// cropping or extending the image according to . - /// - /// The pixel format. - /// The image to transform. - /// The transformation matrix. + /// The affine transform builder. /// The to perform the resampling. - /// The size of the destination image. /// The - public static IImageProcessingContext Transform( - this IImageProcessingContext source, - Matrix3x2 matrix, - IResampler sampler, - Size destinationSize) + public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, destinationSize)); + => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); /// /// Transforms an image by the given matrix. diff --git a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs index c1456f9d77..f898576af0 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers } } -// Nov 4 2018 +// Nov 7 2018 //BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores //.NET Core SDK = 2.1.403 @@ -36,4 +36,10 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers // Method | Runtime | Mean | Error | StdDev | Allocated | //--------- |-------- |---------:|----------:|----------:|----------:| // DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB | -// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | \ No newline at end of file +// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | + +// #### AFTER ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//--------- |-------- |---------:|---------:|---------:|----------:| +// DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB | +// DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB | \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Samplers/Skew.cs b/tests/ImageSharp.Benchmarks/Samplers/Skew.cs new file mode 100644 index 0000000000..84819750af --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Samplers/Skew.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Samplers +{ + [Config(typeof(Config.ShortClr))] + public class Skew + { + [Benchmark] + public Size DoSkew() + { + using (var image = new Image(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) + { + image.Mutate(x => x.Skew(20, 10)); + + return image.Size(); + } + } + } +} + +// Nov 7 2018 +//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 +//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores +//.NET Core SDK = 2.1.403 + +// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT +// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 +// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT + +//LaunchCount=1 TargetCount=3 WarmupCount=3 + +// #### BEFORE ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//------- |-------- |---------:|---------:|----------:|----------:| +// DoSkew | Clr | 78.14 ms | 8.383 ms | 0.4736 ms | 6 KB | +// DoSkew | Core | 44.22 ms | 4.109 ms | 0.2322 ms | 4.28 KB | + +// #### AFTER ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//------- |-------- |---------:|----------:|----------:|----------:| +// DoSkew | Clr | 71.63 ms | 25.589 ms | 1.4458 ms | 6 KB | +// DoSkew | Core | 38.99 ms | 8.640 ms | 0.4882 ms | 4.36 KB | \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 496692d969..564318e5e3 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -2,10 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; @@ -75,21 +73,15 @@ namespace SixLabors.ImageSharp.Tests using (Image image = provider.GetImage()) using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F)); - Matrix3x2 matrix = rotate * scale; + AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) + .AppendRotateMatrixDegrees(45F) + .AppendScaleMatrix(new SizeF(.25F, .25F)) + .AppendTranslationMatrix(new PointF(10, 10)); - // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor - Rectangle srcBounds = blend.Bounds(); - Rectangle destBounds = TransformHelpers.GetTransformedBoundingRectangle(srcBounds, matrix); - Matrix3x2 centeredMatrix = TransformHelpers.GetCenteredTransformMatrix(srcBounds, destBounds, matrix); - - // We pass a new rectangle here based on the dest bounds since we've offset the matrix - blend.Mutate(x => x.Transform( - centeredMatrix, - KnownResamplers.Bicubic, - new Rectangle(0, 0, destBounds.Width, destBounds.Height))); + // Apply a background color so we can see the translation. + blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors.HotPink)); + // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); image.Mutate(x => x.DrawImage(blend, position, mode, .75F)); image.DebugSave(provider, new[] { "Transformed" }); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index ae572498a4..32280d48cd 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -78,15 +78,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler resampler = GetResampler(resamplerName); using (Image 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); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees((float)Math.PI / 4F); - Rectangle sourceRectangle = image.Bounds(); - Matrix3x2 matrix = rotate * translate; - - Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix); - - image.Mutate(c => c.Transform(matrix, resampler, destRectangle)); + image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); VerifyAllPixelsAreWhiteOrTransparent(image); @@ -104,14 +99,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image 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; + image.DebugSave(provider, $"_original"); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(angleDeg) + .AppendScaleMatrix(new SizeF(sx, sy)) + .AppendTranslationMatrix(new PointF(tx, ty)); - this.PrintMatrix(m); + this.PrintMatrix(builder.BuildMatrix()); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"; image.DebugSave(provider, testOutputDetails); @@ -126,9 +122,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(angleDeg) + .AppendScaleMatrix(new SizeF(s, s)); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({s})"; image.DebugSave(provider, testOutputDetails); @@ -155,13 +153,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void Transform_FromSourceRectangle1(TestImageProvider provider) where TPixel : struct, IPixel { - var rectangle = new Rectangle(48, 0, 96, 36); + var rectangle = new Rectangle(48, 0, 48, 24); using (Image image = provider.GetImage()) { - var m = Matrix3x2.CreateScale(2.0F, 1.5F); + image.DebugSave(provider, $"_original"); + AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + .AppendScaleMatrix(new SizeF(2, 1.5F)); - image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -173,13 +173,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void Transform_FromSourceRectangle2(TestImageProvider provider) where TPixel : struct, IPixel { - var rectangle = new Rectangle(0, 24, 48, 48); + var rectangle = new Rectangle(0, 24, 48, 24); using (Image image = provider.GetImage()) { - var m = Matrix3x2.CreateScale(1.0F, 2.0F); + AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + .AppendScaleMatrix(new SizeF(1F, 2F)); - image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -194,12 +195,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(50) + .AppendScaleMatrix(new SizeF(.6F, .6F)); - image.Mutate(i => - { - i.Transform(m, sampler); - }); + image.Mutate(i => i.Transform(builder, sampler)); image.DebugSave(provider, resamplerName); image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); From 5589bd3e39007710b40d4f380690003a81209485 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 21:14:30 +0000 Subject: [PATCH 05/28] Introduce ProjectiveTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 10 +- .../Transforms/AffineTransformProcessor.cs | 6 +- .../CenteredProjectiveTransformProcessor.cs | 40 ---- .../InterpolatedTransformProcessorBase.cs | 116 ----------- .../ProjectiveTransformProcessor.cs | 197 +++++------------- .../Processors/Transforms/TransformHelpers.cs | 138 ------------ .../Transforms/TransformProcessorBase.cs | 2 +- .../Transforms/TransformProcessorHelpers.cs | 58 ++++++ .../Processors/Transforms/TransformUtils.cs | 162 ++++++++++++++ .../Processing/ProjectiveTransformBuilder.cs | 113 ++++++++++ .../Processing/ProjectiveTransformHelper.cs | 166 --------------- src/ImageSharp/Processing/TaperCorner.cs | 26 +++ src/ImageSharp/Processing/TaperSide.cs | 31 +++ .../Processing/TransformExtensions.cs | 40 +--- .../Transforms/AffineTransformTests.cs | 2 +- .../Transforms/ProjectiveTransformTests.cs | 27 +-- .../Transforms/TransformsHelpersTest.cs | 2 +- 17 files changed, 475 insertions(+), 661 deletions(-) delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs create mode 100644 src/ImageSharp/Processing/ProjectiveTransformBuilder.cs delete mode 100644 src/ImageSharp/Processing/ProjectiveTransformHelper.cs create mode 100644 src/ImageSharp/Processing/TaperCorner.cs create mode 100644 src/ImageSharp/Processing/TaperSide.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index e5ce1450f7..ff44915b14 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Processing : this(sourceRectangle.Size) => this.rectangle = sourceRectangle; + /// + /// Gets the source image size. + /// + internal Size Size { get; } + /// /// Prepends a centered rotation matrix using the given rotation in degrees. /// @@ -41,11 +46,6 @@ namespace SixLabors.ImageSharp.Processing return this; } - /// - /// Gets the source image size. - /// - internal Size Size { get; } - /// /// Appends a centered rotation matrix using the given rotation in degrees. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index fb42b83341..2370adb862 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Initializes a new instance of the class. /// - /// The transform matrix + /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size + /// The source image size. public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) { Guard.NotNull(sampler, nameof(sampler)); @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Configuration configuration) { // Handle tranforms that result in output identical to the original. - if (this.TransformMatrix.Equals(Matrix3x2.Identity)) + if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity)) { // The cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs deleted file mode 100644 index 962b9e4c9d..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs +++ /dev/null @@ -1,40 +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.Transforms -{ - /// - /// A base class that provides methods to allow the automatic centering of non-affine transforms - /// - /// The pixel format. - internal abstract class CenteredProjectiveTransformProcessor : ProjectiveTransformProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The source image size - protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) - : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) - { - } - - /// - protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - { - return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); - } - - private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) - { - var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs deleted file mode 100644 index 4737a4102c..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// The base class for performing interpolated affine and non-affine transforms. - /// - /// The pixel format. - internal abstract class InterpolatedTransformProcessorBase : TransformProcessorBase - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The sampler to perform the transform operation. - protected InterpolatedTransformProcessorBase(IResampler sampler) - { - Guard.NotNull(sampler, nameof(sampler)); - this.Sampler = sampler; - } - - /// - /// Gets the sampler to perform interpolation of the transform operation. - /// - public IResampler Sampler { get; } - - /// - /// 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 normalized. - /// - /// The minimum sampling offset - /// The maximum sampling offset - /// The minimum source bounds - /// The maximum source bounds - /// The transformed point dimension - /// The sampler - /// The transformed image scale relative to the source - /// The reference to the collection of weights - /// The length of the weights collection - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length) - { - float sum = 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 weightsRef, x) = weight; - } - - if (sum > 0) - { - for (int i = 0; i < length; i++) - { - ref float wRef = ref Unsafe.Add(ref weightsRef, i); - wRef /= sum; - } - } - } - - /// - /// Calculated the weights for the given point. - /// - /// The minimum source bounds - /// The maximum source bounds - /// The transformed point dimension - /// The sampler - /// The reference to the collection of weights - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef) - { - for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) - { - Unsafe.Add(ref weightsRef, x) = sampler.GetValue(i - point); - } - } - - /// - /// Calculates the sampling radius for the current sampler - /// - /// The source dimension size - /// The destination dimension size - /// The radius, and scaling factor - 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); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 50af26aebf..bfde1769c1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -5,13 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -20,22 +16,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides the base methods to perform non-affine transforms on an image. /// /// The pixel format. - internal class ProjectiveTransformProcessor : InterpolatedTransformProcessorBase + internal class ProjectiveTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { /// /// Initializes a new instance of the class. /// - /// The transform matrix + /// The transform matrix. /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) + /// The source image size. + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) { + Guard.NotNull(sampler, nameof(sampler)); + this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; + this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); } + /// + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + /// /// Gets the matrix used to supply the projective transform /// @@ -60,17 +62,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// protected override void OnFrameApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { + // Handle tranforms that result in output identical to the original. + if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity)) + { + // The cloned will be blank here copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + return; + } + 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 - Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); + var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. - Matrix4x4.Invert(matrix, out matrix); + Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 matrix); const float Epsilon = 0.0000001F; if (this.Sampler is NearestNeighborResampler) @@ -92,7 +98,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int px = (int)MathF.Round(v3.X / z); int py = (int)MathF.Round(v3.Y / z); - if (sourceBounds.Contains(px, py)) + if (sourceRectangle.Contains(px, py)) { destRow[x] = source[px, py]; } @@ -103,145 +109,40 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms 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; - - // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: - var radius = new Vector4(xRadiusScale.radius, yRadiusScale.radius, 0, 0); - - 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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler); + try { - ParallelHelper.IterateRows( + ParallelHelper.IterateRowsWithTempBuffer( targetBounds, configuration, - rows => + (rows, vectorBuffer) => + { + Span vectorSpan = vectorBuffer.Span; + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(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 v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); - float z = MathF.Max(v3.Z, Epsilon); - - // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: - Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z; - - // Clamp sampling pixel radial extents to the source image edges - Vector4 maxXY = point + radius; - Vector4 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; + Span targetRowSpan = destination.GetPixelRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, targetRowSpan, vectorSpan); + ref float ySpanRef = ref kernel.GetYStartReference(y); + ref float xSpanRef = ref kernel.GetXStartReference(y); - 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + 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 v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } + kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); } - }); + + PixelOperations.Instance.FromVector4(configuration, vectorSpan, targetRowSpan); + } + }); + } + finally + { + kernel.Dispose(); } } - - /// - /// Gets a transform matrix adjusted for final processing based upon the target image bounds. - /// - /// The source image bounds. - /// The destination image bounds. - /// - /// The . - /// - protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) => this.TransformMatrix; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs deleted file mode 100644 index 2e85f6c2cc..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using SixLabors.ImageSharp.MetaData.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// Contains helper methods for working with affine and non-affine transforms - /// - internal static class TransformHelpers - { - /// - /// Updates the dimensional metadata of a transformed image - /// - /// The pixel format. - /// The image to update - public static void UpdateDimensionalMetData(Image image) - where TPixel : struct, IPixel - { - ExifProfile profile = image.MetaData.ExifProfile; - if (profile is null) - { - return; - } - - // Removing the previously stored value allows us to set a value with our own data tag if required. - if (profile.GetValue(ExifTag.PixelXDimension) != null) - { - profile.RemoveValue(ExifTag.PixelXDimension); - - if (image.Width <= ushort.MaxValue) - { - profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width); - } - else - { - profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width); - } - } - - if (profile.GetValue(ExifTag.PixelYDimension) != null) - { - profile.RemoveValue(ExifTag.PixelYDimension); - - if (image.Height <= ushort.MaxValue) - { - profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); - } - else - { - profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); - } - } - } - - /// - /// Gets the centered transform matrix based upon the source and destination rectangles - /// - /// The source image bounds. - /// The destination image bounds. - /// The transformation matrix. - /// The - public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) - { - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); - - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - // Translate back to world to pass to the Transform method. - return centered; - } - - /// - /// Gets the centered transform matrix based upon the source and destination rectangles - /// - /// The source image bounds. - /// The destination image bounds. - /// The transformation matrix. - /// The - public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix) - { - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix4x4.Invert(matrix, out Matrix4x4 inverted); - - var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); - var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0); - - Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered); - - // Translate back to world to pass to the Transform method. - return centered; - } - - /// - /// Returns the bounding rectangle relative to the source for the given transformation matrix. - /// - /// The source rectangle. - /// The transformation matrix. - /// - /// The . - /// - 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)); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs index 13ee90a062..4973b90f46 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs @@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) - => TransformHelpers.UpdateDimensionalMetData(destination); + => TransformProcessorHelpers.UpdateDimensionalMetData(destination); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs new file mode 100644 index 0000000000..f5536d0467 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Contains helper methods for working with transforms. + /// + internal static class TransformProcessorHelpers + { + /// + /// Updates the dimensional metadata of a transformed image + /// + /// The pixel format. + /// The image to update + public static void UpdateDimensionalMetData(Image image) + where TPixel : struct, IPixel + { + ExifProfile profile = image.MetaData.ExifProfile; + if (profile is null) + { + return; + } + + // Removing the previously stored value allows us to set a value with our own data tag if required. + if (profile.GetValue(ExifTag.PixelXDimension) != null) + { + profile.RemoveValue(ExifTag.PixelXDimension); + + if (image.Width <= ushort.MaxValue) + { + profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width); + } + else + { + profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width); + } + } + + if (profile.GetValue(ExifTag.PixelYDimension) != null) + { + profile.RemoveValue(ExifTag.PixelYDimension); + + if (image.Height <= ushort.MaxValue) + { + profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); + } + else + { + profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 10cf49c34b..f561d3513f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -58,6 +58,111 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return centered; } + /// + /// Creates a matrix that performs a tapering projective transform. + /// + /// + /// The rectangular size of the image being transformed. + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The + public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction) + { + Matrix4x4 matrix = Matrix4x4.Identity; + + switch (side) + { + case TaperSide.Left: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M13 = (fraction - 1) / size.Width; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + matrix.M32 = size.Height * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M13; + matrix.M32 = size.Height * (1 - fraction) / 2; + break; + } + + break; + + case TaperSide.Top: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M23 = (fraction - 1) / size.Height; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + matrix.M31 = size.Width * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M23; + matrix.M31 = size.Width * (1 - fraction) / 2; + break; + } + + break; + + case TaperSide.Right: + matrix.M11 = 1 / fraction; + matrix.M13 = (1 - fraction) / (size.Width * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M13; + break; + } + + break; + + case TaperSide.Bottom: + matrix.M22 = 1 / fraction; + matrix.M23 = (1 - fraction) / (size.Height * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M23; + break; + } + + break; + } + + return matrix; + } + /// /// Returns the rectangle bounds relative to the source for the given transformation matrix. /// @@ -114,6 +219,63 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + return ConstrainSize(rectangle); + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) + { + if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + { + return rectangle; + } + + Vector2 GetVector(float x, float y) + { + const float Epsilon = 0.0000001F; + var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); + return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); + } + + Vector2 tl = GetVector(rectangle.Left, rectangle.Top); + Vector2 tr = GetVector(rectangle.Right, rectangle.Top); + Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom); + Vector2 br = GetVector(rectangle.Right, rectangle.Bottom); + + return GetBoundingRectangle(tl, tr, bl, br); + } + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The source size. + /// The transformation matrix. + /// + /// The . + /// + public static Size GetTransformedSize(Size size, Matrix4x4 matrix) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + { + return size; + } + + Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + + return ConstrainSize(rectangle); + } + + private static Size ConstrainSize(Rectangle rectangle) + { // We want to resize the canvas here taking into account any translations. int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs new file mode 100644 index 0000000000..3edc993c47 --- /dev/null +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// A helper class for constructing instances for use in projective transforms. + /// + public class ProjectiveTransformBuilder + { + private readonly List matrices = new List(); + private Rectangle rectangle; + + /// + /// Initializes a new instance of the class. + /// + /// The source image size. + public ProjectiveTransformBuilder(Size sourceSize) => this.Size = sourceSize; + + /// + /// Initializes a new instance of the class. + /// + /// The source rectangle. + public ProjectiveTransformBuilder(Rectangle sourceRectangle) + : this(sourceRectangle.Size) + => this.rectangle = sourceRectangle; + + /// + /// Gets the source image size. + /// + internal Size Size { get; } + + /// + /// Prepends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + { + this.matrices.Insert(0, TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + return this; + } + + /// + /// Appends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + { + this.matrices.Add(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + return this; + } + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// The . + public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) + { + this.matrices.Insert(0, matrix); + return this; + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// The . + public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) + { + this.matrices.Add(matrix); + return this; + } + + /// + /// Returns the combined matrix. + /// + /// The . + public Matrix4x4 BuildMatrix() + { + Matrix4x4 matrix = Matrix4x4.Identity; + + // Translate the origin matrix to cater for source rectangle offsets. + if (!this.rectangle.Equals(default)) + { + matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.rectangle.Location, 0)); + } + + foreach (Matrix4x4 m in this.matrices) + { + matrix *= m; + } + + return matrix; + } + + /// + /// Removes all matrices from the builder. + /// + public void Clear() => this.matrices.Clear(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ProjectiveTransformHelper.cs b/src/ImageSharp/Processing/ProjectiveTransformHelper.cs deleted file mode 100644 index 4057ec586c..0000000000 --- a/src/ImageSharp/Processing/ProjectiveTransformHelper.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Enumerates the various options which determine which side to taper - /// - public enum TaperSide - { - /// - /// Taper the left side - /// - Left, - - /// - /// Taper the top side - /// - Top, - - /// - /// Taper the right side - /// - Right, - - /// - /// Taper the bottom side - /// - Bottom - } - - /// - /// Enumerates the various options which determine how to taper corners - /// - public enum TaperCorner - { - /// - /// Taper the left or top corner - /// - LeftOrTop, - - /// - /// Taper the right or bottom corner - /// - RightOrBottom, - - /// - /// Taper the both sets of corners - /// - Both - } - - /// - /// Provides helper methods for working with generalized projective transforms. - /// - public static class ProjectiveTransformHelper - { - /// - /// Creates a matrix that performs a tapering projective transform. - /// - /// - /// The rectangular size of the image being transformed. - /// An enumeration that indicates the side of the rectangle that tapers. - /// An enumeration that indicates on which corners to taper the rectangle. - /// The amount to taper. - /// The - public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction) - { - Matrix4x4 matrix = Matrix4x4.Identity; - - switch (taperSide) - { - case TaperSide.Left: - matrix.M11 = taperFraction; - matrix.M22 = taperFraction; - matrix.M13 = (taperFraction - 1) / size.Width; - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; - matrix.M32 = size.Height * (1 - taperFraction); - break; - - case TaperCorner.Both: - matrix.M12 = (size.Height * 0.5f) * matrix.M13; - matrix.M32 = size.Height * (1 - taperFraction) / 2; - break; - } - - break; - - case TaperSide.Top: - matrix.M11 = taperFraction; - matrix.M22 = taperFraction; - matrix.M23 = (taperFraction - 1) / size.Height; - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; - matrix.M31 = size.Width * (1 - taperFraction); - break; - - case TaperCorner.Both: - matrix.M21 = (size.Width * 0.5f) * matrix.M23; - matrix.M31 = size.Width * (1 - taperFraction) / 2; - break; - } - - break; - - case TaperSide.Right: - matrix.M11 = 1 / taperFraction; - matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction); - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; - break; - - case TaperCorner.Both: - matrix.M12 = (size.Height * 0.5f) * matrix.M13; - break; - } - - break; - - case TaperSide.Bottom: - matrix.M22 = 1 / taperFraction; - matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction); - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; - break; - - case TaperCorner.Both: - matrix.M21 = (size.Width * 0.5f) * matrix.M23; - break; - } - - break; - } - - return matrix; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TaperCorner.cs b/src/ImageSharp/Processing/TaperCorner.cs new file mode 100644 index 0000000000..395b171424 --- /dev/null +++ b/src/ImageSharp/Processing/TaperCorner.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Enumerates the various options which determine how to taper corners + /// + public enum TaperCorner + { + /// + /// Taper the left or top corner + /// + LeftOrTop, + + /// + /// Taper the right or bottom corner + /// + RightOrBottom, + + /// + /// Taper the both sets of corners + /// + Both + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TaperSide.cs b/src/ImageSharp/Processing/TaperSide.cs new file mode 100644 index 0000000000..226d11aed2 --- /dev/null +++ b/src/ImageSharp/Processing/TaperSide.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Enumerates the various options which determine which side to taper + /// + public enum TaperSide + { + /// + /// Taper the left side + /// + Left, + + /// + /// Taper the top side + /// + Top, + + /// + /// Taper the right side + /// + Right, + + /// + /// Taper the bottom side + /// + Bottom + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 4cbd1b041f..ea789eb3d7 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -1,10 +1,8 @@ // 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.Processors.Transforms; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { @@ -14,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing public static class TransformExtensions { /// - /// Transforms an image by the given matrix. + /// Performs an affine transform of an image. /// /// The pixel format. /// The image to transform. @@ -25,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. /// The image to transform. @@ -37,44 +35,26 @@ namespace SixLabors.ImageSharp.Processing => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); /// - /// Transforms an image by the given matrix. + /// Performs a projective transform of an image. /// /// The pixel format. /// The image to transform. - /// The transformation matrix. - /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix) - where TPixel : struct, IPixel - => Transform(source, matrix, KnownResamplers.Bicubic); - - /// - /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. - /// - /// The pixel format. - /// The image to transform. - /// The transformation matrix. - /// The to perform the resampling. + /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) + public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder) where TPixel : struct, IPixel - => source.ApplyProcessor(new ProjectiveTransformProcessor(matrix, sampler, source.GetCurrentSize())); + => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. - /// TODO: Should we be offsetting the matrix here? + /// Performs a projective transform of an image using the specified sampling algorithm. /// /// The pixel format. /// The image to transform. - /// The transformation matrix. + /// The projective transform builder. /// The to perform the resampling. - /// The rectangle to constrain the transformed image to. /// The - internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) + public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - { - var t = Matrix4x4.CreateTranslation(new Vector3(-rectangle.Location, 0)); - Matrix4x4 combinedMatrix = t * matrix; - return source.ApplyProcessor(new ProjectiveTransformProcessor(combinedMatrix, sampler, rectangle.Size)); - } + => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 32280d48cd..e05a8e381b 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees((float)Math.PI / 4F); + .AppendRotateMatrixDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 5190a71e71..0362d75a0b 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); private ITestOutputHelper Output { get; } - + public static readonly TheoryData ResamplerNames = new TheoryData { nameof(KnownResamplers.Bicubic), @@ -60,10 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }; - public ProjectiveTransformTests(ITestOutputHelper output) - { - this.Output = output; - } + public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output; [Theory] [WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] @@ -73,9 +70,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F); + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendTaperMatrix(TaperSide.Right, TaperCorner.Both, .5F); - image.Mutate(i => { i.Transform(m, sampler); }); + image.Mutate(i => i.Transform(builder, sampler)); image.DebugSave(provider, resamplerName); image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); @@ -89,8 +87,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F); - image.Mutate(i => { i.Transform(m); }); + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendTaperMatrix(taperSide, taperCorner, .5F); + + image.Mutate(i => i.Transform(builder)); FormattableString testOutputDetails = $"{taperSide}-{taperCorner}"; image.DebugSave(provider, testOutputDetails); @@ -110,10 +110,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine using (Image image = provider.GetImage()) { - Matrix4x4 m = Matrix4x4.Identity; - m.M13 = 0.01F; + Matrix4x4 matrix = Matrix4x4.Identity; + matrix.M13 = 0.01F; + + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendMatrix(matrix); - image.Mutate(i => { i.Transform(m); }); + image.Mutate(i => i.Transform(builder)); image.DebugSave(provider); image.CompareToReferenceOutput(TolerantComparer, provider); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs index 146ed62304..909e505357 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType); Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType); - TransformHelpers.UpdateDimensionalMetData(img); + TransformProcessorHelpers.UpdateDimensionalMetData(img); Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType); Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType); From 8b0e276de532d4900b722c75099bb3f9d20235ff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 21:45:02 +0000 Subject: [PATCH 06/28] Update tests/Images/External --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index ed8a7b0b6f..e7e0f1311d 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit ed8a7b0b6fe1b2e2a7c822aa617103ae31192655 +Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4 From 15093ba8bff157d0daac92e61e87333779eaa91e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 10:24:32 +0000 Subject: [PATCH 07/28] Change tolerance for 32bit framework builds. --- .../Processing/Transforms/AffineTransformTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index e05a8e381b..15b4500bb2 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -17,7 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { private readonly ITestOutputHelper Output; - private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0085f, 3); + // 1 byte difference on one color component. + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0134F, 3); /// /// angleDeg, sx, sy, tx, ty From 13b9e9c596dc6ca02e1f4c52b6bb6607791ab133 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 21:33:39 +0000 Subject: [PATCH 08/28] Improve transform builder coverage --- .../Processing/AffineTransformBuilder.cs | 16 ++++++++-------- .../Processing/ProjectiveTransformBuilder.cs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ff44915b14..b2b011264a 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) { - this.matrices.Insert(0, TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); return this; } @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) { - this.matrices.Add(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); return this; } @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependScaleMatrix(SizeF scales) { - this.matrices.Insert(0, Matrix3x2Extensions.CreateScale(scales)); + this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); return this; } @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendScaleMatrix(SizeF scales) { - this.matrices.Add(Matrix3x2Extensions.CreateScale(scales)); + this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); return this; } @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) { - this.matrices.Insert(0, TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); return this; } @@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) { - this.matrices.Add(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); return this; } @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependTranslationMatrix(PointF position) { - this.matrices.Insert(0, Matrix3x2Extensions.CreateTranslation(position)); + this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); return this; } @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendTranslationMatrix(PointF position) { - this.matrices.Add(Matrix3x2Extensions.CreateTranslation(position)); + this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); return this; } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 3edc993c47..529dd56b19 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) { - this.matrices.Insert(0, TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); return this; } @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) { - this.matrices.Add(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); return this; } From 8a47a1894b5d312fb697b1d49d52556fcd63c526 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 22:42:05 +0000 Subject: [PATCH 09/28] Cleanup and add dedicated transform builder tests --- .../Processing/AffineTransformBuilder.cs | 48 ++++------- .../Processing/ProjectiveTransformBuilder.cs | 18 ++--- .../Transforms/AffineTransformBuilderTests.cs | 79 +++++++++++++++++++ .../ProjectiveTransformBuilderTests.cs | 78 ++++++++++++++++++ 4 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index b2b011264a..003249d6ed 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -20,7 +20,13 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The source image size. - public AffineTransformBuilder(Size sourceSize) => this.Size = sourceSize; + public AffineTransformBuilder(Size sourceSize) + { + Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); + Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); + + this.Size = sourceSize; + } /// /// Initializes a new instance of the class. @@ -41,10 +47,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) - { - this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -52,10 +55,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - { - this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// /// Prepends a scale matrix from the given vector scale. @@ -63,10 +63,7 @@ namespace SixLabors.ImageSharp.Processing /// The horizontal and vertical scale. /// The . public AffineTransformBuilder PrependScaleMatrix(SizeF scales) - { - this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); - return this; - } + => this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); /// /// Appends a scale matrix from the given vector scale. @@ -74,10 +71,7 @@ namespace SixLabors.ImageSharp.Processing /// The horizontal and vertical scale. /// The . public AffineTransformBuilder AppendScaleMatrix(SizeF scales) - { - this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); - return this; - } + => this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -86,10 +80,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) - { - this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// /// Appends a centered skew matrix from the give angles in degrees. @@ -98,10 +89,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) - { - this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// /// Prepends a translation matrix from the given vector. @@ -109,10 +97,7 @@ namespace SixLabors.ImageSharp.Processing /// The translation position. /// The . public AffineTransformBuilder PrependTranslationMatrix(PointF position) - { - this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); - return this; - } + => this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. @@ -120,10 +105,7 @@ namespace SixLabors.ImageSharp.Processing /// The translation position. /// The . public AffineTransformBuilder AppendTranslationMatrix(PointF position) - { - this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); - return this; - } + => this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); /// /// Prepends a raw matrix. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 529dd56b19..be4c675189 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -20,7 +20,13 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The source image size. - public ProjectiveTransformBuilder(Size sourceSize) => this.Size = sourceSize; + public ProjectiveTransformBuilder(Size sourceSize) + { + Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); + Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); + + this.Size = sourceSize; + } /// /// Initializes a new instance of the class. @@ -43,10 +49,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) - { - this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -56,10 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) - { - this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// /// Prepends a raw matrix. diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs new file mode 100644 index 0000000000..eaa51b1296 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public class AffineTransformBuilderTests + { + [Fact] + public void ConstructorAssignsProperties() + { + var s = new Size(1, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + Assert.Equal(s, builder.Size); + } + + [Fact] + public void ConstructorThrowsInvalid() + { + Assert.Throws(() => + { + var s = new Size(0, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); + + Assert.Throws(() => + { + var s = new Size(1, 0); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + var b1 = new AffineTransformBuilder(rectangle); + var b2 = new AffineTransformBuilder(rectangle); + + const float pi = (float)Math.PI; + + // Forwards + b1.AppendRotateMatrixDegrees(pi) + .AppendSkewMatrixDegrees(pi, pi) + .AppendScaleMatrix(new SizeF(pi, pi)) + .AppendTranslationMatrix(new PointF(pi, pi)); + + // Backwards + b2.PrependTranslationMatrix(new PointF(pi, pi)) + .PrependScaleMatrix(new SizeF(pi, pi)) + .PrependSkewMatrixDegrees(pi, pi) + .PrependRotateMatrixDegrees(pi); + + Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); + } + + [Fact] + public void BuilderCanClear() + { + var rectangle = new Rectangle(0, 0, 3, 3); + var builder = new AffineTransformBuilder(rectangle); + Matrix3x2 matrix = Matrix3x2.Identity; + matrix.M31 = (float)Math.PI; + + Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + + builder.AppendMatrix(matrix); + Assert.NotEqual(Matrix3x2.Identity, builder.BuildMatrix()); + + builder.Clear(); + Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs new file mode 100644 index 0000000000..3dfc42d4f7 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public class ProjectiveTransformBuilderTests + { + [Fact] + public void ConstructorAssignsProperties() + { + var s = new Size(1, 1); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + Assert.Equal(s, builder.Size); + } + + [Fact] + public void ConstructorThrowsInvalid() + { + Assert.Throws(() => + { + var s = new Size(0, 1); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + }); + + Assert.Throws(() => + { + var s = new Size(1, 0); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + }); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + var b1 = new ProjectiveTransformBuilder(rectangle); + var b2 = new ProjectiveTransformBuilder(rectangle); + + const float pi = (float)Math.PI; + + Matrix4x4 m4 = Matrix4x4.Identity; + m4.M31 = pi; + + // Forwards + b1.AppendMatrix(m4) + .AppendTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi); + + // Backwards + b2.PrependTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi) + .PrependMatrix(m4); + + Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); + } + + [Fact] + public void BuilderCanClear() + { + var rectangle = new Rectangle(0, 0, 3, 3); + var builder = new ProjectiveTransformBuilder(rectangle); + Matrix4x4 matrix = Matrix4x4.Identity; + matrix.M31 = (float)Math.PI; + + Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + + builder.AppendMatrix(matrix); + Assert.NotEqual(Matrix4x4.Identity, builder.BuildMatrix()); + + builder.Clear(); + Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + } + } +} From 6a81923ab7f57d7d947f826de1ef426bec5806ca Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Nov 2018 20:34:37 +0000 Subject: [PATCH 10/28] Tighten up coverage on new AotCompilerTools --- tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs b/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs index 5c1dd4bb08..f6397dbd09 100644 --- a/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs +++ b/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs @@ -10,9 +10,6 @@ namespace SixLabors.ImageSharp.Tests.Advanced public class AotCompilerTests { [Fact] - public void AotCompiler_NoExceptions() - { - AotCompilerTools.Seed(); - } + public void AotCompiler_NoExceptions() => AotCompilerTools.Seed(); } } From 1c258dcae42f959e1fcf0cd355f3c2aad63a5bf6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 13 Nov 2018 09:38:57 +0000 Subject: [PATCH 11/28] Update tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs --- .../Processing/Transforms/AffineTransformTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 15b4500bb2..3d7fba2c19 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -207,21 +207,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms } } - private Matrix3x2 MakeManuallyCenteredMatrix(float angleDeg, float s, Image image) - where TPixel : struct, IPixel - { - 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); From da50180e1c7a2aa34d94d578824deec4ea3e6b0a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 22:52:47 +0100 Subject: [PATCH 12/28] add more tests --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 15 +- .../Processing/AffineTransformBuilder.cs | 35 ++-- .../Processors/Transforms/TransformUtils.cs | 11 ++ .../Transforms/AffineTransformBuilderTests.cs | 67 +++----- .../Transforms/TransformBuilderTestBase.cs | 151 ++++++++++++++++++ .../TestUtilities/ApproximateFloatComparer.cs | 13 +- 6 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index f07ccb03b4..45556741e6 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -22,7 +22,8 @@ namespace SixLabors.ImageSharp /// The blue component. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f); + public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => + (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f); /// /// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709. @@ -32,7 +33,8 @@ namespace SixLabors.ImageSharp /// The blue component. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F)); + public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => + (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F)); /// /// Scales a value from a 16 bit to it's 8 bit equivalent. @@ -128,6 +130,15 @@ namespace SixLabors.ImageSharp return x & (m - 1); } + /// + /// Converts degrees to radians + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static float ToRadian(float degrees) + { + return degrees * ((float)Math.PI / 180f); + } + /// /// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation. /// diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 003249d6ed..4d798f4396 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -14,18 +14,15 @@ namespace SixLabors.ImageSharp.Processing public class AffineTransformBuilder { private readonly List matrices = new List(); - private Rectangle rectangle; + private readonly Rectangle rectangle; /// /// Initializes a new instance of the class. /// /// The source image size. public AffineTransformBuilder(Size sourceSize) + : this(new Rectangle(Point.Empty, sourceSize)) { - Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); - Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); - - this.Size = sourceSize; } /// @@ -33,13 +30,17 @@ namespace SixLabors.ImageSharp.Processing /// /// The source rectangle. public AffineTransformBuilder(Rectangle sourceRectangle) - : this(sourceRectangle.Size) - => this.rectangle = sourceRectangle; + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + this.rectangle = sourceRectangle; + } /// /// Gets the source image size. /// - internal Size Size { get; } + internal Size Size => this.rectangle.Size; /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -49,13 +50,29 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + /// + /// Prepends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder PrependRotateMatrixRadians(float radians) + => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + /// /// Appends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + => this.AppendRotateMatrixRadians(ImageMaths.ToRadian(degrees)); + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder AppendRotateMatrixRadians(float radians) + => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// /// Prepends a scale matrix from the given vector scale. diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index f561d3513f..6cef38f8fe 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -23,6 +23,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + /// + /// Creates a centered rotation matrix using the given rotation in radians and the source size. + /// + /// The amount of rotation, in radians. + /// The source image size. + /// The . + public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); + /// /// Creates a centered skew matrix from the give angles in degrees and the source size. /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index eaa51b1296..1b4caee14f 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -4,12 +4,13 @@ using System; using System.Numerics; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - public class AffineTransformBuilderTests + public class AffineTransformBuilderTests : TransformBuilderTestBase { [Fact] public void ConstructorAssignsProperties() @@ -23,57 +24,33 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void ConstructorThrowsInvalid() { Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); + { + var s = new Size(0, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); + { + var s = new Size(1, 0); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); } - [Fact] - public void AppendPrependOpposite() - { - var rectangle = new Rectangle(-1, -1, 3, 3); - var b1 = new AffineTransformBuilder(rectangle); - var b2 = new AffineTransformBuilder(rectangle); - - const float pi = (float)Math.PI; - - // Forwards - b1.AppendRotateMatrixDegrees(pi) - .AppendSkewMatrixDegrees(pi, pi) - .AppendScaleMatrix(new SizeF(pi, pi)) - .AppendTranslationMatrix(new PointF(pi, pi)); - - // Backwards - b2.PrependTranslationMatrix(new PointF(pi, pi)) - .PrependScaleMatrix(new SizeF(pi, pi)) - .PrependSkewMatrixDegrees(pi, pi) - .PrependRotateMatrixDegrees(pi); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslationMatrix(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScaleMatrix(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotateMatrixRadians(radians); - Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); - } + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslationMatrix(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScaleMatrix(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotateMatrixRadians(radians); - [Fact] - public void BuilderCanClear() + protected override Vector2 Execute( + AffineTransformBuilder builder, + Rectangle rectangle, + Vector2 sourcePoint) { - var rectangle = new Rectangle(0, 0, 3, 3); - var builder = new AffineTransformBuilder(rectangle); - Matrix3x2 matrix = Matrix3x2.Identity; - matrix.M31 = (float)Math.PI; - - Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); - - builder.AppendMatrix(matrix); - Assert.NotEqual(Matrix3x2.Identity, builder.BuildMatrix()); - - builder.Clear(); - Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + Matrix3x2 matrix = builder.BuildMatrix(); + return Vector2.Transform(sourcePoint, matrix); } } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs new file mode 100644 index 0000000000..d109387cc4 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public abstract class TransformBuilderTestBase + { + private static readonly ApproximateFloatComparer Comparer = new ApproximateFloatComparer(1e-6f); + + public static readonly TheoryData ScaleTranslate_Data = + new TheoryData + { + // scale, translate, source, expectedDest + + { Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero }, + { Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) }, + { Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) }, + { new Vector2(2, 0.5f), new Vector2(3, 1), new Vector2(10, 20), new Vector2(23, 11) }, + }; + + [Theory] + [MemberData(nameof(ScaleTranslate_Data))] + public void _1Scale_2Translate(Vector2 scale, Vector2 translate, Vector2 source, Vector2 expectedDest) + { + // These operations should be size-agnostic: + var size = new Size(123, 321); + TBuilder builder = this.CreateBuilder(size); + + this.AppendScale(builder, new SizeF(scale)); + this.AppendTranslation(builder, translate); + + Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); + Assert.True(Comparer.Equals(expectedDest, actualDest)); + } + + public static readonly TheoryData TranslateScale_Data = + new TheoryData + { + // translate, scale, source, expectedDest + + { Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero }, + { Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) }, + { new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) }, + }; + + [Theory] + [MemberData(nameof(TranslateScale_Data))] + public void _1Translate_2Scale(Vector2 translate, Vector2 scale, Vector2 source, Vector2 expectedDest) + { + // Translate ans scale are size-agnostic: + var size = new Size(456, 432); + TBuilder builder = this.CreateBuilder(size); + + this.AppendTranslation(builder, translate); + this.AppendScale(builder, new SizeF(scale)); + + Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); + Assert.Equal(expectedDest, actualDest, Comparer); + } + + [Theory] + [InlineData(10, 20)] + [InlineData(-20, 10)] + public void LocationOffsetIsPrepended(int locationX, int locationY) + { + var rectangle = new Rectangle(locationX, locationY, 10, 10); + TBuilder builder = this.CreateBuilder(rectangle); + + this.AppendScale(builder, new SizeF(2, 2)); + + Vector2 actual = this.Execute(builder, rectangle, Vector2.One); + Vector2 expected = new Vector2(-locationX + 1, -locationY + 1) * 2; + + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 42, 84)] + [InlineData(200, 100, 100, 42, 84)] + [InlineData(100, 200, -10, 42, 84)] + public void RotateDegrees_ShouldCreateCenteredMatrix(int width, int height, float deg, float x, float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + this.AppendRotationDegrees(builder, deg); + + // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness + Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + TBuilder b1 = this.CreateBuilder(rectangle); + TBuilder b2 = this.CreateBuilder(rectangle); + + const float pi = (float)Math.PI; + + // Forwards + this.AppendRotationRadians(b1, pi); + this.AppendScale(b1, new SizeF(2, 0.5f)); + this.AppendTranslation(b1, new PointF(123, 321)); + + // Backwards + this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependScale(b2, new SizeF(2, 0.5f)); + this.PrependRotationRadians(b2, pi); + + Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65)); + Vector2 p2 = this.Execute(b2, rectangle, new Vector2(32, 65)); + + Assert.Equal(p1, p2, Comparer); + } + + protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size)); + + protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle); + + protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationRadians(TBuilder builder, float radians); + + protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void PrependRotationRadians(TBuilder builder, float radians); + + protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => + this.AppendRotationRadians(builder, ImageMaths.ToRadian(degrees)); + + protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); + + private static float Sqrt(float a) => (float)Math.Sqrt(a); + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 854e57d8f5..47ca6cccb2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Tests /// internal readonly struct ApproximateFloatComparer : IEqualityComparer, - IEqualityComparer + IEqualityComparer, + IEqualityComparer { private readonly float Epsilon; @@ -33,9 +34,17 @@ namespace SixLabors.ImageSharp.Tests public int GetHashCode(float obj) => obj.GetHashCode(); /// - public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W); + public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W); /// public int GetHashCode(Vector4 obj) => obj.GetHashCode(); + + /// + public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y); + + public int GetHashCode(Vector2 obj) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file From cc2589f88e7887127b19303a19ad5ae0974cb013 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:07:50 +0100 Subject: [PATCH 13/28] rename methods + extend API --- .../Processing/AffineTransformBuilder.cs | 78 +++++++++++++++---- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 6 +- .../Transforms/AffineTransformBuilderTests.cs | 12 +-- .../Transforms/AffineTransformTests.cs | 20 ++--- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 4d798f4396..1620b72234 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) + public AffineTransformBuilder PrependRotationDegrees(float degrees) => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotateMatrixRadians(float radians) + public AffineTransformBuilder PrependRotationRadians(float radians) => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// @@ -63,32 +63,64 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - => this.AppendRotateMatrixRadians(ImageMaths.ToRadian(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.ToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendRotateMatrixRadians(float radians) + public AffineTransformBuilder AppendRotationRadians(float radians) => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix3x2.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix3x2.CreateScale(scale)); + /// /// Prepends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder PrependScaleMatrix(SizeF scales) - => this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); + public AffineTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix3x2.CreateScale(scales)); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScaleMatrix(SizeF scales) - => this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); + public AffineTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix3x2.CreateScale(scales)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -96,7 +128,7 @@ namespace SixLabors.ImageSharp.Processing /// The X angle, in degrees. /// The Y angle, in degrees. /// The . - public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// @@ -105,7 +137,7 @@ namespace SixLabors.ImageSharp.Processing /// The X angle, in degrees. /// The Y angle, in degrees. /// The . - public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// @@ -113,16 +145,32 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslationMatrix(PointF position) - => this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslationMatrix(PointF position) - => this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a raw matrix. diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 564318e5e3..a29342ad46 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,9 +74,9 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) - .AppendRotateMatrixDegrees(45F) - .AppendScaleMatrix(new SizeF(.25F, .25F)) - .AppendTranslationMatrix(new PointF(10, 10)); + .AppendRotationDegrees(45F) + .AppendScale(new SizeF(.25F, .25F)) + .AppendTranslation(new PointF(10, 10)); // Apply a background color so we can see the translation. blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors.HotPink)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 1b4caee14f..639f4b46fa 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -36,13 +36,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }); } - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslationMatrix(translate); - protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScaleMatrix(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotateMatrixRadians(radians); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslationMatrix(translate); - protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScaleMatrix(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotateMatrixRadians(radians); + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 3d7fba2c19..e78bb2c6d9 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(30); + .AppendRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,9 +102,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(angleDeg) - .AppendScaleMatrix(new SizeF(sx, sy)) - .AppendTranslationMatrix(new PointF(tx, ty)); + .AppendRotationDegrees(angleDeg) + .AppendScale(new SizeF(sx, sy)) + .AppendTranslation(new PointF(tx, ty)); this.PrintMatrix(builder.BuildMatrix()); @@ -124,8 +124,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(angleDeg) - .AppendScaleMatrix(new SizeF(s, s)); + .AppendRotationDegrees(angleDeg) + .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -160,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) - .AppendScaleMatrix(new SizeF(2, 1.5F)); + .AppendScale(new SizeF(2, 1.5F)); image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) - .AppendScaleMatrix(new SizeF(1F, 2F)); + .AppendScale(new SizeF(1F, 2F)); image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); @@ -197,8 +197,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(50) - .AppendScaleMatrix(new SizeF(.6F, .6F)); + .AppendRotationDegrees(50) + .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); From 0774d5e794d41eb342b05af7ceb65df1c8258681 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:18:17 +0100 Subject: [PATCH 14/28] Translation, Scale -> ProjectiveTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 26 ++-- .../Processing/ProjectiveTransformBuilder.cs | 113 ++++++++++++++++-- .../ProjectiveTransformBuilderTests.cs | 54 +++------ 3 files changed, 131 insertions(+), 62 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 1620b72234..cd66272b42 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing public class AffineTransformBuilder { private readonly List matrices = new List(); - private readonly Rectangle rectangle; + private readonly Rectangle sourceRectangle; /// /// Initializes a new instance of the class. @@ -34,13 +34,13 @@ namespace SixLabors.ImageSharp.Processing Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - this.rectangle = sourceRectangle; + this.sourceRectangle = sourceRectangle; } /// /// Gets the source image size. /// - internal Size Size => this.rectangle.Size; + internal Size Size => this.sourceRectangle.Size; /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -145,32 +145,32 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(Vector2 position) - => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Prepends a raw matrix. @@ -203,9 +203,9 @@ namespace SixLabors.ImageSharp.Processing Matrix3x2 matrix = Matrix3x2.Identity; // Translate the origin matrix to cater for source rectangle offsets. - if (!this.rectangle.Equals(default)) + if (!this.sourceRectangle.Equals(default)) { - matrix *= Matrix3x2.CreateTranslation(-this.rectangle.Location); + matrix *= Matrix3x2.CreateTranslation(-this.sourceRectangle.Location); } foreach (Matrix3x2 m in this.matrices) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index be4c675189..6c5fb46259 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -14,18 +14,15 @@ namespace SixLabors.ImageSharp.Processing public class ProjectiveTransformBuilder { private readonly List matrices = new List(); - private Rectangle rectangle; + private Rectangle sourceRectangle; /// /// Initializes a new instance of the class. /// /// The source image size. public ProjectiveTransformBuilder(Size sourceSize) + : this(new Rectangle(Point.Empty, sourceSize)) { - Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); - Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); - - this.Size = sourceSize; } /// @@ -33,13 +30,18 @@ namespace SixLabors.ImageSharp.Processing /// /// The source rectangle. public ProjectiveTransformBuilder(Rectangle sourceRectangle) - : this(sourceRectangle.Size) - => this.rectangle = sourceRectangle; + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + this.sourceRectangle = sourceRectangle; + } /// /// Gets the source image size. /// - internal Size Size { get; } + internal Size Size => this.sourceRectangle.Size; + /// /// Prepends a matrix that performs a tapering projective transform. @@ -61,6 +63,96 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + public void AppendRotationRadians(float radians) + { + throw new System.NotImplementedException(); + } + + public void PrependRotationRadians(float radians) + { + throw new System.NotImplementedException(); + } + + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + /// /// Prepends a raw matrix. /// @@ -92,9 +184,9 @@ namespace SixLabors.ImageSharp.Processing Matrix4x4 matrix = Matrix4x4.Identity; // Translate the origin matrix to cater for source rectangle offsets. - if (!this.rectangle.Equals(default)) + if (!this.sourceRectangle.Equals(default)) { - matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.rectangle.Location, 0)); + matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.sourceRectangle.Location, 0)); } foreach (Matrix4x4 m in this.matrices) @@ -109,5 +201,6 @@ namespace SixLabors.ImageSharp.Processing /// Removes all matrices from the builder. /// public void Clear() => this.matrices.Clear(); + } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 3dfc42d4f7..0d9bd301dc 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - public class ProjectiveTransformBuilderTests + public class ProjectiveTransformBuilderTests : TransformBuilderTestBase { [Fact] public void ConstructorAssignsProperties() @@ -34,45 +34,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); }); } - - [Fact] - public void AppendPrependOpposite() - { - var rectangle = new Rectangle(-1, -1, 3, 3); - var b1 = new ProjectiveTransformBuilder(rectangle); - var b2 = new ProjectiveTransformBuilder(rectangle); - - const float pi = (float)Math.PI; - - Matrix4x4 m4 = Matrix4x4.Identity; - m4.M31 = pi; - - // Forwards - b1.AppendMatrix(m4) - .AppendTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi); - - // Backwards - b2.PrependTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi) - .PrependMatrix(m4); - - Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); - } - - [Fact] - public void BuilderCanClear() + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + + protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + + protected override Vector2 Execute( + ProjectiveTransformBuilder builder, + Rectangle rectangle, + Vector2 sourcePoint) { - var rectangle = new Rectangle(0, 0, 3, 3); - var builder = new ProjectiveTransformBuilder(rectangle); - Matrix4x4 matrix = Matrix4x4.Identity; - matrix.M31 = (float)Math.PI; - - Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); - - builder.AppendMatrix(matrix); - Assert.NotEqual(Matrix4x4.Identity, builder.BuildMatrix()); - - builder.Clear(); - Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + Matrix4x4 matrix = builder.BuildMatrix(); + return Vector2.Transform(sourcePoint, matrix); } } } From e5f7b12c7ee165b2996cfe3d704c9ce6ac2e6e2d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:29:34 +0100 Subject: [PATCH 15/28] extend ProjectiveTransformBuilder --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 2 +- .../Processing/AffineTransformBuilder.cs | 29 +++++++------- .../Processing/ProjectiveTransformBuilder.cs | 38 ++++++++++++++++--- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 4 +- .../Transforms/AffineTransformTests.cs | 8 ++-- .../ProjectiveTransformBuilderTests.cs | 5 ++- .../Transforms/TransformBuilderTestBase.cs | 2 +- 8 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 45556741e6..c0c8e07e6f 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp /// Converts degrees to radians /// [MethodImpl(InliningOptions.ShortMethod)] - public static float ToRadian(float degrees) + public static float DegreesToRadians(float degrees) { return degrees * ((float)Math.PI / 180f); } diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index cd66272b42..1f26d079d4 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,37 +42,38 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; + /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a centered rotation matrix using the given rotation in radians. /// - /// The amount of rotation, in degrees. + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + public AffineTransformBuilder PrependCenteredRotationRadians(float radians) + => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// - /// Prepends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotationRadians(float radians) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + public AffineTransformBuilder AppendCenteredRotationRadians(float radians) + => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Prepends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.ToRadian(degrees)); + public AffineTransformBuilder PrependCenteredRotationDegrees(float degrees) + => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in degrees. /// - /// The amount of rotation, in radians. + /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + public AffineTransformBuilder AppendCenteredRotationDegrees(float degrees) + => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 6c5fb46259..58c02108ac 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); this.sourceRectangle = sourceRectangle; - } + } /// /// Gets the source image size. @@ -63,16 +63,44 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - public void AppendRotationRadians(float radians) + /// + /// Prepends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder PrependCenteredRotationRadians(float radians) { - throw new System.NotImplementedException(); + var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + return this.PrependMatrix(m); } - public void PrependRotationRadians(float radians) + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder AppendCenteredRotationRadians(float radians) { - throw new System.NotImplementedException(); + var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + return this.AppendMatrix(m); } + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) + => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder AppendCenteredRotationDegrees(float degrees) + => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + /// /// Prepends a scale matrix from the given uniform scale. /// diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index a29342ad46..2f89236544 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) - .AppendRotationDegrees(45F) + .AppendCenteredRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 639f4b46fa..2a43e56e70 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -38,11 +38,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index e78bb2c6d9..f86db5641f 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(30); + .AppendCenteredRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(angleDeg) + .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); @@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(angleDeg) + .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(50) + .AppendCenteredRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 0d9bd301dc..f467bed40c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); }); } + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); protected override Vector2 Execute( ProjectiveTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index d109387cc4..3f259c9aa2 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder builder, float radians); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, ImageMaths.ToRadian(degrees)); + this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); From 59f47d457b1c72d8ae2baa49fdf351932f7bb9fd Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:31:05 +0100 Subject: [PATCH 16/28] fix trailing whitespace warnings --- src/ImageSharp/Processing/AffineTransformBuilder.cs | 1 - src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 1f26d079d4..a1305d1212 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,7 +42,6 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; - /// /// Prepends a centered rotation matrix using the given rotation in radians. /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 58c02108ac..9bcbaf8343 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -42,7 +42,6 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; - /// /// Prepends a matrix that performs a tapering projective transform. /// @@ -229,6 +228,5 @@ namespace SixLabors.ImageSharp.Processing /// Removes all matrices from the builder. /// public void Clear() => this.matrices.Clear(); - } } \ No newline at end of file From 103d12901234874c91131faeedc1123088830581 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:09:47 +0100 Subject: [PATCH 17/28] Make AffineTransformProcessor and AffineTransformBuilder API more flexible --- .../Processing/AffineTransformBuilder.cs | 83 ++++++++----------- .../Transforms/AffineTransformProcessor.cs | 6 +- .../Processing/TransformExtensions.cs | 71 ++++++++++++++-- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 28 +------ .../Transforms/AffineTransformTests.cs | 18 ++-- .../ProjectiveTransformBuilderTests.cs | 2 + .../Transforms/TransformBuilderTestBase.cs | 18 +++- 8 files changed, 136 insertions(+), 92 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index a1305d1212..42d555f203 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -13,34 +14,7 @@ namespace SixLabors.ImageSharp.Processing /// public class AffineTransformBuilder { - private readonly List matrices = new List(); - private readonly Rectangle sourceRectangle; - - /// - /// Initializes a new instance of the class. - /// - /// The source image size. - public AffineTransformBuilder(Size sourceSize) - : this(new Rectangle(Point.Empty, sourceSize)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The source rectangle. - public AffineTransformBuilder(Rectangle sourceRectangle) - { - Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); - Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - - this.sourceRectangle = sourceRectangle; - } - - /// - /// Gets the source image size. - /// - internal Size Size => this.sourceRectangle.Size; + private readonly List> matrixFactories = new List>(); /// /// Prepends a centered rotation matrix using the given rotation in radians. @@ -48,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public AffineTransformBuilder PrependCenteredRotationRadians(float radians) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// /// Appends a centered rotation matrix using the given rotation in radians. @@ -56,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendCenteredRotationRadians(float radians) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -129,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + => this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); /// /// Appends a centered skew matrix from the give angles in degrees. @@ -138,7 +112,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + => this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); /// /// Prepends a translation matrix from the given vector. @@ -179,7 +153,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) { - this.matrices.Insert(0, matrix); + this.matrixFactories.Insert(0, _ => matrix); return this; } @@ -190,35 +164,50 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) { - this.matrices.Add(matrix); + this.matrixFactories.Add(_ => matrix); return this; } /// - /// Returns the combined matrix. + /// Returns the combined matrix for a given source size. + /// + /// The source image size + /// The . + public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); + + /// + /// Returns the combined matrix for a given source rectangle. /// + /// The rectangle in the source image /// The . - public Matrix3x2 BuildMatrix() + public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) { - Matrix3x2 matrix = Matrix3x2.Identity; + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); // Translate the origin matrix to cater for source rectangle offsets. - if (!this.sourceRectangle.Equals(default)) - { - matrix *= Matrix3x2.CreateTranslation(-this.sourceRectangle.Location); - } + var matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - foreach (Matrix3x2 m in this.matrices) + Size size = sourceRectangle.Size; + + foreach (Func factory in this.matrixFactories) { - matrix *= m; + matrix *= factory(size); } return matrix; } - /// - /// Removes all matrices from the builder. - /// - public void Clear() => this.matrices.Clear(); + private AffineTransformBuilder Prepend(Func factory) + { + this.matrixFactories.Insert(0, factory); + return this; + } + + private AffineTransformBuilder Append(Func factory) + { + this.matrixFactories.Add(factory); + return this; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 2370adb862..f357b6c277 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -24,13 +24,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) + /// The target dimensions. + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) { Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); + this.TargetDimensions = targetDimensions;; } /// diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index ea789eb3d7..de4a3e5ba7 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -1,8 +1,11 @@ // 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.Processors.Transforms; +using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { @@ -11,6 +14,29 @@ namespace SixLabors.ImageSharp.Processing /// public static class TransformExtensions { + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + Matrix3x2 transform, + Size targetDimensions, + IResampler sampler) + where TPixel : struct, IPixel + { + return ctx.ApplyProcessor( + new AffineTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + } + /// /// Performs an affine transform of an image. /// @@ -18,7 +44,9 @@ namespace SixLabors.ImageSharp.Processing /// The image to transform. /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + AffineTransformBuilder builder) where TPixel : struct, IPixel => Transform(source, builder, KnownResamplers.Bicubic); @@ -26,13 +54,39 @@ namespace SixLabors.ImageSharp.Processing /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . + /// The source rectangle + /// The affine transform builder. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + AffineTransformBuilder builder, + IResampler sampler) + where TPixel : struct, IPixel + { + Matrix3x2 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform); + return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . /// The affine transform builder. /// The to perform the resampling. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder, IResampler sampler) + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + AffineTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); + { + return ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + } /// /// Performs a projective transform of an image. @@ -41,7 +95,9 @@ namespace SixLabors.ImageSharp.Processing /// The image to transform. /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder) where TPixel : struct, IPixel => Transform(source, builder, KnownResamplers.Bicubic); @@ -53,7 +109,10 @@ namespace SixLabors.ImageSharp.Processing /// The projective transform builder. /// The to perform the resampling. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder, IResampler sampler) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); } diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 2f89236544..f3772e3aa3 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests using (Image image = provider.GetImage()) using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { - AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 2a43e56e70..cbbf59caf5 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -12,30 +12,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { - [Fact] - public void ConstructorAssignsProperties() - { - var s = new Size(1, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - Assert.Equal(s, builder.Size); - } - - [Fact] - public void ConstructorThrowsInvalid() - { - Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); - - Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); - } - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); @@ -44,12 +20,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); + protected override Vector2 Execute( AffineTransformBuilder builder, Rectangle rectangle, Vector2 sourcePoint) { - Matrix3x2 matrix = builder.BuildMatrix(); + Matrix3x2 matrix = builder.BuildMatrix(rectangle); return Vector2.Transform(sourcePoint, matrix); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index f86db5641f..0c167d7f97 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler resampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); @@ -101,12 +101,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { image.DebugSave(provider, $"_original"); - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); - this.PrintMatrix(builder.BuildMatrix()); + this.PrintMatrix(builder.BuildMatrix(image.Size())); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); @@ -159,10 +159,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { image.DebugSave(provider, $"_original"); - AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(2, 1.5F)); - image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); + image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -178,10 +178,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(1F, 2F)); - image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); + image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index f467bed40c..14ed57033e 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -35,6 +35,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }); } + protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(rectangle); + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 3f259c9aa2..648ed5861f 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -129,9 +129,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(p1, p2, Comparer); } + [Theory] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(-1, 0)] + public void ThrowsForInvalidSizes(int width, int height) + { + var size = new Size(width, height); + + Assert.ThrowsAny( + () => + { + TBuilder builder = this.CreateBuilder(size); + this.Execute(builder, new Rectangle(Point.Empty, size), Vector2.Zero); + }); + } + protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size)); - protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle); + protected abstract TBuilder CreateBuilder(Rectangle rectangle); protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void AppendScale(TBuilder builder, SizeF scale); From eab8a5677c73074f68cb369814a27beaf2e57641 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:19:53 +0100 Subject: [PATCH 18/28] fix rotate and skew processors --- .../Processors/Transforms/RotateProcessor.cs | 10 +++++++++- .../Processing/Processors/Transforms/SkewProcessor.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 57cca4bf9e..69ecf59215 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; + using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.ParallelUtils; @@ -34,12 +36,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the rotating operation. /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) - : base( + : this( TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; + // Helper constructor + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, rotationMatrix)) + { + } + /// /// Gets the angle of rotation in degrees. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 4a006a9dfe..c7b1d74104 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Numerics; + using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -32,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the skew operation. /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) - : base( + : this( TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize), sampler, sourceSize) @@ -41,6 +43,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.DegreesY = degreesY; } + // Helper constructor: + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, skewMatrix)) + { + } + /// /// Gets the angle of rotation along the x-axis in degrees. /// From 81e16e48a3141ecd1e5fdfd62ab07cc6449600bb Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:29:31 +0100 Subject: [PATCH 19/28] StyleCop --- .../Processors/Transforms/AffineTransformProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index f357b6c277..aabaa63cf9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions;; + this.TargetDimensions = targetDimensions; } /// From bbfb2a0ef9249183bce9f8128d4ad058361a8ef2 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 02:37:10 +0100 Subject: [PATCH 20/28] Rotation around custom center point. Rename PrependCenteredRotationRadians back to PrependRotationRadians --- .../Processing/AffineTransformBuilder.cs | 40 ++++++++++++---- .../Processing/ProjectiveTransformBuilder.cs | 35 ++++++++++++-- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 8 +++- .../Transforms/AffineTransformTests.cs | 8 ++-- .../ProjectiveTransformBuilderTests.cs | 8 +++- .../Transforms/TransformBuilderTestBase.cs | 46 +++++++++++++++++-- 7 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 42d555f203..eab75cfdbe 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -20,33 +20,55 @@ namespace SixLabors.ImageSharp.Processing /// Prepends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center point /// The . - public AffineTransformBuilder PrependCenteredRotationRadians(float radians) - => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + => this.PrependMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center point + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) + => this.AppendMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); + + /// + /// Prepends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder PrependRotationRadians(float radians) + => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + + /// + /// Appends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendCenteredRotationRadians(float radians) + public AffineTransformBuilder AppendRotationRadians(float radians) => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public AffineTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Appends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendCenteredRotationDegrees(float degrees) - => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 9bcbaf8343..8bde90d51a 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -67,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public ProjectiveTransformBuilder PrependCenteredRotationRadians(float radians) + public ProjectiveTransformBuilder PrependRotationRadians(float radians) { var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); return this.PrependMatrix(m); @@ -78,27 +79,51 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public ProjectiveTransformBuilder AppendCenteredRotationRadians(float radians) + public ProjectiveTransformBuilder AppendRotationRadians(float radians) { var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); return this.AppendMatrix(m); } + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation center. + /// The . + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + { + var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); + return this.PrependMatrix(m); + } + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation center. + /// The . + internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) + { + var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); + return this.AppendMatrix(m); + } + /// /// Prepends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . - public ProjectiveTransformBuilder AppendCenteredRotationDegrees(float degrees) - => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index f3772e3aa3..374454afba 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(45F) + .AppendRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index cbbf59caf5..e025851684 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -14,11 +14,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => + builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => + builder.PrependRotationRadians(radians, center); protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 0c167d7f97..ed6d3ef2bc 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(30); + .AppendRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(angleDeg) + .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); @@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(angleDeg) + .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(50) + .AppendRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 14ed57033e..01448ac596 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -39,11 +39,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => + builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => + builder.PrependRotationRadians(radians, center); protected override Vector2 Execute( ProjectiveTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 648ed5861f..badf430132 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -87,7 +87,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms [InlineData(200, 100, 10, 42, 84)] [InlineData(200, 100, 100, 42, 84)] [InlineData(100, 200, -10, 42, 84)] - public void RotateDegrees_ShouldCreateCenteredMatrix(int width, int height, float deg, float x, float y) + public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter( + int width, + int height, + float deg, + float x, + float y) { var size = new Size(width, height); TBuilder builder = this.CreateBuilder(size); @@ -97,13 +102,41 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); - var position = new Vector2(x, y); + var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); Assert.Equal(actual, expected, Comparer); } - + + [Theory] + [InlineData(200, 100, 10, 30, 61, 42, 84)] + [InlineData(200, 100, 100, 30, 10, 20, 84)] + [InlineData(100, 200, -10, 30, 20, 11, 84)] + public void AppendRotationDegrees_WithRotationCenter( + int width, + int height, + float deg, + float cx, + float cy, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + var centerPoint = new Vector2(cx, cy); + this.AppendRotationDegrees(builder, deg, centerPoint); + + var matrix = Matrix3x2.CreateRotation(ImageMaths.DegreesToRadians(deg), centerPoint); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + [Fact] public void AppendPrependOpposite() { @@ -116,10 +149,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // Forwards this.AppendRotationRadians(b1, pi); this.AppendScale(b1, new SizeF(2, 0.5f)); + this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f)); this.AppendTranslation(b1, new PointF(123, 321)); // Backwards this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependScale(b2, new SizeF(2, 0.5f)); this.PrependRotationRadians(b2, pi); @@ -152,14 +187,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void AppendScale(TBuilder builder, SizeF scale); protected abstract void AppendRotationRadians(TBuilder builder, float radians); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); protected abstract void PrependTranslation(TBuilder builder, PointF translate); protected abstract void PrependScale(TBuilder builder, SizeF scale); protected abstract void PrependRotationRadians(TBuilder builder, float radians); + protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); + protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => + this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees), center); + protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); private static float Sqrt(float a) => (float)Math.Sqrt(a); From 5d034d5fc0f51d593a7ae2b7a3b827308d3b9415 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 18:16:47 +0000 Subject: [PATCH 21/28] Use existing method. (There's precedence) --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 14 +------------- .../Processing/AffineTransformBuilder.cs | 4 ++-- .../Processing/ProjectiveTransformBuilder.cs | 5 ++--- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index c0c8e07e6f..8720140e13 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -125,19 +125,7 @@ namespace SixLabors.ImageSharp /// should be power of 2. /// [MethodImpl(InliningOptions.ShortMethod)] - public static int ModuloP2(int x, int m) - { - return x & (m - 1); - } - - /// - /// Converts degrees to radians - /// - [MethodImpl(InliningOptions.ShortMethod)] - public static float DegreesToRadians(float degrees) - { - return degrees * ((float)Math.PI / 180f); - } + public static int ModuloP2(int x, int m) => x & (m - 1); /// /// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation. diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index eab75cfdbe..a91d1f0d2a 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Appends a rotation matrix using the given rotation angle in degrees @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 8bde90d51a..ad6ef20bb1 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -115,7 +114,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -123,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. From cfa7cfd3db4ecdd9d62e7ae14cfc4221dca99473 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 18:22:39 +0000 Subject: [PATCH 22/28] Remove "Matrix" suffix --- src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 4 ++-- .../Processing/Transforms/ProjectiveTransformTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index ad6ef20bb1..dff9bc6d94 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing /// An enumeration that indicates on which corners to taper the rectangle. /// The amount to taper. /// The . - public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// An enumeration that indicates on which corners to taper the rectangle. /// The amount to taper. /// The . - public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 0362d75a0b..9436b7828c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) - .AppendTaperMatrix(TaperSide.Right, TaperCorner.Both, .5F); + .AppendTaper(TaperSide.Right, TaperCorner.Both, .5F); image.Mutate(i => i.Transform(builder, sampler)); @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) - .AppendTaperMatrix(taperSide, taperCorner, .5F); + .AppendTaper(taperSide, taperCorner, .5F); image.Mutate(i => i.Transform(builder)); From ffba817bcae3a2895534c037798bf8e30232e06d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 22:59:31 +0000 Subject: [PATCH 23/28] Update dependencies --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 4 ++-- .../Converters/CIeLchToCieLabConverter.cs | 2 +- .../Converters/CieLabToCieLchConverter.cs | 2 +- .../Converters/CieLchuvToCieLuvConverter.cs | 2 +- .../Converters/CieLuvToCieLchuvConverter.cs | 2 +- src/ImageSharp/ImageSharp.csproj | 2 +- src/ImageSharp/Processing/AffineTransformBuilder.cs | 4 ++-- src/ImageSharp/Processing/KnownFilterMatrices.cs | 2 +- .../Processing/ProjectiveTransformBuilder.cs | 9 ++------- .../Processing/Transforms/TransformBuilderTestBase.cs | 10 ++++------ 10 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 1cb3f444f0..ae5069be1d 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -38,8 +38,8 @@ - - + + All diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs index dd352db809..40d8c5bc69 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation // Conversion algorithm described here: // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC float l = input.L, c = input.C, hDegrees = input.H; - float hRadians = MathFExtensions.DegreeToRadian(hDegrees); + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); float a = c * MathF.Cos(hRadians); float b = c * MathF.Sin(hRadians); diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs index 81196604e5..2b859205a0 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation float l = input.L, a = input.A, b = input.B; float c = MathF.Sqrt((a * a) + (b * b)); float hRadians = MathF.Atan2(b, a); - float hDegrees = MathFExtensions.RadianToDegree(hRadians); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); // Wrap the angle round at 360. hDegrees %= 360; diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs index 4f5a20bec7..ba5b8bfb79 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation // Conversion algorithm described here: // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29 float l = input.L, c = input.C, hDegrees = input.H; - float hRadians = MathFExtensions.DegreeToRadian(hDegrees); + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); float u = c * MathF.Cos(hRadians); float v = c * MathF.Sin(hRadians); diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs index 297c18c5c3..3c7d356a5e 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation float l = input.L, a = input.U, b = input.V; float c = MathF.Sqrt((a * a) + (b * b)); float hRadians = MathF.Atan2(b, a); - float hDegrees = MathFExtensions.RadianToDegree(hRadians); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); // Wrap the angle round at 360. hDegrees %= 360; diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 29d29d50d8..c19c92426f 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -42,7 +42,7 @@ - + All diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index a91d1f0d2a..ed5aa987f4 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a rotation matrix using the given rotation angle in degrees @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/KnownFilterMatrices.cs b/src/ImageSharp/Processing/KnownFilterMatrices.cs index 4f5e3c8697..cf0d19ff85 100644 --- a/src/ImageSharp/Processing/KnownFilterMatrices.cs +++ b/src/ImageSharp/Processing/KnownFilterMatrices.cs @@ -322,7 +322,7 @@ namespace SixLabors.ImageSharp.Processing degrees += 360; } - float radian = MathFExtensions.DegreeToRadian(degrees); + float radian = GeometryUtilities.DegreeToRadian(degrees); float cosRadian = MathF.Cos(radian); float sinRadian = MathF.Sin(radian); diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index dff9bc6d94..750f2f5d0e 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. @@ -247,10 +247,5 @@ namespace SixLabors.ImageSharp.Processing return matrix; } - - /// - /// Removes all matrices from the builder. - /// - public void Clear() => this.matrices.Clear(); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index badf430132..dffa3c7ac6 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -3,8 +3,6 @@ using System; using System.Numerics; - -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; @@ -128,7 +126,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var centerPoint = new Vector2(cx, cy); this.AppendRotationDegrees(builder, deg, centerPoint); - var matrix = Matrix3x2.CreateRotation(ImageMaths.DegreesToRadians(deg), centerPoint); + var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(deg), centerPoint); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -195,13 +193,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); + this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => - this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees), center); + this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); - + private static float Sqrt(float a) => (float)Math.Sqrt(a); } } \ No newline at end of file From 7ea99129b5745fd780b11d4f84a959d1cdb5c406 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 00:46:32 +0000 Subject: [PATCH 24/28] Match APIs --- .../Processing/AffineTransformBuilder.cs | 114 +++++----- .../ProjectiveTransformProcessor.cs | 6 +- .../Processing/ProjectiveTransformBuilder.cs | 210 ++++++++---------- .../Processing/TransformExtensions.cs | 96 +++++--- .../Transforms/AffineTransformBuilderTests.cs | 15 +- .../ProjectiveTransformBuilderTests.cs | 40 +--- .../Transforms/ProjectiveTransformTests.cs | 6 +- .../Transforms/TransformBuilderTestBase.cs | 12 +- 8 files changed, 246 insertions(+), 253 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ed5aa987f4..29c37d7bf0 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -17,22 +17,13 @@ namespace SixLabors.ImageSharp.Processing private readonly List> matrixFactories = new List>(); /// - /// Prepends a centered rotation matrix using the given rotation in radians. - /// - /// The amount of rotation, in radians. - /// The rotation center point - /// The . - public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - => this.PrependMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); - - /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Prepends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// - /// The amount of rotation, in radians. - /// The rotation center point + /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - => this.AppendMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); + public AffineTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a rotation matrix using the given rotation angle in radians @@ -44,31 +35,40 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Appends a rotation matrix using the given rotation angle in radians - /// and the image center point as rotation center. + /// Prepends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation origin point. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateRotation(radians, origin)); /// - /// Prepends a rotation matrix using the given rotation angle in degrees + /// Appends a rotation matrix using the given rotation angle in degrees /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// - /// Appends a rotation matrix using the given rotation angle in degrees + /// Appends a rotation matrix using the given rotation angle in radians /// and the image center point as rotation center. /// - /// The amount of rotation, in degrees. + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + public AffineTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateRotation(radians, origin)); /// /// Prepends a scale matrix from the given uniform scale. @@ -79,12 +79,12 @@ namespace SixLabors.ImageSharp.Processing => this.PrependMatrix(Matrix3x2.CreateScale(scale)); /// - /// Appends a scale matrix from the given uniform scale. + /// Prepends a scale matrix from the given vector scale. /// - /// The uniform scale. + /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScale(float scale) - => this.AppendMatrix(Matrix3x2.CreateScale(scale)); + public AffineTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); /// /// Prepends a scale matrix from the given vector scale. @@ -95,28 +95,28 @@ namespace SixLabors.ImageSharp.Processing => this.PrependMatrix(Matrix3x2.CreateScale(scales)); /// - /// Appends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given uniform scale. /// - /// The horizontal and vertical scale. + /// The uniform scale. /// The . - public AffineTransformBuilder AppendScale(Vector2 scales) - => this.AppendMatrix(Matrix3x2.CreateScale(scales)); + public AffineTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix3x2.CreateScale(scale)); /// - /// Prepends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given vector scale. /// - /// The horizontal and vertical scale. + /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder PrependScale(SizeF scale) - => this.PrependScale((Vector2)scale); + public AffineTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScale(SizeF scales) - => this.AppendScale((Vector2)scales); + public AffineTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix3x2.CreateScale(scales)); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -141,66 +141,58 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(Vector2 position) - => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// - /// Appends a translation matrix from the given vector. + /// Prepends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// - /// Prepends a translation matrix from the given vector. + /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a raw matrix. /// /// The matrix to prepend. /// The . - public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) - { - this.matrixFactories.Insert(0, _ => matrix); - return this; - } + public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) => this.Prepend(_ => matrix); /// /// Appends a raw matrix. /// /// The matrix to append. /// The . - public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) - { - this.matrixFactories.Add(_ => matrix); - return this; - } + public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) => this.Append(_ => matrix); /// /// Returns the combined matrix for a given source size. /// - /// The source image size + /// The source image size. /// The . public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); /// /// Returns the combined matrix for a given source rectangle. /// - /// The rectangle in the source image + /// The rectangle in the source image. /// The . public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index bfde1769c1..273156e2eb 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -24,13 +24,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size. - public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) + /// The target dimensions. + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions) { Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); + this.TargetDimensions = targetDimensions; } /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 750f2f5d0e..a905467bba 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -13,34 +14,7 @@ namespace SixLabors.ImageSharp.Processing /// public class ProjectiveTransformBuilder { - private readonly List matrices = new List(); - private Rectangle sourceRectangle; - - /// - /// Initializes a new instance of the class. - /// - /// The source image size. - public ProjectiveTransformBuilder(Size sourceSize) - : this(new Rectangle(Point.Empty, sourceSize)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The source rectangle. - public ProjectiveTransformBuilder(Rectangle sourceRectangle) - { - Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); - Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - - this.sourceRectangle = sourceRectangle; - } - - /// - /// Gets the source image size. - /// - internal Size Size => this.sourceRectangle.Size; + private readonly List> matrixFactories = new List>(); /// /// Prepends a matrix that performs a tapering projective transform. @@ -50,7 +24,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) - => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -60,7 +34,15 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) - => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a centered rotation matrix using the given rotation in radians. @@ -68,33 +50,32 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - { - var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); - return this.PrependMatrix(m); - } + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center. /// The . - public ProjectiveTransformBuilder AppendRotationRadians(float radians) - { - var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); - return this.AppendMatrix(m); - } + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. - /// The rotation center. /// The . - internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - { - var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); - return this.PrependMatrix(m); - } + public ProjectiveTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in radians. @@ -103,80 +84,69 @@ namespace SixLabors.ImageSharp.Processing /// The rotation center. /// The . internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - { - var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); - return this.AppendMatrix(m); - } + => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a scale matrix from the given uniform scale. /// - /// The amount of rotation, in degrees. - /// The . - public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix4x4.CreateScale(scale)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Prepends a scale matrix from the given vector scale. /// - /// The amount of rotation, in degrees. - /// The . - public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); /// - /// Prepends a scale matrix from the given uniform scale. + /// Prepends a scale matrix from the given vector scale. /// - /// The uniform scale. - /// The . - public ProjectiveTransformBuilder PrependScale(float scale) - => this.PrependMatrix(Matrix4x4.CreateScale(scale)); + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); /// /// Appends a scale matrix from the given uniform scale. /// /// The uniform scale. - /// The . + /// The . public ProjectiveTransformBuilder AppendScale(float scale) => this.AppendMatrix(Matrix4x4.CreateScale(scale)); /// - /// Prepends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder PrependScale(Vector2 scales) - => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + /// The . + public ProjectiveTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. - /// The . + /// The . public ProjectiveTransformBuilder AppendScale(Vector2 scales) - => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); - - /// - /// Prepends a scale matrix from the given vector scale. - /// - /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder PrependScale(SizeF scale) - => this.PrependScale((Vector2)scale); + => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); /// - /// Appends a scale matrix from the given vector scale. + /// Prepends a translation matrix from the given vector. /// - /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder AppendScale(SizeF scales) - => this.AppendScale((Vector2)scales); + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// /// Prepends a translation matrix from the given vector. /// /// The translation position. - /// The . + /// The . public ProjectiveTransformBuilder PrependTranslation(Vector2 position) => this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); @@ -184,68 +154,72 @@ namespace SixLabors.ImageSharp.Processing /// Appends a translation matrix from the given vector. /// /// The translation position. - /// The . - public ProjectiveTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); - - /// - /// Prepends a translation matrix from the given vector. - /// - /// The translation position. - /// The . - public ProjectiveTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + /// The . + public ProjectiveTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. - /// The . - public ProjectiveTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + /// The . + public ProjectiveTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); /// /// Prepends a raw matrix. /// /// The matrix to prepend. /// The . - public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) - { - this.matrices.Insert(0, matrix); - return this; - } + public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) => this.Prepend(_ => matrix); /// /// Appends a raw matrix. /// /// The matrix to append. /// The . - public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) - { - this.matrices.Add(matrix); - return this; - } + public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) => this.Append(_ => matrix); + + /// + /// Returns the combined matrix for a given source size. + /// + /// The source image size. + /// The . + public Matrix4x4 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); /// - /// Returns the combined matrix. + /// Returns the combined matrix for a given source rectangle. /// + /// The rectangle in the source image. /// The . - public Matrix4x4 BuildMatrix() + public Matrix4x4 BuildMatrix(Rectangle sourceRectangle) { - Matrix4x4 matrix = Matrix4x4.Identity; + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); // Translate the origin matrix to cater for source rectangle offsets. - if (!this.sourceRectangle.Equals(default)) - { - matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.sourceRectangle.Location, 0)); - } + var matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); + + Size size = sourceRectangle.Size; - foreach (Matrix4x4 m in this.matrices) + foreach (Func factory in this.matrixFactories) { - matrix *= m; + matrix *= factory(size); } return matrix; } + + private ProjectiveTransformBuilder Prepend(Func factory) + { + this.matrixFactories.Insert(0, factory); + return this; + } + + private ProjectiveTransformBuilder Append(Func factory) + { + this.matrixFactories.Add(factory); + return this; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index de4a3e5ba7..db14b6baf9 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -15,40 +15,32 @@ namespace SixLabors.ImageSharp.Processing public static class TransformExtensions { /// - /// Performs an affine transform of an image using the specified sampling algorithm. + /// Performs an affine transform of an image. /// /// The pixel format. - /// The . - /// The source rectangle - /// The transformation matrix. - /// The size of the result image. - /// The to perform the resampling. + /// The image to transform. + /// The affine transform builder. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext ctx, - Rectangle sourceRectangle, - Matrix3x2 transform, - Size targetDimensions, - IResampler sampler) + this IImageProcessingContext source, + AffineTransformBuilder builder) where TPixel : struct, IPixel - { - return ctx.ApplyProcessor( - new AffineTransformProcessor(transform, sampler, targetDimensions), - sourceRectangle); - } + => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Performs an affine transform of an image. + /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . /// The affine transform builder. + /// The to perform the resampling. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext source, - AffineTransformBuilder builder) + this IImageProcessingContext ctx, + AffineTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel - => Transform(source, builder, KnownResamplers.Bicubic); + => ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); /// /// Performs an affine transform of an image using the specified sampling algorithm. @@ -76,16 +68,22 @@ namespace SixLabors.ImageSharp.Processing /// /// The pixel format. /// The . - /// The affine transform builder. + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. /// The to perform the resampling. /// The public static IImageProcessingContext Transform( this IImageProcessingContext ctx, - AffineTransformBuilder builder, + Rectangle sourceRectangle, + Matrix3x2 transform, + Size targetDimensions, IResampler sampler) where TPixel : struct, IPixel { - return ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + return ctx.ApplyProcessor( + new AffineTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); } /// @@ -105,15 +103,59 @@ namespace SixLabors.ImageSharp.Processing /// Performs a projective transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . /// The projective transform builder. /// The to perform the resampling. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext source, + this IImageProcessingContext ctx, + ProjectiveTransformBuilder builder, + IResampler sampler) + where TPixel : struct, IPixel + => ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The projective transform builder. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, ProjectiveTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); + { + Matrix4x4 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform); + return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + Matrix4x4 transform, + Size targetDimensions, + IResampler sampler) + where TPixel : struct, IPixel + { + return ctx.ApplyProcessor( + new ProjectiveTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index e025851684..bdc66641a1 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -1,28 +1,31 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; -using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => - builder.PrependRotationRadians(radians, center); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => + builder.PrependRotationRadians(radians, origin); protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); @@ -35,4 +38,4 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms return Vector2.Transform(sourcePoint, matrix); } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 01448ac596..287d7be491 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -1,60 +1,40 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; using SixLabors.ImageSharp.Processing; using SixLabors.Primitives; -using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class ProjectiveTransformBuilderTests : TransformBuilderTestBase { - [Fact] - public void ConstructorAssignsProperties() - { - var s = new Size(1, 1); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - Assert.Equal(s, builder.Size); - } - - [Fact] - public void ConstructorThrowsInvalid() - { - Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - }); - - Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - }); - } - - protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(rectangle); + protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(); protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => - builder.PrependRotationRadians(radians, center); + + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => + builder.PrependRotationRadians(radians, origin); protected override Vector2 Execute( ProjectiveTransformBuilder builder, Rectangle rectangle, Vector2 sourcePoint) { - Matrix4x4 matrix = builder.BuildMatrix(); + Matrix4x4 matrix = builder.BuildMatrix(rectangle); return Vector2.Transform(sourcePoint, matrix); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 9436b7828c..1da660d222 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendTaper(TaperSide.Right, TaperCorner.Both, .5F); image.Mutate(i => i.Transform(builder, sampler)); @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendTaper(taperSide, taperCorner, .5F); image.Mutate(i => i.Transform(builder)); @@ -113,7 +113,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Matrix4x4 matrix = Matrix4x4.Identity; matrix.M13 = 0.01F; - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendMatrix(matrix); image.Mutate(i => i.Transform(builder)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index dffa3c7ac6..720c87cedf 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -18,7 +18,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms new TheoryData { // scale, translate, source, expectedDest - { Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero }, { Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) }, { Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) }, @@ -44,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms new TheoryData { // translate, scale, source, expectedDest - { Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero }, { Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) }, { new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) }, @@ -183,14 +181,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract TBuilder CreateBuilder(Rectangle rectangle); protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationRadians(TBuilder builder, float radians); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void PrependRotationRadians(TBuilder builder, float radians); - protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); + + protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); @@ -199,7 +203,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); - - private static float Sqrt(float a) => (float)Math.Sqrt(a); } } \ No newline at end of file From a1b74ea1cb3e1fecddcb7af74cb4c48a46734060 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 10:15:52 +0000 Subject: [PATCH 25/28] Add large jpeg to 32bit skip --- tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 5977e59cf3..15f7f92a83 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -49,7 +49,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.NoEoiProgressive517, TestImages.Jpeg.Issues.BadRstProgressive518, TestImages.Jpeg.Issues.InvalidEOI695, - TestImages.Jpeg.Issues.ExifResizeOutOfRange696 + TestImages.Jpeg.Issues.ExifResizeOutOfRange696, + TestImages.Jpeg.Issues.ExifGetString750Transform }; return !TestEnvironment.Is64BitProcess && largeImagesToSkipOn32Bit.Contains(provider.SourceFileOrDescription); From 964ebcc1c30e965ab66be49c99f1dfad5d47e2b9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 10:23:06 +0000 Subject: [PATCH 26/28] Remove unused variables --- .../Processors/Transforms/AffineTransformProcessor.cs | 4 +--- .../Processors/Transforms/ProjectiveTransformProcessor.cs | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index aabaa63cf9..5a3b5943b7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -69,14 +69,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Handle tranforms that result in output identical to the original. if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity)) { - // The cloned will be blank here copy all the pixel data over + // The clone will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } - int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 273156e2eb..98d488a420 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -65,14 +65,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Handle tranforms that result in output identical to the original. if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity)) { - // The cloned will be blank here copy all the pixel data over + // The clone will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } - int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. @@ -129,7 +127,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. - var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); From 35ff16d5a6cb4caf54df2eba7646dc4eabd393b3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 12:12:42 +0000 Subject: [PATCH 27/28] Add Rotate(degrees, origin) --- .../Processing/AffineTransformBuilder.cs | 22 ++++++++++-- .../Processing/ProjectiveTransformBuilder.cs | 34 ++++++++++++++----- .../Transforms/AffineTransformBuilderTests.cs | 7 ++-- .../ProjectiveTransformBuilderTests.cs | 7 ++-- .../Transforms/TransformBuilderTestBase.cs | 8 ++--- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 29c37d7bf0..ef0b24f815 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -35,7 +35,16 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Prepends a centered rotation matrix using the given rotation in radians. + /// Prepends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. /// The rotation origin point. @@ -62,7 +71,16 @@ namespace SixLabors.ImageSharp.Processing => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. /// The rotation origin point. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index a905467bba..5a19b890d9 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -53,13 +53,22 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a centered rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. - /// The rotation center. + /// The rotation origin point. /// The . - internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -78,13 +87,22 @@ namespace SixLabors.ImageSharp.Processing => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a centered rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. - /// The rotation center. + /// The rotation origin point. /// The . - internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index bdc66641a1..14b8672641 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -15,8 +15,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => - builder.AppendRotationRadians(radians, center); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 287d7be491..9f58826cd8 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -17,8 +17,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => - builder.AppendRotationRadians(radians, center); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 720c87cedf..43ad756e04 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -186,7 +186,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void AppendRotationRadians(TBuilder builder, float radians); - protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin); protected abstract void PrependTranslation(TBuilder builder, PointF translate); @@ -196,11 +196,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); - protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => - this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); } From 8cdb72888db1200b5d8dc333989a8150dd38bb11 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Nov 2018 14:21:53 +0000 Subject: [PATCH 28/28] Add Skew methods and tests. --- .../Processing/AffineTransformBuilder.cs | 58 ++++++++++++ .../Processors/Transforms/TransformUtils.cs | 12 +++ .../Processing/ProjectiveTransformBuilder.cs | 76 +++++++++++++++ .../Transforms/AffineTransformBuilderTests.cs | 51 +++++++--- .../ProjectiveTransformBuilderTests.cs | 30 ++++-- .../Transforms/TransformBuilderTestBase.cs | 94 ++++++++++++++++--- 6 files changed, 291 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ef0b24f815..c3d01241c9 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -145,6 +145,35 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) => this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + /// /// Appends a centered skew matrix from the give angles in degrees. /// @@ -154,6 +183,35 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) => this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + /// /// Prepends a translation matrix from the given vector. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 6cef38f8fe..24b15d3098 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -46,6 +46,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); + /// + /// Creates a centered skew matrix from the give angles in radians and the source size. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The source image size. + /// The . + public static Matrix3x2 CreateSkewMatrixRadians(float radiansX, float radiansY, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); + /// /// Gets the centered transform matrix based upon the source and destination rectangles. /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 5a19b890d9..c29941d071 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -152,6 +152,82 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendScale(Vector2 scales) => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + /// /// Prepends a translation matrix from the given vector. /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 14b8672641..70159e18ac 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -9,28 +9,55 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); - protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) + => builder.AppendRotationDegrees(degrees); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) + => builder.AppendRotationDegrees(degrees, origin); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) + => builder.AppendRotationRadians(radians); - protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) + => builder.AppendRotationRadians(radians, origin); - protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) + => builder.AppendScale(scale); - protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY) + => builder.AppendSkewDegrees(degreesX, degreesY); - protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) + => builder.AppendSkewDegrees(degreesX, degreesY, origin); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) + => builder.AppendSkewRadians(radiansX, radiansY); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => - builder.PrependRotationRadians(radians, origin); + protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.AppendSkewRadians(radiansX, radiansY, origin); - protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) + => builder.AppendTranslation(translate); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) + => builder.PrependRotationRadians(radians); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) + => builder.PrependRotationRadians(radians, origin); + + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) + => builder.PrependScale(scale); + + protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) + => builder.PrependSkewRadians(radiansX, radiansY); + + protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.PrependSkewRadians(radiansX, radiansY, origin); + + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) + => builder.PrependTranslation(translate); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 9f58826cd8..d82cd1689d 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -11,24 +11,42 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(); - protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); - protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); - protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); + protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY) + => builder.AppendSkewDegrees(degreesX, degreesY); - protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) + => builder.AppendSkewDegrees(degreesX, degreesY, origin); - protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) + => builder.AppendSkewRadians(radiansX, radiansY); + + protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.AppendSkewRadians(radiansX, radiansY, origin); + + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + + protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) + => builder.PrependSkewRadians(radiansX, radiansY); + + protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.PrependSkewRadians(radiansX, radiansY, origin); + + protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.PrependRotationRadians(radians, origin); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 43ad756e04..71e3b71797 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -86,17 +86,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter( int width, int height, - float deg, + float degrees, float x, float y) { var size = new Size(width, height); TBuilder builder = this.CreateBuilder(size); - this.AppendRotationDegrees(builder, deg); + this.AppendRotationDegrees(builder, degrees); // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness - Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); + Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(degrees, size); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -112,7 +112,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void AppendRotationDegrees_WithRotationCenter( int width, int height, - float deg, + float degrees, float cx, float cy, float x, @@ -122,9 +122,63 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms TBuilder builder = this.CreateBuilder(size); var centerPoint = new Vector2(cx, cy); - this.AppendRotationDegrees(builder, deg, centerPoint); + this.AppendRotationDegrees(builder, degrees, centerPoint); - var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(deg), centerPoint); + var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 10, 42, 84)] + [InlineData(200, 100, 100, 100, 42, 84)] + [InlineData(100, 200, -10, -10, 42, 84)] + public void AppendSkewDegrees_WithoutSpecificSkewCenter_SkewIsCenteredAroundImageCenter( + int width, + int height, + float degreesX, + float degreesY, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + this.AppendSkewDegrees(builder, degreesX, degreesY); + + Matrix3x2 matrix = TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 10, 30, 61, 42, 84)] + [InlineData(200, 100, 100, 100, 30, 10, 20, 84)] + [InlineData(100, 200, -10, -10, 30, 20, 11, 84)] + public void AppendSkewDegrees_WithSkewCenter( + int width, + int height, + float degreesX, + float degreesY, + float cx, + float cy, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + var centerPoint = new Vector2(cx, cy); + this.AppendSkewDegrees(builder, degreesX, degreesY, centerPoint); + + var matrix = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -144,14 +198,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // Forwards this.AppendRotationRadians(b1, pi); + this.AppendSkewRadians(b1, pi, pi); this.AppendScale(b1, new SizeF(2, 0.5f)); this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f)); + this.AppendSkewRadians(b1, pi, pi / 2, new Vector2(-0.5f, -0.1f)); this.AppendTranslation(b1, new PointF(123, 321)); // Backwards this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependSkewRadians(b2, pi, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependScale(b2, new SizeF(2, 0.5f)); + this.PrependSkewRadians(b2, pi, pi); this.PrependRotationRadians(b2, pi); Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65)); @@ -180,25 +238,37 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract TBuilder CreateBuilder(Rectangle rectangle); - protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); - protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); protected abstract void AppendRotationRadians(TBuilder builder, float radians); protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); - protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY); + + protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY, Vector2 origin); + + protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY); + + protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); + + protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void PrependRotationRadians(TBuilder builder, float radians); protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); + protected abstract void PrependScale(TBuilder builder, SizeF scale); - protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); + protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY); + + protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); + + protected abstract void PrependTranslation(TBuilder builder, PointF translate); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); }