diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs new file mode 100644 index 0000000000..4d70fba84f --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides the base methods to perform affine transforms on an image. + /// + /// The pixel format. + internal abstract class AffineProcessor : ResamplingWeightedProcessor + where TPixel : struct, IPixel + { + // TODO: Move to constants somewhere else to prevent generic type duplication. + private static readonly Rectangle DefaultRectangle = new Rectangle(0, 0, 1, 1); + + /// + /// Initializes a new instance of the class. + /// + /// The sampler to perform the resize operation. + protected AffineProcessor(IResampler sampler) + : base(sampler, 1, 1, DefaultRectangle) // Hack to prevent Guard throwing in base, we always set the canvas + { + } + + /// + /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. + /// + public bool Expand { get; set; } = true; + + /// + /// Returns the processing matrix used for transforming the image. + /// + /// The + protected abstract Matrix3x2 CreateProcessingMatrix(); + + /// + /// Creates a new target canvas to contain the results of the matrix transform. + /// + /// The source rectangle. + protected virtual void CreateNewCanvas(Rectangle sourceRectangle) + { + if (this.ResizeRectangle == DefaultRectangle) + { + if (this.Expand) + { + this.ResizeRectangle = Matrix3x2.Invert(this.CreateProcessingMatrix(), out Matrix3x2 sizeMatrix) + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; + } + else + { + this.ResizeRectangle = sourceRectangle; + } + } + + this.Width = this.ResizeRectangle.Width; + this.Height = this.ResizeRectangle.Height; + } + + /// + protected override Image CreateDestination(Image source, Rectangle sourceRectangle) + { + this.CreateNewCanvas(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(this.Width, this.Height, x.MetaData.Clone())); + + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + } + + /// + /// Gets a transform matrix adjusted to center upon the target image bounds. + /// + /// The source image. + /// The transform matrix. + /// + /// The . + /// + protected Matrix3x2 GetCenteredMatrix(ImageFrame source, Matrix3x2 matrix) + { + var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.ResizeRectangle.Width * .5F, -this.ResizeRectangle.Height * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); + return (translationToTargetCenter * matrix) * translateToSourceCenter; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs b/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs deleted file mode 100644 index 4a15254ab2..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Provides methods to transform an image using a . - /// - /// The pixel format. - internal abstract class Matrix3x2Processor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Gets the rectangle designating the target canvas. - /// - protected Rectangle CanvasRectangle { get; private set; } - - /// - /// Creates a new target canvas to contain the results of the matrix transform. - /// - /// The source rectangle. - /// The processing matrix. - protected void CreateNewCanvas(Rectangle sourceRectangle, Matrix3x2 processMatrix) - { - Matrix3x2 sizeMatrix; - this.CanvasRectangle = Matrix3x2.Invert(processMatrix, out sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; - } - - /// - /// Gets a transform matrix adjusted to center upon the target image bounds. - /// - /// The source image. - /// The transform matrix. - /// - /// The . - /// - protected Matrix3x2 GetCenteredMatrix(ImageFrame source, Matrix3x2 matrix) - { - var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.CanvasRectangle.Width * .5F, -this.CanvasRectangle.Height * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return (translationToTargetCenter * matrix) * translateToSourceCenter; - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 86a0c73603..f47d483596 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -6,7 +6,6 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -17,46 +16,84 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : Matrix3x2Processor + internal class RotateProcessor : AffineProcessor where TPixel : struct, IPixel { + private Matrix3x2 transformMatrix; + /// - /// The transform matrix to apply. + /// Initializes a new instance of the class. /// - private Matrix3x2 processMatrix; + public RotateProcessor() + : base(new BicubicResampler()) + { + } /// - /// Gets or sets the angle of processMatrix in degrees. + /// Initializes a new instance of the class. /// - public float Angle { get; set; } + /// The sampler to perform the resize operation. + public RotateProcessor(IResampler sampler) + : base(sampler) + { + } /// - /// Gets or sets a value indicating whether to expand the canvas to fit the rotated image. + /// Gets or sets the angle of processMatrix in degrees. /// - public bool Expand { get; set; } = true; + public float Angle { get; set; } /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override Matrix3x2 CreateProcessingMatrix() { - if (this.OptimizedApply(source, configuration)) + if (this.transformMatrix == default(Matrix3x2)) + { + this.transformMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0)); + } + + return this.transformMatrix; + } + + /// + protected override void CreateNewCanvas(Rectangle sourceRectangle) + { + if (MathF.Abs(this.Angle) < Constants.Epsilon || + MathF.Abs(this.Angle - 180) < Constants.Epsilon) + { + this.ResizeRectangle = sourceRectangle; + } + + if (MathF.Abs(this.Angle - 90) < Constants.Epsilon || + MathF.Abs(this.Angle - 270) < Constants.Epsilon) + { + // We always expand enumerated rectangle values + this.ResizeRectangle = new Rectangle(0, 0, sourceRectangle.Height, sourceRectangle.Width); + } + + base.CreateNewCanvas(sourceRectangle); + } + + /// + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + if (this.OptimizedApply(source, destination, configuration)) { return; } - int height = this.CanvasRectangle.Height; - int width = this.CanvasRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix); + int height = this.ResizeRectangle.Height; + int width = this.ResizeRectangle.Width; + Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + // TODO: Use our new weights functionality to resample on transform + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => { - Span targetRow = targetPixels.GetRowSpan(y); + Span destRow = destination.GetPixelRowSpan(y); for (int x = 0; x < width; x++) { @@ -64,34 +101,16 @@ namespace SixLabors.ImageSharp.Processing.Processors if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - targetRow[x] = source[transformedPoint.X, transformedPoint.Y]; + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; } } }); - - source.SwapPixelsBuffers(targetPixels); - } - } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - if (MathF.Abs(this.Angle) < Constants.Epsilon || MathF.Abs(this.Angle - 90) < Constants.Epsilon || MathF.Abs(this.Angle - 180) < Constants.Epsilon || MathF.Abs(this.Angle - 270) < Constants.Epsilon) - { - return; - } - - this.processMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0)); - if (this.Expand) - { - this.CreateNewCanvas(sourceRectangle, this.processMatrix); - } } /// - protected override void AfterImageApply(Image source, Rectangle sourceRectangle) + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) { - ExifProfile profile = source.MetaData.ExifProfile; + ExifProfile profile = destination.MetaData.ExifProfile; if (profile == null) { return; @@ -116,33 +135,35 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Rotates the images with an optimized method when the angle is 90, 180 or 270 degrees. /// /// The source image. + /// The destination image. /// The configuration. /// /// The /// - private bool OptimizedApply(ImageFrame source, Configuration configuration) + private bool OptimizedApply(ImageFrame source, ImageFrame destination, Configuration configuration) { if (MathF.Abs(this.Angle) < Constants.Epsilon) { - // No need to do anything so return. + // The destination will be blank here so copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return true; } if (MathF.Abs(this.Angle - 90) < Constants.Epsilon) { - this.Rotate90(source, configuration); + this.Rotate90(source, destination, configuration); return true; } if (MathF.Abs(this.Angle - 180) < Constants.Epsilon) { - this.Rotate180(source, configuration); + this.Rotate180(source, destination, configuration); return true; } if (MathF.Abs(this.Angle - 270) < Constants.Epsilon) { - this.Rotate270(source, configuration); + this.Rotate270(source, destination, configuration); return true; } @@ -153,95 +174,82 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Rotates the image 270 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate270(ImageFrame source, Configuration configuration) + private void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(height, width)) - { - using (PixelAccessor sourcePixels = source.Lock()) + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - for (int x = 0; x < width; x++) - { - int newX = height - y - 1; - newX = height - newX - 1; - int newY = width - x - 1; - targetPixels[newX, newY] = sourcePixels[x, y]; - } - }); - } + Span sourceRow = source.GetPixelRowSpan(y); + for (int x = 0; x < width; x++) + { + int newX = height - y - 1; + newX = height - newX - 1; + int newY = width - x - 1; - source.SwapPixelsBuffers(targetPixels); - } + destination[newX, newY] = sourceRow[x]; + } + }); } /// /// Rotates the image 180 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate180(ImageFrame source, Configuration configuration) + private void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(height - y - 1); - - for (int x = 0; x < width; x++) - { - targetRow[width - x - 1] = sourceRow[x]; - } - }); + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = destination.GetPixelRowSpan(height - y - 1); - source.SwapPixelsBuffers(targetPixels); - } + for (int x = 0; x < width; x++) + { + targetRow[width - x - 1] = sourceRow[x]; + } + }); } /// /// Rotates the image 90 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate90(ImageFrame source, Configuration configuration) + private void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(height, width)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span sourceRow = source.GetPixelRowSpan(y); + int newX = height - y - 1; + for (int x = 0; x < width; x++) { - Span sourceRow = source.GetPixelRowSpan(y); - int newX = height - y - 1; - for (int x = 0; x < width; x++) - { - targetPixels[newX, x] = sourceRow[x]; - } - }); - - source.SwapPixelsBuffers(targetPixels); - } + destination[newX, x] = sourceRow[x]; + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 316e2a2af3..ba84eab9e8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -6,7 +6,6 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -16,13 +15,19 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : Matrix3x2Processor + internal class SkewProcessor : AffineProcessor where TPixel : struct, IPixel { + private Matrix3x2 transformMatrix; + /// - /// The transform matrix to apply. + /// Initializes a new instance of the class. /// - private Matrix3x2 processMatrix; + /// The sampler to perform the skew operation. + public SkewProcessor(IResampler sampler) + : base(sampler) + { + } /// /// Gets or sets the angle of rotation along the x-axis in degrees. @@ -34,52 +39,44 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public float AngleY { get; set; } - /// - /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. - /// - public bool Expand { get; set; } = true; + /// + protected override Matrix3x2 CreateProcessingMatrix() + { + if (this.transformMatrix == default(Matrix3x2)) + { + this.transformMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0)); + } + + return this.transformMatrix; + } /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { - int height = this.CanvasRectangle.Height; - int width = this.CanvasRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix); + int height = this.ResizeRectangle.Height; + int width = this.ResizeRectangle.Width; + Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + // TODO: Use our new weights functionality to resample on transform + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) { - Span targetRow = targetPixels.GetRowSpan(y); + var transformedPoint = Point.Skew(new Point(x, y), matrix); - for (int x = 0; x < width; x++) + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - var transformedPoint = Point.Skew(new Point(x, y), matrix); - - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - targetRow[x] = source[transformedPoint.X, transformedPoint.Y]; - } + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; } - }); - - source.SwapPixelsBuffers(targetPixels); - } - } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - this.processMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0)); - if (this.Expand) - { - this.CreateNewCanvas(sourceRectangle, this.processMatrix); - } + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index 7b35a879bc..436aa3cd98 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; @@ -47,6 +46,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new RotateProcessor { Angle = degrees, Expand = expand }); + => source.ApplyProcessor(new RotateProcessor(new BicubicResampler()) { Angle = degrees, Expand = expand }); } } diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index 00411946d9..873dbe5100 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp @@ -37,6 +37,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new SkewProcessor { AngleX = degreesX, AngleY = degreesY, Expand = expand }); + => source.ApplyProcessor(new SkewProcessor(new BicubicResampler()) { AngleX = degreesX, AngleY = degreesY, Expand = expand }); } }